@mastra/livekit 0.4.0-alpha.0 → 0.4.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_workflow_generator = require("./workflow-generator-B67QrY8L.cjs");
2
+ const require_workflow_generator = require("./workflow-generator-Bs-JQgFr.cjs");
3
3
  let crypto = require("crypto");
4
4
  let _livekit_protocol = require("@livekit/protocol");
5
5
  let livekit_server_sdk = require("livekit-server-sdk");
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as DEFAULT_LIVEKIT_AGENT_NAME, i as serializeSessionMetadata, n as pipeAgentReplyToWriter } from "./workflow-generator-BtfClQcM.js";
1
+ import { a as DEFAULT_LIVEKIT_AGENT_NAME, i as serializeSessionMetadata, n as pipeAgentReplyToWriter } from "./workflow-generator-CfNiAc3Z.js";
2
2
  import { randomUUID } from "crypto";
3
3
  import { RoomAgentDispatch, RoomConfiguration } from "@livekit/protocol";
4
4
  import { AccessToken, AgentDispatchClient } from "livekit-server-sdk";
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_workflow_generator = require("./workflow-generator-B67QrY8L.cjs");
2
+ const require_workflow_generator = require("./workflow-generator-Bs-JQgFr.cjs");
3
3
  const require_remote = require("./remote-DlCC6aCn.cjs");
4
4
  let crypto = require("crypto");
5
5
  let _livekit_agents = require("@livekit/agents");
@@ -1,4 +1,4 @@
1
- import { r as parseSessionMetadata, t as createWorkflowReplyGenerator } from "./workflow-generator-BtfClQcM.js";
1
+ import { r as parseSessionMetadata, t as createWorkflowReplyGenerator } from "./workflow-generator-CfNiAc3Z.js";
2
2
  import { a as chatContextToMessages, i as createMastraVoiceAgent, n as MastraVoiceAgent, t as createRemoteAgentReplyGenerator } from "./remote-bPj_0lAr.js";
3
3
  import { randomUUID } from "crypto";
4
4
  import { InferenceRunner, ServerOptions, cli, defineAgent, metrics, voice } from "@livekit/agents";
@@ -84,6 +84,10 @@ function pipeAgentReplyToWriter(agentStream, writer) {
84
84
  controller.close();
85
85
  } }).pipeTo(writer).then(() => text);
86
86
  }
87
+ function toError(error) {
88
+ if (error instanceof Error) return error;
89
+ return new Error(error === void 0 ? "workflow run failed" : String(error));
90
+ }
87
91
  /**
88
92
  * A {@link VoiceReplyGenerator} backed by a Mastra workflow. Per turn it starts a fresh run to
89
93
  * completion (LiveKit owns the turn boundary, so there is no suspend/resume and no conversation
@@ -129,6 +133,7 @@ function createWorkflowReplyGenerator(options) {
129
133
  try {
130
134
  for await (const chunk of output.fullStream) {
131
135
  if (cancelled) break;
136
+ if (chunk.type === "workflow-finish" && chunk.payload?.workflowStatus === "failed") break;
132
137
  if (chunk.type !== "workflow-step-output") continue;
133
138
  const payload = chunk.payload;
134
139
  if (replyStep && payload.stepName !== replyStep) continue;
@@ -148,11 +153,16 @@ function createWorkflowReplyGenerator(options) {
148
153
  }
149
154
  }
150
155
  }
151
- if (!cancelled && !streamedAny && resultText) {
152
- const finalText = resultText(await output.result);
153
- if (finalText) {
154
- replyText += finalText;
155
- controller.enqueue(finalText);
156
+ if (!cancelled) {
157
+ const result = await output.result;
158
+ const failed = result;
159
+ if (failed?.status === "failed") throw toError(failed.error);
160
+ if (!streamedAny && resultText) {
161
+ const finalText = resultText(result);
162
+ if (finalText) {
163
+ replyText += finalText;
164
+ controller.enqueue(finalText);
165
+ }
156
166
  }
157
167
  }
158
168
  if (!cancelled) controller.close();
@@ -206,4 +216,4 @@ Object.defineProperty(exports, "serializeSessionMetadata", {
206
216
  }
207
217
  });
208
218
 
209
- //# sourceMappingURL=workflow-generator-B67QrY8L.cjs.map
219
+ //# sourceMappingURL=workflow-generator-Bs-JQgFr.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-generator-Bs-JQgFr.cjs","names":["ReadableStream"],"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"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\nfunction toError(error: unknown): Error {\n if (error instanceof Error) return error;\n return new Error(error === undefined ? 'workflow run failed' : String(error));\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 // Step failures don't throw into fullStream; stop reading on a failed finish status and\n // let the output.result check below throw the run's actual error.\n if (\n chunk.type === 'workflow-finish' &&\n (chunk.payload as { workflowStatus?: unknown } | undefined)?.workflowStatus === 'failed'\n ) {\n break;\n }\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) {\n // A failed run resolves (not rejects) output.result, so check its status explicitly.\n const result = await output.result;\n const failed = result as { status?: unknown; error?: unknown } | undefined;\n if (failed?.status === 'failed') throw toError(failed.error);\n if (!streamedAny && resultText) {\n const finalText = resultText(result);\n if (finalText) {\n replyText += finalText;\n controller.enqueue(finalText);\n }\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"],"mappings":"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"}
@@ -84,6 +84,10 @@ function pipeAgentReplyToWriter(agentStream, writer) {
84
84
  controller.close();
85
85
  } }).pipeTo(writer).then(() => text);
86
86
  }
87
+ function toError(error) {
88
+ if (error instanceof Error) return error;
89
+ return new Error(error === void 0 ? "workflow run failed" : String(error));
90
+ }
87
91
  /**
88
92
  * A {@link VoiceReplyGenerator} backed by a Mastra workflow. Per turn it starts a fresh run to
89
93
  * completion (LiveKit owns the turn boundary, so there is no suspend/resume and no conversation
@@ -129,6 +133,7 @@ function createWorkflowReplyGenerator(options) {
129
133
  try {
130
134
  for await (const chunk of output.fullStream) {
131
135
  if (cancelled) break;
136
+ if (chunk.type === "workflow-finish" && chunk.payload?.workflowStatus === "failed") break;
132
137
  if (chunk.type !== "workflow-step-output") continue;
133
138
  const payload = chunk.payload;
134
139
  if (replyStep && payload.stepName !== replyStep) continue;
@@ -148,11 +153,16 @@ function createWorkflowReplyGenerator(options) {
148
153
  }
149
154
  }
150
155
  }
151
- if (!cancelled && !streamedAny && resultText) {
152
- const finalText = resultText(await output.result);
153
- if (finalText) {
154
- replyText += finalText;
155
- controller.enqueue(finalText);
156
+ if (!cancelled) {
157
+ const result = await output.result;
158
+ const failed = result;
159
+ if (failed?.status === "failed") throw toError(failed.error);
160
+ if (!streamedAny && resultText) {
161
+ const finalText = resultText(result);
162
+ if (finalText) {
163
+ replyText += finalText;
164
+ controller.enqueue(finalText);
165
+ }
156
166
  }
157
167
  }
158
168
  if (!cancelled) controller.close();
@@ -177,4 +187,4 @@ function createWorkflowReplyGenerator(options) {
177
187
  //#endregion
178
188
  export { DEFAULT_LIVEKIT_AGENT_NAME as a, serializeSessionMetadata as i, pipeAgentReplyToWriter as n, parseSessionMetadata as r, createWorkflowReplyGenerator as t };
179
189
 
180
- //# sourceMappingURL=workflow-generator-BtfClQcM.js.map
190
+ //# sourceMappingURL=workflow-generator-CfNiAc3Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-generator-CfNiAc3Z.js","names":[],"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"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\nfunction toError(error: unknown): Error {\n if (error instanceof Error) return error;\n return new Error(error === undefined ? 'workflow run failed' : String(error));\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 // Step failures don't throw into fullStream; stop reading on a failed finish status and\n // let the output.result check below throw the run's actual error.\n if (\n chunk.type === 'workflow-finish' &&\n (chunk.payload as { workflowStatus?: unknown } | undefined)?.workflowStatus === 'failed'\n ) {\n break;\n }\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) {\n // A failed run resolves (not rejects) output.result, so check its status explicitly.\n const result = await output.result;\n const failed = result as { status?: unknown; error?: unknown } | undefined;\n if (failed?.status === 'failed') throw toError(failed.error);\n if (!streamedAny && resultText) {\n const finalText = resultText(result);\n if (finalText) {\n replyText += finalText;\n controller.enqueue(finalText);\n }\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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-generator.d.ts","sourceRoot":"","sources":["../src/workflow-generator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EAEb,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,6BAA6B;IAC5C,oGAAoG;IACpG,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5D;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACtE;;;OAGG;IACH,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOlE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAO7E;AAED,6FAA6F;AAC7F,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,oBAAoB,EACjC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,mBAAmB,CAwFxG"}
1
+ {"version":3,"file":"workflow-generator.d.ts","sourceRoot":"","sources":["../src/workflow-generator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EAEb,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,6BAA6B;IAC5C,oGAAoG;IACpG,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5D;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACtE;;;OAGG;IACH,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAOlE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAO7E;AAED,6FAA6F;AAC7F,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,oBAAoB,EACjC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAOD;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,mBAAmB,CAsGxG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/livekit",
3
- "version": "0.4.0-alpha.0",
3
+ "version": "0.4.1-alpha.0",
4
4
  "description": "LiveKit voice integration for Mastra agents — realtime voice with semantic turn detection and barge-in",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -90,11 +90,11 @@
90
90
  "eslint": "^10.7.0",
91
91
  "tsdown": "0.22.9",
92
92
  "typescript": "^7.0.2",
93
- "vitest": "4.1.10",
94
- "zod": "^4.4.3",
95
- "@internal/lint": "0.0.132",
96
- "@mastra/core": "1.67.0-alpha.1",
97
- "@internal/types-builder": "0.0.107"
93
+ "vitest": "4.1.11",
94
+ "zod": "^4.6.4",
95
+ "@internal/lint": "0.0.137",
96
+ "@mastra/core": "1.72.0-alpha.1",
97
+ "@internal/types-builder": "0.0.112"
98
98
  },
99
99
  "scripts": {
100
100
  "build:lib": "tsdown --silent --config tsdown.config.ts",
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflow-generator-B67QrY8L.cjs","names":["ReadableStream"],"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"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"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflow-generator-BtfClQcM.js","names":[],"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"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"],"mappings":"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"}