@mastra/livekit 0.0.0 → 0.2.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/CHANGELOG.md +159 -0
- package/LICENSE.md +30 -0
- package/README.md +270 -7
- package/dist/bridge.d.ts +153 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/chunk-2E3MTAOA.js +133 -0
- package/dist/chunk-2E3MTAOA.js.map +1 -0
- package/dist/chunk-MWTEZOBS.cjs +139 -0
- package/dist/chunk-MWTEZOBS.cjs.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/dispatch.d.ts +20 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/index.cjs +105 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +24 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/metadata.d.ts +17 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/observability.d.ts +46 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/routes.d.ts +52 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/run.d.ts +32 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/voice-thread.d.ts +23 -0
- package/dist/voice-thread.d.ts.map +1 -0
- package/dist/worker-entry.cjs +656 -0
- package/dist/worker-entry.cjs.map +1 -0
- package/dist/worker-entry.d.ts +8 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +652 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-setup.d.ts +5 -0
- package/dist/worker-setup.d.ts.map +1 -0
- package/dist/worker.d.ts +187 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/workflow-generator.d.ts +92 -0
- package/dist/workflow-generator.d.ts.map +1 -0
- package/package.json +24 -14
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ReadableStream } from 'node:stream/web';
|
|
2
|
+
import { llm, voice } from '@livekit/agents';
|
|
3
|
+
import type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';
|
|
4
|
+
import type { TracingContext } from '@mastra/core/observability';
|
|
5
|
+
import { RequestContext } from '@mastra/core/request-context';
|
|
6
|
+
import type { VoiceTurnMessage } from './messages.js';
|
|
7
|
+
export type MastraStreamOptions = Partial<AgentExecutionOptionsBase<unknown>>;
|
|
8
|
+
export interface VoiceToolCall {
|
|
9
|
+
toolCallId: string;
|
|
10
|
+
toolName: string;
|
|
11
|
+
args?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface MastraVoiceAgentMemory {
|
|
14
|
+
thread: string;
|
|
15
|
+
resource?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per
|
|
19
|
+
* detected user turn; the bridge builds this context and asks the generator for the reply.
|
|
20
|
+
*/
|
|
21
|
+
export interface VoiceTurnContext {
|
|
22
|
+
/**
|
|
23
|
+
* The messages to generate a reply from. With Mastra Memory on, only the messages new since
|
|
24
|
+
* the agent last spoke (history comes from the thread); with memory off, the full session.
|
|
25
|
+
*
|
|
26
|
+
* For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(...,
|
|
27
|
+
* { memory })` inside a step so the agent backfills history from the thread (no duplication). A
|
|
28
|
+
* stateless workflow that wants the entire transcript every turn should read `chatCtx` instead
|
|
29
|
+
* (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from.
|
|
30
|
+
*/
|
|
31
|
+
messages: VoiceTurnMessage[];
|
|
32
|
+
/** The raw LiveKit chat context, for generators that want the full transcript or message parts. */
|
|
33
|
+
chatCtx: llm.ChatContext;
|
|
34
|
+
/** Resolved memory mapping for the call, or `false` when memory is disabled. */
|
|
35
|
+
memory: MastraVoiceAgentMemory | false;
|
|
36
|
+
/** Request context forwarded to generation. */
|
|
37
|
+
requestContext?: RequestContext;
|
|
38
|
+
/** Voice-call span context, so each turn's generation nests under the call trace. */
|
|
39
|
+
tracingContext?: TracingContext;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes.
|
|
43
|
+
*/
|
|
44
|
+
export interface VoiceTurnResult {
|
|
45
|
+
/** The assistant reply text streamed this turn, accumulated from the model's text deltas. */
|
|
46
|
+
text: string;
|
|
47
|
+
/** Tool calls the agent made during the turn, in order. */
|
|
48
|
+
toolCalls: VoiceToolCall[];
|
|
49
|
+
/** True when barge-in cut the turn short before it finished streaming. */
|
|
50
|
+
interrupted: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */
|
|
53
|
+
export interface VoiceTurnCompleteContext extends VoiceTurnContext {
|
|
54
|
+
/** The reply the agent produced this turn. */
|
|
55
|
+
result: VoiceTurnResult;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio
|
|
59
|
+
* path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory
|
|
60
|
+
* maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A
|
|
61
|
+
* thrown error or rejected promise is logged, not propagated. Because the resolved `memory`
|
|
62
|
+
* mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking
|
|
63
|
+
* `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}.
|
|
64
|
+
*/
|
|
65
|
+
export type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Produces a stream of text deltas for one conversational turn, or `null` to stay silent.
|
|
68
|
+
* Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying
|
|
69
|
+
* generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and
|
|
70
|
+
* `createWorkflowReplyGenerator` (a Mastra workflow).
|
|
71
|
+
*/
|
|
72
|
+
export type VoiceReplyGenerator = (ctx: VoiceTurnContext) => ReadableStream<string> | null | Promise<ReadableStream<string> | null>;
|
|
73
|
+
export interface AgentReplyGeneratorOptions {
|
|
74
|
+
/** The Mastra agent that generates replies. Tools and memory run inside this agent. */
|
|
75
|
+
agent: MastraAgent;
|
|
76
|
+
/** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */
|
|
77
|
+
streamOptions?: MastraStreamOptions;
|
|
78
|
+
/** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */
|
|
79
|
+
toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;
|
|
80
|
+
/** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */
|
|
81
|
+
onTurnComplete?: VoiceTurnCompleteHook;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,
|
|
85
|
+
* tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,
|
|
86
|
+
* which aborts the in-flight `agent.stream()`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator;
|
|
89
|
+
export interface MastraVoiceAgentOptions {
|
|
90
|
+
/**
|
|
91
|
+
* The Mastra agent that generates replies. Tools and memory run inside this agent. Provide
|
|
92
|
+
* either this or {@link MastraVoiceAgentOptions.generate}.
|
|
93
|
+
*/
|
|
94
|
+
agent?: MastraAgent;
|
|
95
|
+
/**
|
|
96
|
+
* A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of
|
|
97
|
+
* `agent` to drive replies with a workflow or any custom generator.
|
|
98
|
+
*/
|
|
99
|
+
generate?: VoiceReplyGenerator;
|
|
100
|
+
/**
|
|
101
|
+
* Conversation persistence. When set, only messages new since the agent last spoke are
|
|
102
|
+
* sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit
|
|
103
|
+
* in-session context is sent on every turn instead.
|
|
104
|
+
*/
|
|
105
|
+
memory?: MastraVoiceAgentMemory | false;
|
|
106
|
+
/** Request context entries forwarded to generation. */
|
|
107
|
+
requestContext?: RequestContext | Record<string, unknown>;
|
|
108
|
+
/**
|
|
109
|
+
* Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. "Let
|
|
110
|
+
* me look that up.") to speak it while the tool runs; it also appears in the transcript. Return
|
|
111
|
+
* nothing to stay silent. Applies to the agent generator built here; the workflow generator
|
|
112
|
+
* takes its own equivalent via `createWorkflowReplyGenerator`.
|
|
113
|
+
*/
|
|
114
|
+
toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;
|
|
115
|
+
/**
|
|
116
|
+
* Called once per turn after the reply has finished streaming to text-to-speech. Runs off the
|
|
117
|
+
* audio path and fire-and-forget — the turn does not await it — so post-turn memory
|
|
118
|
+
* maintenance, CRM writes, or analytics never delay the caller or the next turn. The context
|
|
119
|
+
* carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so
|
|
120
|
+
* this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error
|
|
121
|
+
* or rejected promise is logged, not propagated. Applies to the agent generator built here; the
|
|
122
|
+
* workflow generator takes its own via `createWorkflowReplyGenerator`.
|
|
123
|
+
*/
|
|
124
|
+
onTurnComplete?: VoiceTurnCompleteHook;
|
|
125
|
+
/** Extra options merged into every `agent.stream()` call (agent generator only). */
|
|
126
|
+
streamOptions?: MastraStreamOptions;
|
|
127
|
+
/** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */
|
|
128
|
+
instructions?: string;
|
|
129
|
+
id?: voice.AgentOptions<unknown>['id'];
|
|
130
|
+
stt?: voice.AgentOptions<unknown>['stt'];
|
|
131
|
+
vad?: voice.AgentOptions<unknown>['vad'];
|
|
132
|
+
tts?: voice.AgentOptions<unknown>['tts'];
|
|
133
|
+
turnHandling?: voice.AgentOptions<unknown>['turnHandling'];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.
|
|
137
|
+
*
|
|
138
|
+
* LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls
|
|
139
|
+
* `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}
|
|
140
|
+
* which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts
|
|
141
|
+
* the in-flight generation.
|
|
142
|
+
*/
|
|
143
|
+
export declare class MastraVoiceAgent extends voice.Agent {
|
|
144
|
+
readonly mastraAgent?: MastraAgent;
|
|
145
|
+
readonly memory: MastraVoiceAgentMemory | false;
|
|
146
|
+
readonly requestContext?: RequestContext;
|
|
147
|
+
readonly streamOptions?: MastraStreamOptions;
|
|
148
|
+
private readonly replyGenerator;
|
|
149
|
+
constructor(options: MastraVoiceAgentOptions);
|
|
150
|
+
llmNode(chatCtx: llm.ChatContext, _toolCtx: llm.ToolContext, _modelSettings: voice.ModelSettings): Promise<ReadableStream<llm.ChatChunk | string> | null>;
|
|
151
|
+
}
|
|
152
|
+
export declare function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent;
|
|
153
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 @@
|
|
|
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
|