@mastra/livekit 0.0.0 → 0.2.0-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.
Files changed (44) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/LICENSE.md +30 -0
  3. package/README.md +270 -7
  4. package/dist/bridge.d.ts +153 -0
  5. package/dist/bridge.d.ts.map +1 -0
  6. package/dist/chunk-2E3MTAOA.js +133 -0
  7. package/dist/chunk-2E3MTAOA.js.map +1 -0
  8. package/dist/chunk-MWTEZOBS.cjs +139 -0
  9. package/dist/chunk-MWTEZOBS.cjs.map +1 -0
  10. package/dist/constants.d.ts +3 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/dispatch.d.ts +20 -0
  13. package/dist/dispatch.d.ts.map +1 -0
  14. package/dist/index.cjs +105 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +91 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/messages.d.ts +24 -0
  21. package/dist/messages.d.ts.map +1 -0
  22. package/dist/metadata.d.ts +17 -0
  23. package/dist/metadata.d.ts.map +1 -0
  24. package/dist/observability.d.ts +46 -0
  25. package/dist/observability.d.ts.map +1 -0
  26. package/dist/routes.d.ts +52 -0
  27. package/dist/routes.d.ts.map +1 -0
  28. package/dist/run.d.ts +32 -0
  29. package/dist/run.d.ts.map +1 -0
  30. package/dist/voice-thread.d.ts +23 -0
  31. package/dist/voice-thread.d.ts.map +1 -0
  32. package/dist/worker-entry.cjs +656 -0
  33. package/dist/worker-entry.cjs.map +1 -0
  34. package/dist/worker-entry.d.ts +8 -0
  35. package/dist/worker-entry.d.ts.map +1 -0
  36. package/dist/worker-entry.js +652 -0
  37. package/dist/worker-entry.js.map +1 -0
  38. package/dist/worker-setup.d.ts +5 -0
  39. package/dist/worker-setup.d.ts.map +1 -0
  40. package/dist/worker.d.ts +187 -0
  41. package/dist/worker.d.ts.map +1 -0
  42. package/dist/workflow-generator.d.ts +92 -0
  43. package/dist/workflow-generator.d.ts.map +1 -0
  44. package/package.json +24 -14
@@ -0,0 +1,133 @@
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
@@ -0,0 +1 @@
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"]}
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ var web = require('stream/web');
4
+
5
+ // src/constants.ts
6
+ var DEFAULT_LIVEKIT_AGENT_NAME = "mastra-voice";
7
+
8
+ // src/metadata.ts
9
+ function parseSessionMetadata(raw) {
10
+ if (!raw) return {};
11
+ try {
12
+ const parsed = JSON.parse(raw);
13
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
14
+ return parsed;
15
+ }
16
+ } catch {
17
+ }
18
+ return {};
19
+ }
20
+ function serializeSessionMetadata(metadata) {
21
+ return JSON.stringify(metadata);
22
+ }
23
+ function unwrapStepText(output) {
24
+ if (typeof output === "string") return output;
25
+ if (output && typeof output === "object" && output.type === "text-delta") {
26
+ const text = output.payload?.text;
27
+ return typeof text === "string" ? text : void 0;
28
+ }
29
+ return void 0;
30
+ }
31
+ function unwrapStepToolCall(output) {
32
+ if (!output || typeof output !== "object" || output.type !== "tool-call") return void 0;
33
+ const payload = output.payload;
34
+ if (payload && typeof payload.toolCallId === "string" && typeof payload.toolName === "string") {
35
+ return { toolCallId: payload.toolCallId, toolName: payload.toolName, args: payload.args };
36
+ }
37
+ return void 0;
38
+ }
39
+ function pipeAgentReplyToWriter(agentStream, writer) {
40
+ let text = "";
41
+ const forwarded = new web.ReadableStream({
42
+ start: async (controller) => {
43
+ for await (const chunk of agentStream.fullStream) {
44
+ const type = chunk?.type;
45
+ if (type === "text-delta") {
46
+ const delta = chunk.payload?.text;
47
+ if (typeof delta === "string" && delta) {
48
+ text += delta;
49
+ controller.enqueue(chunk);
50
+ }
51
+ } else if (type === "tool-call") {
52
+ controller.enqueue(chunk);
53
+ }
54
+ }
55
+ controller.close();
56
+ }
57
+ });
58
+ return forwarded.pipeTo(writer).then(() => text);
59
+ }
60
+ function createWorkflowReplyGenerator(options) {
61
+ const { workflow, workflowInput, replyStep, resultText, toolFeedback, onTurnComplete } = options;
62
+ return async (ctx) => {
63
+ const inputData = await workflowInput(ctx);
64
+ const run = await workflow.createRun();
65
+ const streamArgs = {
66
+ inputData
67
+ };
68
+ if (ctx.tracingContext) streamArgs.tracingContext = ctx.tracingContext;
69
+ if (ctx.requestContext) streamArgs.requestContext = ctx.requestContext;
70
+ const output = run.stream(streamArgs);
71
+ let cancelled = false;
72
+ let replyText = "";
73
+ const toolCalls = [];
74
+ const emitTurnComplete = (interrupted) => {
75
+ if (!onTurnComplete) return;
76
+ const completeCtx = { ...ctx, result: { text: replyText, toolCalls, interrupted } };
77
+ Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
78
+ console.warn("@mastra/livekit: onTurnComplete hook threw", error);
79
+ });
80
+ };
81
+ return new web.ReadableStream({
82
+ start: async (controller) => {
83
+ let streamedAny = false;
84
+ try {
85
+ for await (const chunk of output.fullStream) {
86
+ if (cancelled) break;
87
+ if (chunk.type !== "workflow-step-output") continue;
88
+ const payload = chunk.payload;
89
+ if (replyStep && payload.stepName !== replyStep) continue;
90
+ const text = unwrapStepText(payload.output);
91
+ if (text) {
92
+ streamedAny = true;
93
+ replyText += text;
94
+ controller.enqueue(text);
95
+ continue;
96
+ }
97
+ const toolCall = unwrapStepToolCall(payload.output);
98
+ if (toolCall) {
99
+ toolCalls.push(toolCall);
100
+ if (toolFeedback) {
101
+ const filler = toolFeedback(toolCall);
102
+ if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
103
+ }
104
+ }
105
+ }
106
+ if (!cancelled && !streamedAny && resultText) {
107
+ const finalText = resultText(await output.result);
108
+ if (finalText) {
109
+ replyText += finalText;
110
+ controller.enqueue(finalText);
111
+ }
112
+ }
113
+ if (!cancelled) controller.close();
114
+ emitTurnComplete(cancelled);
115
+ } catch (error) {
116
+ if (cancelled) {
117
+ emitTurnComplete(true);
118
+ return;
119
+ }
120
+ controller.error(error);
121
+ }
122
+ },
123
+ cancel: () => {
124
+ cancelled = true;
125
+ void Promise.resolve(run.cancel()).catch((error) => {
126
+ console.warn("@mastra/livekit: failed to cancel the workflow run on barge-in", error);
127
+ });
128
+ }
129
+ });
130
+ };
131
+ }
132
+
133
+ exports.DEFAULT_LIVEKIT_AGENT_NAME = DEFAULT_LIVEKIT_AGENT_NAME;
134
+ exports.createWorkflowReplyGenerator = createWorkflowReplyGenerator;
135
+ exports.parseSessionMetadata = parseSessionMetadata;
136
+ exports.pipeAgentReplyToWriter = pipeAgentReplyToWriter;
137
+ exports.serializeSessionMetadata = serializeSessionMetadata;
138
+ //# sourceMappingURL=chunk-MWTEZOBS.cjs.map
139
+ //# sourceMappingURL=chunk-MWTEZOBS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"names":["ReadableStream"],"mappings":"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","file":"chunk-MWTEZOBS.cjs","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"]}
@@ -0,0 +1,3 @@
1
+ /** Default LiveKit agent name used for explicit dispatch when none is configured. */
2
+ export declare const DEFAULT_LIVEKIT_AGENT_NAME = "mastra-voice";
3
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,eAAO,MAAM,0BAA0B,iBAAiB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { LiveKitSessionMetadata } from './metadata.js';
2
+ export interface DispatchVoiceSessionOptions {
3
+ /** Room to dispatch the agent into (created on demand). */
4
+ roomName: string;
5
+ /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */
6
+ agentName?: string;
7
+ metadata?: LiveKitSessionMetadata;
8
+ /** Defaults to `LIVEKIT_URL`. */
9
+ serverUrl?: string;
10
+ /** Defaults to `LIVEKIT_API_KEY`. */
11
+ apiKey?: string;
12
+ /** Defaults to `LIVEKIT_API_SECRET`. */
13
+ apiSecret?: string;
14
+ }
15
+ /**
16
+ * Programmatically dispatches a Mastra voice agent into a LiveKit room — for
17
+ * server-initiated sessions such as outbound calls or joining an existing room.
18
+ */
19
+ export declare function dispatchVoiceSession(options: DispatchVoiceSessionOptions): Promise<import("livekit-server-sdk").AgentDispatch>;
20
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,uDAgB9E"}
package/dist/index.cjs ADDED
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ var chunkMWTEZOBS_cjs = require('./chunk-MWTEZOBS.cjs');
4
+ var crypto = require('crypto');
5
+ var protocol = require('@livekit/protocol');
6
+ var livekitServerSdk = require('livekit-server-sdk');
7
+
8
+ function stringField(body, key) {
9
+ const value = body[key];
10
+ return typeof value === "string" && value.length > 0 ? value : void 0;
11
+ }
12
+ function liveKitConnectionRoute(options = {}) {
13
+ const handler = async (c) => {
14
+ const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;
15
+ const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;
16
+ const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;
17
+ if (!serverUrl || !apiKey || !apiSecret) {
18
+ return c.json(
19
+ {
20
+ error: "LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET (or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute)."
21
+ },
22
+ 500
23
+ );
24
+ }
25
+ const body = await c.req.json().then(
26
+ (parsed) => parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
27
+ ).catch(() => ({}));
28
+ const args = { body, context: c };
29
+ const metadata = options.metadata ? await options.metadata(args) : {
30
+ agentId: stringField(body, "agentId"),
31
+ threadId: stringField(body, "threadId"),
32
+ resourceId: stringField(body, "resourceId")
33
+ };
34
+ const roomName = typeof options.roomName === "function" ? options.roomName(args) : options.roomName ?? `mastra-voice-${crypto.randomUUID().slice(0, 8)}`;
35
+ const identity = typeof options.participantIdentity === "function" ? options.participantIdentity(args) : options.participantIdentity ?? metadata.resourceId ?? `user-${crypto.randomUUID().slice(0, 8)}`;
36
+ metadata.threadId ??= roomName;
37
+ const token = new livekitServerSdk.AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? "15m" });
38
+ token.addGrant({
39
+ room: roomName,
40
+ roomJoin: true,
41
+ canPublish: true,
42
+ canSubscribe: true,
43
+ canPublishData: true,
44
+ canUpdateOwnMetadata: true
45
+ });
46
+ token.roomConfig = new protocol.RoomConfiguration({
47
+ agents: [
48
+ new protocol.RoomAgentDispatch({
49
+ agentName: options.agentName ?? chunkMWTEZOBS_cjs.DEFAULT_LIVEKIT_AGENT_NAME,
50
+ metadata: chunkMWTEZOBS_cjs.serializeSessionMetadata(metadata)
51
+ })
52
+ ]
53
+ });
54
+ const details = {
55
+ serverUrl,
56
+ roomName,
57
+ participantName: identity,
58
+ participantToken: await token.toJwt()
59
+ };
60
+ return c.json(details);
61
+ };
62
+ return {
63
+ path: options.path ?? "/voice/livekit/connection-details",
64
+ method: "POST",
65
+ requiresAuth: options.requiresAuth,
66
+ handler
67
+ };
68
+ }
69
+ function toHttpUrl(url) {
70
+ return url.replace(/^ws/, "http");
71
+ }
72
+ async function dispatchVoiceSession(options) {
73
+ const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;
74
+ const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;
75
+ const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;
76
+ if (!serverUrl) {
77
+ throw new Error("@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.");
78
+ }
79
+ if (!apiKey || !apiSecret) {
80
+ throw new Error(
81
+ "@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession."
82
+ );
83
+ }
84
+ const client = new livekitServerSdk.AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);
85
+ return client.createDispatch(options.roomName, options.agentName ?? chunkMWTEZOBS_cjs.DEFAULT_LIVEKIT_AGENT_NAME, {
86
+ metadata: chunkMWTEZOBS_cjs.serializeSessionMetadata(options.metadata ?? {})
87
+ });
88
+ }
89
+
90
+ Object.defineProperty(exports, "DEFAULT_LIVEKIT_AGENT_NAME", {
91
+ enumerable: true,
92
+ get: function () { return chunkMWTEZOBS_cjs.DEFAULT_LIVEKIT_AGENT_NAME; }
93
+ });
94
+ Object.defineProperty(exports, "pipeAgentReplyToWriter", {
95
+ enumerable: true,
96
+ get: function () { return chunkMWTEZOBS_cjs.pipeAgentReplyToWriter; }
97
+ });
98
+ Object.defineProperty(exports, "serializeSessionMetadata", {
99
+ enumerable: true,
100
+ get: function () { return chunkMWTEZOBS_cjs.serializeSessionMetadata; }
101
+ });
102
+ exports.dispatchVoiceSession = dispatchVoiceSession;
103
+ exports.liveKitConnectionRoute = liveKitConnectionRoute;
104
+ //# sourceMappingURL=index.cjs.map
105
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/routes.ts","../src/dispatch.ts"],"names":["randomUUID","AccessToken","RoomConfiguration","RoomAgentDispatch","DEFAULT_LIVEKIT_AGENT_NAME","serializeSessionMetadata","AgentDispatchClient"],"mappings":";;;;;;;AAgDA,SAAS,WAAA,CAAY,MAA+B,GAAA,EAAiC;AACnF,EAAA,MAAM,KAAA,GAAQ,KAAK,GAAG,CAAA;AACtB,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,MAAA,GAAS,IAAI,KAAA,GAAQ,MAAA;AACjE;AAaO,SAAS,sBAAA,CAAuB,OAAA,GAAyC,EAAC,EAAa;AAC5F,EAAA,MAAM,OAAA,GAAU,OAAO,CAAA,KAAyB;AAC9C,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,GAAA,CAAI,WAAA;AACnD,IAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,GAAA,CAAI,eAAA;AAC7C,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,GAAA,CAAI,kBAAA;AACnD,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,MAAA,IAAU,CAAC,SAAA,EAAW;AACvC,MAAA,OAAO,CAAA,CAAE,IAAA;AAAA,QACP;AAAA,UACE,KAAA,EACE;AAAA,SAEJ;AAAA,QACA;AAAA,OACF;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAgC,MAAM,CAAA,CAAE,GAAA,CAC3C,MAAK,CACL,IAAA;AAAA,MAAK,CAAC,MAAA,KACL,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAK,MAAA,GAAqC;AAAC,KAC1G,CACC,KAAA,CAAM,OAAO,EAAC,CAAE,CAAA;AACnB,IAAA,MAAM,IAAA,GAA8B,EAAE,IAAA,EAAM,OAAA,EAAS,CAAA,EAAE;AAEvD,IAAA,MAAM,WAAmC,OAAA,CAAQ,QAAA,GAC7C,MAAM,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,GAC3B;AAAA,MACE,OAAA,EAAS,WAAA,CAAY,IAAA,EAAM,SAAS,CAAA;AAAA,MACpC,QAAA,EAAU,WAAA,CAAY,IAAA,EAAM,UAAU,CAAA;AAAA,MACtC,UAAA,EAAY,WAAA,CAAY,IAAA,EAAM,YAAY;AAAA,KAC5C;AAEJ,IAAA,MAAM,WACJ,OAAO,OAAA,CAAQ,QAAA,KAAa,UAAA,GACxB,QAAQ,QAAA,CAAS,IAAI,CAAA,GACpB,OAAA,CAAQ,YAAY,CAAA,aAAA,EAAgBA,iBAAA,GAAa,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACnE,IAAA,MAAM,WACJ,OAAO,OAAA,CAAQ,wBAAwB,UAAA,GACnC,OAAA,CAAQ,oBAAoB,IAAI,CAAA,GAC/B,QAAQ,mBAAA,IAAuB,QAAA,CAAS,cAAc,CAAA,KAAA,EAAQA,iBAAA,GAAa,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAE7F,IAAA,QAAA,CAAS,QAAA,KAAa,QAAA;AAEtB,IAAA,MAAM,KAAA,GAAQ,IAAIC,4BAAA,CAAY,MAAA,EAAQ,SAAA,EAAW,EAAE,QAAA,EAAU,GAAA,EAAK,OAAA,CAAQ,GAAA,IAAO,KAAA,EAAO,CAAA;AACxF,IAAA,KAAA,CAAM,QAAA,CAAS;AAAA,MACb,IAAA,EAAM,QAAA;AAAA,MACN,QAAA,EAAU,IAAA;AAAA,MACV,UAAA,EAAY,IAAA;AAAA,MACZ,YAAA,EAAc,IAAA;AAAA,MACd,cAAA,EAAgB,IAAA;AAAA,MAChB,oBAAA,EAAsB;AAAA,KACvB,CAAA;AACD,IAAA,KAAA,CAAM,UAAA,GAAa,IAAIC,0BAAA,CAAkB;AAAA,MACvC,MAAA,EAAQ;AAAA,QACN,IAAIC,0BAAA,CAAkB;AAAA,UACpB,SAAA,EAAW,QAAQ,SAAA,IAAaC,4CAAA;AAAA,UAChC,QAAA,EAAUC,2CAAyB,QAAQ;AAAA,SAC5C;AAAA;AACH,KACD,CAAA;AAED,IAAA,MAAM,OAAA,GAAoC;AAAA,MACxC,SAAA;AAAA,MACA,QAAA;AAAA,MACA,eAAA,EAAiB,QAAA;AAAA,MACjB,gBAAA,EAAkB,MAAM,KAAA,CAAM,KAAA;AAAM,KACtC;AACA,IAAA,OAAO,CAAA,CAAE,KAAK,OAAO,CAAA;AAAA,EACvB,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,QAAQ,IAAA,IAAQ,mCAAA;AAAA,IACtB,MAAA,EAAQ,MAAA;AAAA,IACR,cAAc,OAAA,CAAQ,YAAA;AAAA,IACtB;AAAA,GACF;AACF;ACzHA,SAAS,UAAU,GAAA,EAAqB;AACtC,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,MAAM,CAAA;AAClC;AAMA,eAAsB,qBAAqB,OAAA,EAAsC;AAC/E,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,GAAA,CAAI,WAAA;AACnD,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,GAAA,CAAI,eAAA;AAC7C,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,GAAA,CAAI,kBAAA;AACnD,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,MAAM,IAAI,MAAM,6EAA6E,CAAA;AAAA,EAC/F;AACA,EAAA,IAAI,CAAC,MAAA,IAAU,CAAC,SAAA,EAAW;AACzB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,MAAM,SAAS,IAAIC,oCAAA,CAAoB,UAAU,SAAS,CAAA,EAAG,QAAQ,SAAS,CAAA;AAC9E,EAAA,OAAO,OAAO,cAAA,CAAe,OAAA,CAAQ,QAAA,EAAU,OAAA,CAAQ,aAAaF,4CAAA,EAA4B;AAAA,IAC9F,QAAA,EAAUC,0CAAA,CAAyB,OAAA,CAAQ,QAAA,IAAY,EAAE;AAAA,GAC1D,CAAA;AACH","file":"index.cjs","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';\nimport type { ContextWithMastra, ApiRoute } from '@mastra/core/server';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\n/** Response body of the connection-details route. Matches LiveKit's frontend starter contract. */\nexport interface LiveKitConnectionDetails {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n}\n\nexport interface ConnectionRequestArgs {\n body: Record<string, unknown>;\n context: ContextWithMastra;\n}\n\nexport interface LiveKitConnectionRouteOptions {\n /**\n * Route path. Defaults to `/voice/livekit/connection-details`. Mastra reserves the\n * `/api` prefix for built-in routes, so custom paths must not start with `/api`.\n */\n path?: string;\n /** LiveKit server URL (`wss://...`). Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n /** LiveKit agent name for explicit dispatch. Must match the worker's `agentName`. */\n agentName?: string;\n /** Token time-to-live. Defaults to `'15m'`. */\n ttl?: string | number;\n /** Defaults to `true` (Mastra custom routes require auth unless opted out). */\n requiresAuth?: boolean;\n roomName?: string | ((args: ConnectionRequestArgs) => string);\n participantIdentity?: string | ((args: ConnectionRequestArgs) => string);\n /**\n * Session metadata delivered to the worker via agent dispatch. Defaults to passing\n * through `agentId`, `threadId`, and `resourceId` from the request body.\n */\n metadata?: (args: ConnectionRequestArgs) => LiveKitSessionMetadata | Promise<LiveKitSessionMetadata>;\n}\n\nfunction stringField(body: Record<string, unknown>, key: string): string | undefined {\n const value = body[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * An ApiRoute for `server.apiRoutes` that mints a LiveKit access token with the Mastra\n * voice agent dispatched into the room. Frontends call it to join a voice session:\n *\n * ```ts\n * export const mastra = new Mastra({\n * agents: { support },\n * server: { apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })] },\n * });\n * ```\n */\nexport function liveKitConnectionRoute(options: LiveKitConnectionRouteOptions = {}): ApiRoute {\n const handler = async (c: ContextWithMastra) => {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl || !apiKey || !apiSecret) {\n return c.json(\n {\n error:\n 'LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +\n '(or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute).',\n },\n 500,\n );\n }\n\n const body: Record<string, unknown> = await c.req\n .json()\n .then((parsed: unknown) =>\n parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {},\n )\n .catch(() => ({}));\n const args: ConnectionRequestArgs = { body, context: c };\n\n const metadata: LiveKitSessionMetadata = options.metadata\n ? await options.metadata(args)\n : {\n agentId: stringField(body, 'agentId'),\n threadId: stringField(body, 'threadId'),\n resourceId: stringField(body, 'resourceId'),\n };\n\n const roomName =\n typeof options.roomName === 'function'\n ? options.roomName(args)\n : (options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`);\n const identity =\n typeof options.participantIdentity === 'function'\n ? options.participantIdentity(args)\n : (options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`);\n // One memory thread per room unless the caller pins a thread explicitly.\n metadata.threadId ??= roomName;\n\n const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? '15m' });\n token.addGrant({\n room: roomName,\n roomJoin: true,\n canPublish: true,\n canSubscribe: true,\n canPublishData: true,\n canUpdateOwnMetadata: true,\n });\n token.roomConfig = new RoomConfiguration({\n agents: [\n new RoomAgentDispatch({\n agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,\n metadata: serializeSessionMetadata(metadata),\n }),\n ],\n });\n\n const details: LiveKitConnectionDetails = {\n serverUrl,\n roomName,\n participantName: identity,\n participantToken: await token.toJwt(),\n };\n return c.json(details);\n };\n\n return {\n path: options.path ?? '/voice/livekit/connection-details',\n method: 'POST',\n requiresAuth: options.requiresAuth,\n handler,\n } as ApiRoute;\n}\n","import { AgentDispatchClient } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\nexport interface DispatchVoiceSessionOptions {\n /** Room to dispatch the agent into (created on demand). */\n roomName: string;\n /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */\n agentName?: string;\n metadata?: LiveKitSessionMetadata;\n /** Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n}\n\nfunction toHttpUrl(url: string): string {\n return url.replace(/^ws/, 'http');\n}\n\n/**\n * Programmatically dispatches a Mastra voice agent into a LiveKit room — for\n * server-initiated sessions such as outbound calls or joining an existing room.\n */\nexport async function dispatchVoiceSession(options: DispatchVoiceSessionOptions) {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl) {\n throw new Error('@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.');\n }\n if (!apiKey || !apiSecret) {\n throw new Error(\n '@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession.',\n );\n }\n const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);\n return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {\n metadata: serializeSessionMetadata(options.metadata ?? {}),\n });\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export { liveKitConnectionRoute } from './routes.js';
2
+ export type { LiveKitConnectionRouteOptions, LiveKitConnectionDetails, ConnectionRequestArgs } from './routes.js';
3
+ export { dispatchVoiceSession } from './dispatch.js';
4
+ export type { DispatchVoiceSessionOptions } from './dispatch.js';
5
+ export { serializeSessionMetadata } from './metadata.js';
6
+ export type { LiveKitSessionMetadata } from './metadata.js';
7
+ export { pipeAgentReplyToWriter } from './workflow-generator.js';
8
+ export type { AgentReplyStreamLike } from './workflow-generator.js';
9
+ export { DEFAULT_LIVEKIT_AGENT_NAME } from './constants.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,6BAA6B,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAC/G,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACtD,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ import { DEFAULT_LIVEKIT_AGENT_NAME, serializeSessionMetadata } from './chunk-2E3MTAOA.js';
2
+ export { DEFAULT_LIVEKIT_AGENT_NAME, pipeAgentReplyToWriter, serializeSessionMetadata } from './chunk-2E3MTAOA.js';
3
+ import { randomUUID } from 'crypto';
4
+ import { RoomConfiguration, RoomAgentDispatch } from '@livekit/protocol';
5
+ import { AgentDispatchClient, AccessToken } from 'livekit-server-sdk';
6
+
7
+ function stringField(body, key) {
8
+ const value = body[key];
9
+ return typeof value === "string" && value.length > 0 ? value : void 0;
10
+ }
11
+ function liveKitConnectionRoute(options = {}) {
12
+ const handler = async (c) => {
13
+ const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;
14
+ const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;
15
+ const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;
16
+ if (!serverUrl || !apiKey || !apiSecret) {
17
+ return c.json(
18
+ {
19
+ error: "LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET (or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute)."
20
+ },
21
+ 500
22
+ );
23
+ }
24
+ const body = await c.req.json().then(
25
+ (parsed) => parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
26
+ ).catch(() => ({}));
27
+ const args = { body, context: c };
28
+ const metadata = options.metadata ? await options.metadata(args) : {
29
+ agentId: stringField(body, "agentId"),
30
+ threadId: stringField(body, "threadId"),
31
+ resourceId: stringField(body, "resourceId")
32
+ };
33
+ const roomName = typeof options.roomName === "function" ? options.roomName(args) : options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`;
34
+ const identity = typeof options.participantIdentity === "function" ? options.participantIdentity(args) : options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`;
35
+ metadata.threadId ??= roomName;
36
+ const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? "15m" });
37
+ token.addGrant({
38
+ room: roomName,
39
+ roomJoin: true,
40
+ canPublish: true,
41
+ canSubscribe: true,
42
+ canPublishData: true,
43
+ canUpdateOwnMetadata: true
44
+ });
45
+ token.roomConfig = new RoomConfiguration({
46
+ agents: [
47
+ new RoomAgentDispatch({
48
+ agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,
49
+ metadata: serializeSessionMetadata(metadata)
50
+ })
51
+ ]
52
+ });
53
+ const details = {
54
+ serverUrl,
55
+ roomName,
56
+ participantName: identity,
57
+ participantToken: await token.toJwt()
58
+ };
59
+ return c.json(details);
60
+ };
61
+ return {
62
+ path: options.path ?? "/voice/livekit/connection-details",
63
+ method: "POST",
64
+ requiresAuth: options.requiresAuth,
65
+ handler
66
+ };
67
+ }
68
+ function toHttpUrl(url) {
69
+ return url.replace(/^ws/, "http");
70
+ }
71
+ async function dispatchVoiceSession(options) {
72
+ const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;
73
+ const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;
74
+ const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;
75
+ if (!serverUrl) {
76
+ throw new Error("@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.");
77
+ }
78
+ if (!apiKey || !apiSecret) {
79
+ throw new Error(
80
+ "@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession."
81
+ );
82
+ }
83
+ const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);
84
+ return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {
85
+ metadata: serializeSessionMetadata(options.metadata ?? {})
86
+ });
87
+ }
88
+
89
+ export { dispatchVoiceSession, liveKitConnectionRoute };
90
+ //# sourceMappingURL=index.js.map
91
+ //# sourceMappingURL=index.js.map