@mastra/livekit 0.4.2 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.d.ts +23 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/client-entry.cjs +17 -0
- package/dist/client-entry.cjs.map +1 -0
- package/dist/client-entry.d.ts +8 -0
- package/dist/client-entry.d.ts.map +1 -0
- package/dist/client-entry.js +16 -0
- package/dist/client-entry.js.map +1 -0
- package/dist/dispatch.d.ts +7 -1
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/index.cjs +119 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -25
- package/dist/index.js.map +1 -1
- package/dist/llm-plugin.d.ts +4 -0
- package/dist/llm-plugin.d.ts.map +1 -1
- package/dist/observability.d.ts +3 -0
- package/dist/observability.d.ts.map +1 -1
- package/dist/plugin-entry.cjs +17 -4
- package/dist/plugin-entry.cjs.map +1 -1
- package/dist/plugin-entry.d.ts +4 -0
- package/dist/plugin-entry.d.ts.map +1 -1
- package/dist/plugin-entry.js +15 -5
- package/dist/plugin-entry.js.map +1 -1
- package/dist/recording-route.d.ts +31 -0
- package/dist/recording-route.d.ts.map +1 -0
- package/dist/recording-types.d.ts +11 -0
- package/dist/recording-types.d.ts.map +1 -0
- package/dist/recording.d.ts +21 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/{remote-bPj_0lAr.js → remote-CIMCGicy.js} +207 -15
- package/dist/remote-CIMCGicy.js.map +1 -0
- package/dist/{remote-DlCC6aCn.cjs → remote-GSOxa--f.cjs} +219 -9
- package/dist/remote-GSOxa--f.cjs.map +1 -0
- package/dist/remote.d.ts.map +1 -1
- package/dist/routes.d.ts +14 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/speech.d.ts +19 -0
- package/dist/speech.d.ts.map +1 -0
- package/dist/turn-metrics-CqqzdLDe.js +53 -0
- package/dist/turn-metrics-CqqzdLDe.js.map +1 -0
- package/dist/turn-metrics-CtVHHdJj.cjs +82 -0
- package/dist/turn-metrics-CtVHHdJj.cjs.map +1 -0
- package/dist/turn-metrics.d.ts +69 -0
- package/dist/turn-metrics.d.ts.map +1 -0
- package/dist/voice-stream.d.ts +3 -0
- package/dist/voice-stream.d.ts.map +1 -0
- package/dist/worker-entry.cjs +31 -4
- package/dist/worker-entry.cjs.map +1 -1
- package/dist/worker-entry.d.ts +4 -0
- package/dist/worker-entry.d.ts.map +1 -1
- package/dist/worker-entry.js +29 -5
- package/dist/worker-entry.js.map +1 -1
- package/dist/worker.d.ts +5 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/{workflow-generator-CfNiAc3Z.js → workflow-generator-5nHqlZV7.js} +24 -4
- package/dist/workflow-generator-5nHqlZV7.js.map +1 -0
- package/dist/{workflow-generator-Bs-JQgFr.cjs → workflow-generator-D67FvbFe.cjs} +24 -4
- package/dist/workflow-generator-D67FvbFe.cjs.map +1 -0
- package/dist/workflow-generator.d.ts +1 -1
- package/dist/workflow-generator.d.ts.map +1 -1
- package/package.json +23 -8
- package/dist/remote-DlCC6aCn.cjs.map +0 -1
- package/dist/remote-bPj_0lAr.js.map +0 -1
- package/dist/workflow-generator-Bs-JQgFr.cjs.map +0 -1
- package/dist/workflow-generator-CfNiAc3Z.js.map +0 -1
package/dist/plugin-entry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as VOICE_TEXT_FLUSH, r as VOICE_TURN_METADATA, t as VOICE_FLUSH_METADATA } from "./turn-metrics-CqqzdLDe.js";
|
|
2
|
+
import { a as createAgentReplyGenerator, c as chatContextToMessages, l as extractNewTurnMessages, n as mastraLLMNode, r as observeVoiceSession, s as instrumentVoiceReply, t as createRemoteAgentReplyGenerator } from "./remote-CIMCGicy.js";
|
|
2
3
|
import { DEFAULT_API_CONNECT_OPTIONS, llm } from "@livekit/agents";
|
|
3
4
|
import { RequestContext } from "@mastra/core/request-context";
|
|
4
5
|
//#region src/llm-plugin.ts
|
|
@@ -52,6 +53,7 @@ function livekitToolNames(toolCtx) {
|
|
|
52
53
|
*/
|
|
53
54
|
var MastraLLM = class extends llm.LLM {
|
|
54
55
|
#model;
|
|
56
|
+
#onTurnMetrics;
|
|
55
57
|
#memory;
|
|
56
58
|
#requestContext;
|
|
57
59
|
/** Non-remote sources are built once; remote is built per turn so it can pick up `connOptions.timeoutMs`. */
|
|
@@ -60,6 +62,7 @@ var MastraLLM = class extends llm.LLM {
|
|
|
60
62
|
#warnedToolCtx = false;
|
|
61
63
|
constructor(options) {
|
|
62
64
|
super();
|
|
65
|
+
this.#onTurnMetrics = options.onTurnMetrics;
|
|
63
66
|
const sources = [
|
|
64
67
|
options.remote ? "remote" : void 0,
|
|
65
68
|
options.agent ? "agent" : void 0,
|
|
@@ -125,6 +128,7 @@ var MastraLLM = class extends llm.LLM {
|
|
|
125
128
|
toolCtx,
|
|
126
129
|
connOptions,
|
|
127
130
|
generator: this.resolveGenerator(connOptions),
|
|
131
|
+
onTurnMetrics: this.#onTurnMetrics,
|
|
128
132
|
memory: this.#memory,
|
|
129
133
|
requestContext: this.#requestContext
|
|
130
134
|
});
|
|
@@ -140,6 +144,7 @@ var MastraLLM = class extends llm.LLM {
|
|
|
140
144
|
*/
|
|
141
145
|
var MastraLLMStream = class extends llm.LLMStream {
|
|
142
146
|
#generator;
|
|
147
|
+
#onTurnMetrics;
|
|
143
148
|
#memory;
|
|
144
149
|
#requestContext;
|
|
145
150
|
constructor(mastraLLM, options) {
|
|
@@ -149,6 +154,7 @@ var MastraLLMStream = class extends llm.LLMStream {
|
|
|
149
154
|
connOptions: options.connOptions
|
|
150
155
|
});
|
|
151
156
|
this.#generator = options.generator;
|
|
157
|
+
this.#onTurnMetrics = options.onTurnMetrics;
|
|
152
158
|
this.#memory = options.memory;
|
|
153
159
|
this.#requestContext = options.requestContext;
|
|
154
160
|
}
|
|
@@ -165,13 +171,13 @@ var MastraLLMStream = class extends llm.LLMStream {
|
|
|
165
171
|
usage = turnUsage;
|
|
166
172
|
}
|
|
167
173
|
};
|
|
168
|
-
const reply = await this.#generator
|
|
174
|
+
const { reply, identity } = await instrumentVoiceReply(this.#generator, turnCtx, this.#onTurnMetrics);
|
|
169
175
|
if (!reply) return;
|
|
170
176
|
if (this.abortController.signal.aborted) {
|
|
171
177
|
await reply.cancel().catch(() => {});
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
|
-
const id =
|
|
180
|
+
const id = identity.attemptId;
|
|
175
181
|
const reader = reply.getReader();
|
|
176
182
|
const onAbort = () => void reader.cancel().catch(() => {});
|
|
177
183
|
this.abortController.signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -184,7 +190,11 @@ var MastraLLMStream = class extends llm.LLMStream {
|
|
|
184
190
|
id,
|
|
185
191
|
delta: {
|
|
186
192
|
role: "assistant",
|
|
187
|
-
content: value
|
|
193
|
+
...typeof value === "string" ? { content: value } : {},
|
|
194
|
+
extra: {
|
|
195
|
+
[VOICE_TURN_METADATA]: identity,
|
|
196
|
+
[VOICE_FLUSH_METADATA]: typeof value !== "string"
|
|
197
|
+
}
|
|
188
198
|
}
|
|
189
199
|
});
|
|
190
200
|
}
|
|
@@ -202,6 +212,6 @@ var MastraLLMStream = class extends llm.LLMStream {
|
|
|
202
212
|
}
|
|
203
213
|
};
|
|
204
214
|
//#endregion
|
|
205
|
-
export { MastraLLM, createRemoteAgentReplyGenerator };
|
|
215
|
+
export { MastraLLM, VOICE_TEXT_FLUSH, createRemoteAgentReplyGenerator, mastraLLMNode, observeVoiceSession };
|
|
206
216
|
|
|
207
217
|
//# sourceMappingURL=plugin-entry.js.map
|
package/dist/plugin-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-entry.js","names":["#model","#memory","#requestContext","#staticGenerator","#remoteOptions","#warnedToolCtx","#generator"],"sources":["../src/llm-plugin.ts"],"sourcesContent":["import { DEFAULT_API_CONNECT_OPTIONS, llm } from '@livekit/agents';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport type { Agent as MastraAgent } from '@mastra/core/agent';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { createAgentReplyGenerator } from './bridge';\nimport type {\n MastraVoiceAgentMemory,\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteHook,\n VoiceTurnContext,\n VoiceTurnUsage,\n} from './bridge';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport { createRemoteAgentReplyGenerator } from './remote';\nimport type { RemoteMastraAgentOptions } from './remote';\n\nexport type { RemoteMastraAgentOptions } from './remote';\n\n/**\n * Options for {@link MastraLLM}. Provide **exactly one** reply source — `remote` (the headline: a\n * Mastra app on a remote server), `agent` (an in-process Mastra agent), or `generate` (a custom\n * {@link VoiceReplyGenerator}). The `toolFeedback` / `onToolCall` / `onTurnComplete` hooks apply to\n * the `remote` and `agent` sources; a `generate` source owns its own hooks.\n */\nexport interface MastraLLMOptions {\n /** Remote Mastra server. Provide exactly one of `remote`, `agent`, `generate`. */\n remote?: RemoteMastraAgentOptions;\n /** In-process Mastra agent (reuses `createAgentReplyGenerator`). */\n agent?: MastraAgent;\n /** Custom reply source (escape hatch; owns its own tool-feedback / turn-complete behavior). */\n generate?: VoiceReplyGenerator;\n\n /**\n * Conversation persistence, resolved by the customer per call (from SIP/caller identity). When set,\n * only messages new since the agent last spoke are sent each turn and Mastra Memory supplies\n * history. When omitted/false, the full LiveKit chat context is sent every turn.\n *\n * NOTE: incompatible with the session's `preemptiveGeneration` option — a speculative turn\n * persists a partial user message and a partial reply before being discarded. Leave preemptive\n * generation off when using `memory`, or set `memory.options.readOnly` and persist committed\n * turns yourself from the session's conversation-item events.\n *\n * The plugin cannot detect the combination at runtime, so this stays a documented constraint\n * rather than a warning: the LLM interface never receives the session (so the option can't be\n * read), a preemptive `chat()` is shape-identical to a real turn (LiveKit drives the same\n * `generateReply` with a draft transcript and no marker), and the observable signature — a\n * cancelled stream followed by a `chat()` whose trailing user message changed — is exactly what\n * an ordinary barge-in correction looks like, so a heuristic would warn on every barge-in.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation (tenant, dialed number, ...). */\n requestContext?: RequestContext | Record<string, unknown>;\n\n /** Speak a short filler while a (server-side) tool runs. Applies to the `remote`/`agent` sources. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. Applies to the `remote`/`agent` sources. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after each reply finishes. Applies to the `remote`/`agent` sources. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\nfunction mapUsageToLiveKit(usage: VoiceTurnUsage): llm.CompletionUsage {\n return {\n completionTokens: usage.completionTokens,\n promptTokens: usage.promptTokens,\n promptCachedTokens: usage.promptCachedTokens,\n totalTokens: usage.totalTokens,\n };\n}\n\n/**\n * Tool names carried by a `toolCtx`, across @livekit/agents versions: 1.5+ always passes a\n * `ToolContext` class instance (tools behind getters, `Object.keys` sees only private fields),\n * while 1.4 and the object shorthand pass a plain name→tool map.\n */\nfunction livekitToolNames(toolCtx: object): string[] {\n const instance = toolCtx as { flatten?: unknown };\n if (typeof instance.flatten === 'function') {\n return (instance.flatten as () => object[])().map(tool => {\n const { id, name } = tool as { id?: string; name?: string };\n return id ?? name ?? 'unknown';\n });\n }\n return Object.keys(toolCtx);\n}\n\n/**\n * A standard LiveKit LLM plugin (`llm.LLM`) backed by a Mastra agent. Drop it into the `llm` slot of a\n * customer-owned `voice.AgentSession` and the Mastra app (agent loop, tools, memory, observability)\n * runs wherever it's deployed — most importantly on a **remote** Mastra server reached over HTTP.\n *\n * Tools are defined and executed **server-side** on the Mastra agent; LiveKit-side `toolCtx` is\n * ignored (with a one-time warning). Tool activity surfaces via `toolFeedback` (spoken) and\n * `onToolCall` / `onTurnComplete` (programmatic). `voice.Agent` instructions do **not** reach the\n * Mastra agent — put instructions on the Mastra agent instead.\n *\n * @example\n * ```ts\n * const session = new voice.AgentSession({\n * stt: 'deepgram/nova-3',\n * tts: 'cartesia/sonic-3',\n * llm: new MastraLLM({\n * remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'callCenter' },\n * memory: { thread: callId, resource: callerId },\n * }),\n * });\n * ```\n */\nexport class MastraLLM extends llm.LLM {\n readonly #model: string;\n readonly #memory: MastraVoiceAgentMemory | false;\n readonly #requestContext?: RequestContext;\n /** Non-remote sources are built once; remote is built per turn so it can pick up `connOptions.timeoutMs`. */\n readonly #staticGenerator?: VoiceReplyGenerator;\n readonly #remoteOptions?: RemoteMastraAgentOptions & {\n toolFeedback?: MastraLLMOptions['toolFeedback'];\n onToolCall?: MastraLLMOptions['onToolCall'];\n onTurnComplete?: VoiceTurnCompleteHook;\n };\n #warnedToolCtx = false;\n\n constructor(options: MastraLLMOptions) {\n super();\n const sources = [\n options.remote ? 'remote' : undefined,\n options.agent ? 'agent' : undefined,\n options.generate ? 'generate' : undefined,\n ].filter(Boolean) as string[];\n if (sources.length !== 1) {\n throw new Error(\n `@mastra/livekit: MastraLLM requires exactly one reply source — \\`remote\\`, \\`agent\\`, or \\`generate\\` — ` +\n `but got ${sources.length === 0 ? 'none' : sources.join(' + ')}.`,\n );\n }\n\n this.#memory = options.memory ?? false;\n this.#requestContext = toRequestContext(options.requestContext);\n\n if (options.remote) {\n this.#model = options.remote.agentId;\n this.#remoteOptions = {\n ...options.remote,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n };\n } else if (options.agent) {\n this.#model = options.agent.id ?? options.agent.name;\n this.#staticGenerator = createAgentReplyGenerator({\n agent: options.agent,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n this.#model = 'mastra-generator';\n this.#staticGenerator = options.generate;\n }\n }\n\n label(): string {\n return 'mastra.MastraLLM';\n }\n\n override get model(): string {\n return this.#model;\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n /**\n * Resolves the reply generator for a turn. Non-remote sources are built once; the remote transport\n * is built per turn so its connect + first-token timeout can come from the session's\n * `connOptions.timeoutMs`, with base-class retries owning retry (transport `retries: 0`).\n */\n private resolveGenerator(connOptions: APIConnectOptions): VoiceReplyGenerator {\n if (this.#staticGenerator) return this.#staticGenerator;\n const remote = this.#remoteOptions!;\n return createRemoteAgentReplyGenerator({\n ...remote,\n retries: 0,\n timeoutMs: remote.timeoutMs ?? connOptions.timeoutMs,\n });\n }\n\n override chat({\n chatCtx,\n toolCtx,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): llm.LLMStream {\n // Tools run server-side; warn once if the customer wired LiveKit-side tools.\n if (!this.#warnedToolCtx && toolCtx) {\n const ignored = livekitToolNames(toolCtx);\n if (ignored.length > 0) {\n this.#warnedToolCtx = true;\n console.warn(\n `@mastra/livekit: MastraLLM ignores LiveKit-side tools (${ignored.join(', ')}). ` +\n `Tools are defined and executed server-side on the Mastra agent — move them there.`,\n );\n }\n }\n return new MastraLLMStream(this, {\n chatCtx,\n toolCtx,\n connOptions,\n generator: this.resolveGenerator(connOptions),\n memory: this.#memory,\n requestContext: this.#requestContext,\n });\n }\n\n /** No-op in v1: nothing in the LiveKit session/worker ever calls `prewarm()` automatically. */\n override prewarm(): void {}\n}\n\ninterface MastraLLMStreamOptions {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContext;\n connOptions: APIConnectOptions;\n generator: VoiceReplyGenerator;\n memory: MastraVoiceAgentMemory | false;\n requestContext?: RequestContext;\n}\n\n/**\n * The `llm.LLMStream` `MastraLLM` returns per turn. `run()` extracts the turn's messages, drives\n * the reply generator, and pushes assistant `ChatChunk`s into `this.queue` (NOT `this.output` — the\n * base class drains queue → output and computes TTFT / duration / usage). Barge-in aborts via\n * `this.abortController` and `run()` returns silently.\n */\nclass MastraLLMStream extends llm.LLMStream {\n readonly #generator: VoiceReplyGenerator;\n readonly #memory: MastraVoiceAgentMemory | false;\n readonly #requestContext?: RequestContext;\n\n constructor(mastraLLM: MastraLLM, options: MastraLLMStreamOptions) {\n super(mastraLLM, { chatCtx: options.chatCtx, toolCtx: options.toolCtx, connOptions: options.connOptions });\n this.#generator = options.generator;\n this.#memory = options.memory;\n this.#requestContext = options.requestContext;\n }\n\n protected async run(): Promise<void> {\n const messages =\n this.#memory === false ? chatContextToMessages(this.chatCtx) : extractNewTurnMessages(this.chatCtx);\n // No new input to answer → close without a request (equivalent to the wrapper returning null).\n if (messages.length === 0) return;\n\n let usage: VoiceTurnUsage | undefined;\n const turnCtx: VoiceTurnContext = {\n messages,\n chatCtx: this.chatCtx,\n memory: this.#memory,\n requestContext: this.#requestContext,\n onUsage: turnUsage => {\n usage = turnUsage;\n },\n };\n\n const reply = await this.#generator(turnCtx);\n if (!reply) return;\n if (this.abortController.signal.aborted) {\n await reply.cancel().catch(() => {});\n return;\n }\n\n // A single provider response id ties all of this turn's chunks together for the base class metrics.\n const id = globalThis.crypto.randomUUID();\n const reader = reply.getReader();\n const onAbort = () => void reader.cancel().catch(() => {});\n this.abortController.signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (this.abortController.signal.aborted) break;\n if (value) this.queue.put({ id, delta: { role: 'assistant', content: value } });\n }\n } catch (error) {\n // Barge-in tears down the reply stream; that surfaces as a read rejection but is not a failure.\n if (this.abortController.signal.aborted) return;\n throw error;\n } finally {\n this.abortController.signal.removeEventListener('abort', onAbort);\n }\n // Return silently on barge-in — throwing would feed the base class's error/retry machinery.\n if (this.abortController.signal.aborted) return;\n // Final usage-only chunk → the base class reads usage from the last chunk that carries it.\n if (usage) this.queue.put({ id, usage: mapUsageToLiveKit(usage) });\n }\n}\n\nexport { MastraLLMStream };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-entry.js","names":["#model","#onTurnMetrics","#memory","#requestContext","#staticGenerator","#remoteOptions","#warnedToolCtx","#generator"],"sources":["../src/llm-plugin.ts"],"sourcesContent":["import { DEFAULT_API_CONNECT_OPTIONS, llm } from '@livekit/agents';\nimport type { APIConnectOptions } from '@livekit/agents';\nimport type { Agent as MastraAgent } from '@mastra/core/agent';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { createAgentReplyGenerator, instrumentVoiceReply } from './bridge';\nimport type {\n MastraVoiceAgentMemory,\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteHook,\n VoiceTurnContext,\n VoiceTurnUsage,\n} from './bridge';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport { createRemoteAgentReplyGenerator } from './remote';\nimport type { RemoteMastraAgentOptions } from './remote';\nimport type { VoiceTurnMetricsHook } from './turn-metrics';\nimport { VOICE_FLUSH_METADATA, VOICE_TURN_METADATA } from './turn-metrics';\n\nexport type { RemoteMastraAgentOptions } from './remote';\n\n/**\n * Options for {@link MastraLLM}. Provide **exactly one** reply source — `remote` (the headline: a\n * Mastra app on a remote server), `agent` (an in-process Mastra agent), or `generate` (a custom\n * {@link VoiceReplyGenerator}). The `toolFeedback` / `onToolCall` / `onTurnComplete` hooks apply to\n * the `remote` and `agent` sources; a `generate` source owns its own hooks.\n */\nexport interface MastraLLMOptions {\n /** Per-attempt metrics; use observeVoiceSession for playback metrics. */\n onTurnMetrics?: VoiceTurnMetricsHook;\n /** Remote Mastra server. Provide exactly one of `remote`, `agent`, `generate`. */\n remote?: RemoteMastraAgentOptions;\n /** In-process Mastra agent (reuses `createAgentReplyGenerator`). */\n agent?: MastraAgent;\n /** Custom reply source (escape hatch; owns its own tool-feedback / turn-complete behavior). */\n generate?: VoiceReplyGenerator;\n\n /**\n * Conversation persistence, resolved by the customer per call (from SIP/caller identity). When set,\n * only messages new since the agent last spoke are sent each turn and Mastra Memory supplies\n * history. When omitted/false, the full LiveKit chat context is sent every turn.\n *\n * NOTE: incompatible with the session's `preemptiveGeneration` option — a speculative turn\n * persists a partial user message and a partial reply before being discarded. Leave preemptive\n * generation off when using `memory`, or set `memory.options.readOnly` and persist committed\n * turns yourself from the session's conversation-item events.\n *\n * The plugin cannot detect the combination at runtime, so this stays a documented constraint\n * rather than a warning: the LLM interface never receives the session (so the option can't be\n * read), a preemptive `chat()` is shape-identical to a real turn (LiveKit drives the same\n * `generateReply` with a draft transcript and no marker), and the observable signature — a\n * cancelled stream followed by a `chat()` whose trailing user message changed — is exactly what\n * an ordinary barge-in correction looks like, so a heuristic would warn on every barge-in.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation (tenant, dialed number, ...). */\n requestContext?: RequestContext | Record<string, unknown>;\n\n /** Speak a short filler while a (server-side) tool runs. Applies to the `remote`/`agent` sources. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. Applies to the `remote`/`agent` sources. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after each reply finishes. Applies to the `remote`/`agent` sources. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\nfunction mapUsageToLiveKit(usage: VoiceTurnUsage): llm.CompletionUsage {\n return {\n completionTokens: usage.completionTokens,\n promptTokens: usage.promptTokens,\n promptCachedTokens: usage.promptCachedTokens,\n totalTokens: usage.totalTokens,\n };\n}\n\n/**\n * Tool names carried by a `toolCtx`, across @livekit/agents versions: 1.5+ always passes a\n * `ToolContext` class instance (tools behind getters, `Object.keys` sees only private fields),\n * while 1.4 and the object shorthand pass a plain name→tool map.\n */\nfunction livekitToolNames(toolCtx: object): string[] {\n const instance = toolCtx as { flatten?: unknown };\n if (typeof instance.flatten === 'function') {\n return (instance.flatten as () => object[])().map(tool => {\n const { id, name } = tool as { id?: string; name?: string };\n return id ?? name ?? 'unknown';\n });\n }\n return Object.keys(toolCtx);\n}\n\n/**\n * A standard LiveKit LLM plugin (`llm.LLM`) backed by a Mastra agent. Drop it into the `llm` slot of a\n * customer-owned `voice.AgentSession` and the Mastra app (agent loop, tools, memory, observability)\n * runs wherever it's deployed — most importantly on a **remote** Mastra server reached over HTTP.\n *\n * Tools are defined and executed **server-side** on the Mastra agent; LiveKit-side `toolCtx` is\n * ignored (with a one-time warning). Tool activity surfaces via `toolFeedback` (spoken) and\n * `onToolCall` / `onTurnComplete` (programmatic). `voice.Agent` instructions do **not** reach the\n * Mastra agent — put instructions on the Mastra agent instead.\n *\n * @example\n * ```ts\n * const session = new voice.AgentSession({\n * stt: 'deepgram/nova-3',\n * tts: 'cartesia/sonic-3',\n * llm: new MastraLLM({\n * remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'callCenter' },\n * memory: { thread: callId, resource: callerId },\n * }),\n * });\n * ```\n */\nexport class MastraLLM extends llm.LLM {\n readonly #model: string;\n readonly #onTurnMetrics?: VoiceTurnMetricsHook;\n readonly #memory: MastraVoiceAgentMemory | false;\n readonly #requestContext?: RequestContext;\n /** Non-remote sources are built once; remote is built per turn so it can pick up `connOptions.timeoutMs`. */\n readonly #staticGenerator?: VoiceReplyGenerator;\n readonly #remoteOptions?: RemoteMastraAgentOptions & {\n toolFeedback?: MastraLLMOptions['toolFeedback'];\n onToolCall?: MastraLLMOptions['onToolCall'];\n onTurnComplete?: VoiceTurnCompleteHook;\n };\n #warnedToolCtx = false;\n\n constructor(options: MastraLLMOptions) {\n super();\n this.#onTurnMetrics = options.onTurnMetrics;\n const sources = [\n options.remote ? 'remote' : undefined,\n options.agent ? 'agent' : undefined,\n options.generate ? 'generate' : undefined,\n ].filter(Boolean) as string[];\n if (sources.length !== 1) {\n throw new Error(\n `@mastra/livekit: MastraLLM requires exactly one reply source — \\`remote\\`, \\`agent\\`, or \\`generate\\` — ` +\n `but got ${sources.length === 0 ? 'none' : sources.join(' + ')}.`,\n );\n }\n\n this.#memory = options.memory ?? false;\n this.#requestContext = toRequestContext(options.requestContext);\n\n if (options.remote) {\n this.#model = options.remote.agentId;\n this.#remoteOptions = {\n ...options.remote,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n };\n } else if (options.agent) {\n this.#model = options.agent.id ?? options.agent.name;\n this.#staticGenerator = createAgentReplyGenerator({\n agent: options.agent,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n this.#model = 'mastra-generator';\n this.#staticGenerator = options.generate;\n }\n }\n\n label(): string {\n return 'mastra.MastraLLM';\n }\n\n override get model(): string {\n return this.#model;\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n /**\n * Resolves the reply generator for a turn. Non-remote sources are built once; the remote transport\n * is built per turn so its connect + first-token timeout can come from the session's\n * `connOptions.timeoutMs`, with base-class retries owning retry (transport `retries: 0`).\n */\n private resolveGenerator(connOptions: APIConnectOptions): VoiceReplyGenerator {\n if (this.#staticGenerator) return this.#staticGenerator;\n const remote = this.#remoteOptions!;\n return createRemoteAgentReplyGenerator({\n ...remote,\n retries: 0,\n timeoutMs: remote.timeoutMs ?? connOptions.timeoutMs,\n });\n }\n\n override chat({\n chatCtx,\n toolCtx,\n connOptions = DEFAULT_API_CONNECT_OPTIONS,\n }: {\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContext;\n connOptions?: APIConnectOptions;\n parallelToolCalls?: boolean;\n toolChoice?: llm.ToolChoice;\n extraKwargs?: Record<string, unknown>;\n }): llm.LLMStream {\n // Tools run server-side; warn once if the customer wired LiveKit-side tools.\n if (!this.#warnedToolCtx && toolCtx) {\n const ignored = livekitToolNames(toolCtx);\n if (ignored.length > 0) {\n this.#warnedToolCtx = true;\n console.warn(\n `@mastra/livekit: MastraLLM ignores LiveKit-side tools (${ignored.join(', ')}). ` +\n `Tools are defined and executed server-side on the Mastra agent — move them there.`,\n );\n }\n }\n return new MastraLLMStream(this, {\n chatCtx,\n toolCtx,\n connOptions,\n generator: this.resolveGenerator(connOptions),\n onTurnMetrics: this.#onTurnMetrics,\n memory: this.#memory,\n requestContext: this.#requestContext,\n });\n }\n\n /** No-op in v1: nothing in the LiveKit session/worker ever calls `prewarm()` automatically. */\n override prewarm(): void {}\n}\n\ninterface MastraLLMStreamOptions {\n onTurnMetrics?: VoiceTurnMetricsHook;\n chatCtx: llm.ChatContext;\n toolCtx?: llm.ToolContext;\n connOptions: APIConnectOptions;\n generator: VoiceReplyGenerator;\n memory: MastraVoiceAgentMemory | false;\n requestContext?: RequestContext;\n}\n\n/**\n * The `llm.LLMStream` `MastraLLM` returns per turn. `run()` extracts the turn's messages, drives\n * the reply generator, and pushes assistant `ChatChunk`s into `this.queue` (NOT `this.output` — the\n * base class drains queue → output and computes TTFT / duration / usage). Barge-in aborts via\n * `this.abortController` and `run()` returns silently.\n */\nclass MastraLLMStream extends llm.LLMStream {\n readonly #generator: VoiceReplyGenerator;\n readonly #onTurnMetrics?: VoiceTurnMetricsHook;\n readonly #memory: MastraVoiceAgentMemory | false;\n readonly #requestContext?: RequestContext;\n\n constructor(mastraLLM: MastraLLM, options: MastraLLMStreamOptions) {\n super(mastraLLM, { chatCtx: options.chatCtx, toolCtx: options.toolCtx, connOptions: options.connOptions });\n this.#generator = options.generator;\n this.#onTurnMetrics = options.onTurnMetrics;\n this.#memory = options.memory;\n this.#requestContext = options.requestContext;\n }\n\n protected async run(): Promise<void> {\n const messages =\n this.#memory === false ? chatContextToMessages(this.chatCtx) : extractNewTurnMessages(this.chatCtx);\n // No new input to answer → close without a request (equivalent to the wrapper returning null).\n if (messages.length === 0) return;\n\n let usage: VoiceTurnUsage | undefined;\n const turnCtx: VoiceTurnContext = {\n messages,\n chatCtx: this.chatCtx,\n memory: this.#memory,\n requestContext: this.#requestContext,\n onUsage: turnUsage => {\n usage = turnUsage;\n },\n };\n\n const { reply, identity } = await instrumentVoiceReply(this.#generator, turnCtx, this.#onTurnMetrics);\n if (!reply) return;\n if (this.abortController.signal.aborted) {\n await reply.cancel().catch(() => {});\n return;\n }\n\n // A single provider response id ties all of this turn's chunks together for the base class metrics.\n const id = identity.attemptId;\n const reader = reply.getReader();\n const onAbort = () => void reader.cancel().catch(() => {});\n this.abortController.signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (this.abortController.signal.aborted) break;\n if (value)\n this.queue.put({\n id,\n delta: {\n role: 'assistant',\n ...(typeof value === 'string' ? { content: value } : {}),\n extra: { [VOICE_TURN_METADATA]: identity, [VOICE_FLUSH_METADATA]: typeof value !== 'string' },\n },\n });\n }\n } catch (error) {\n // Barge-in tears down the reply stream; that surfaces as a read rejection but is not a failure.\n if (this.abortController.signal.aborted) return;\n throw error;\n } finally {\n this.abortController.signal.removeEventListener('abort', onAbort);\n }\n // Return silently on barge-in — throwing would feed the base class's error/retry machinery.\n if (this.abortController.signal.aborted) return;\n // Final usage-only chunk → the base class reads usage from the last chunk that carries it.\n if (usage) this.queue.put({ id, usage: mapUsageToLiveKit(usage) });\n }\n}\n\nexport { MastraLLMStream };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ApiRoute, ContextWithMastra } from '@mastra/core/server';
|
|
2
|
+
import type { SpanRecord } from '@mastra/core/storage';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import type { LiveKitRecording } from './recording-types.js';
|
|
5
|
+
export declare const recordingSchema: z.ZodObject<{
|
|
6
|
+
url: z.ZodURL;
|
|
7
|
+
expiresAt: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export interface LiveKitRecordingResolverArgs {
|
|
10
|
+
traceId: string;
|
|
11
|
+
/** Taken from the stored voice call span, never from the browser request. */
|
|
12
|
+
roomName: string;
|
|
13
|
+
span: SpanRecord;
|
|
14
|
+
context: ContextWithMastra;
|
|
15
|
+
}
|
|
16
|
+
export interface LiveKitRecordingRouteOptions {
|
|
17
|
+
/** Studio discovers this route at its default path: /voice/livekit/recordings/:traceId. */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** Defaults to true. Only disable for a local demo. */
|
|
20
|
+
requiresAuth?: boolean;
|
|
21
|
+
/** Required trace-specific access policy. Only true permits trace lookup and URL resolution. */
|
|
22
|
+
authorize: (args: {
|
|
23
|
+
traceId: string;
|
|
24
|
+
context: ContextWithMastra;
|
|
25
|
+
}) => boolean | Promise<boolean>;
|
|
26
|
+
/** Return a playable URL, or undefined if the recording is not available yet. */
|
|
27
|
+
resolveRecording: (args: LiveKitRecordingResolverArgs) => Promise<LiveKitRecording | undefined>;
|
|
28
|
+
}
|
|
29
|
+
/** Resolves a voice call trace to a recording without persisting signed URLs in traces. */
|
|
30
|
+
export declare function liveKitRecordingRoute(options: LiveKitRecordingRouteOptions): ApiRoute;
|
|
31
|
+
//# sourceMappingURL=recording-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-route.d.ts","sourceRoot":"","sources":["../src/recording-route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAA4B,MAAM,mBAAmB,CAAC;AAGpF,eAAO,MAAM,eAAe;;;iBAGY,CAAC;AAEzC,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,4BAA4B;IAC3C,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gGAAgG;IAChG,SAAS,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,iBAAiB,CAAA;KAAE,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjG,iFAAiF;IACjF,gBAAgB,EAAE,CAAC,IAAI,EAAE,4BAA4B,KAAK,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;CACjG;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,QAAQ,CA4CrF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A playback URL, normally signed with a short expiry by your storage provider. */
|
|
2
|
+
export interface LiveKitRecording {
|
|
3
|
+
url: string;
|
|
4
|
+
expiresAt?: string;
|
|
5
|
+
}
|
|
6
|
+
export type LiveKitRecordingResponse = ({
|
|
7
|
+
status: 'ready';
|
|
8
|
+
} & LiveKitRecording) | {
|
|
9
|
+
status: 'unavailable';
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=recording-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording-types.d.ts","sourceRoot":"","sources":["../src/recording-types.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,wBAAwB,GAAG,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,gBAAgB,CAAC,GAAG;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RoomEgress } from 'livekit-server-sdk';
|
|
2
|
+
/**
|
|
3
|
+
* LiveKit auto-egress settings, accepted as a plain object or a RoomEgress instance.
|
|
4
|
+
* Use `room.audioOnly` and `room.fileOutputs` for a single mixed audio recording.
|
|
5
|
+
* Storage credentials belong here on the server, never in session metadata or tokens.
|
|
6
|
+
*/
|
|
7
|
+
export type LiveKitRecordingOptions = NonNullable<ConstructorParameters<typeof RoomEgress>[0]>;
|
|
8
|
+
/** A recording-enabled session cannot reuse an existing LiveKit room. */
|
|
9
|
+
export declare class LiveKitRecordingRoomConflictError extends Error {
|
|
10
|
+
readonly roomName: string;
|
|
11
|
+
constructor(roomName: string);
|
|
12
|
+
}
|
|
13
|
+
/** Creates a fresh room with recording configured before any agent or caller joins. */
|
|
14
|
+
export declare function createRecordingRoom(options: {
|
|
15
|
+
roomName: string;
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
apiSecret: string;
|
|
19
|
+
recording: LiveKitRecordingOptions;
|
|
20
|
+
}): Promise<import("livekit-server-sdk").Room>;
|
|
21
|
+
//# sourceMappingURL=recording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.d.ts","sourceRoot":"","sources":["../src/recording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,oBAAoB,CAAC;AAEnE;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC,qBAAqB,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/F,yEAAyE;AACzE,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,YAAY,QAAQ,EAAE,MAAM,EAM3B;CACF;AAED,uFAAuF;AACvF,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,uBAAuB,CAAC;CACpC,8CAeA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as notifyVoiceObserver, i as createGenerationMetrics, n as VOICE_TEXT_FLUSH, r as VOICE_TURN_METADATA } from "./turn-metrics-CqqzdLDe.js";
|
|
2
|
+
import { ReadableStream as ReadableStream$1 } from "stream/web";
|
|
3
|
+
import { APIConnectionError, APIError, APIStatusError, APITimeoutError, FlushSentinel, llm, voice } from "@livekit/agents";
|
|
3
4
|
import { RequestContext } from "@mastra/core/request-context";
|
|
4
5
|
function textOfMessage(message) {
|
|
5
6
|
const parts = [];
|
|
@@ -84,6 +85,29 @@ function chatContextToMessages(chatCtx) {
|
|
|
84
85
|
return messages;
|
|
85
86
|
}
|
|
86
87
|
//#endregion
|
|
88
|
+
//#region src/voice-stream.ts
|
|
89
|
+
/** A one-to-one mapping that awaits upstream cancellation, including pending reads. */
|
|
90
|
+
function mapVoiceStream(source, map) {
|
|
91
|
+
const reader = source.getReader();
|
|
92
|
+
let cancelled = false;
|
|
93
|
+
return new ReadableStream({
|
|
94
|
+
async pull(controller) {
|
|
95
|
+
try {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (cancelled) return;
|
|
98
|
+
if (done) controller.close();
|
|
99
|
+
else controller.enqueue(map(value));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!cancelled) controller.error(error);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
cancel(reason) {
|
|
105
|
+
cancelled = true;
|
|
106
|
+
return reader.cancel(reason);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
87
111
|
//#region src/bridge.ts
|
|
88
112
|
const DEFAULT_INSTRUCTIONS = "You are a helpful voice assistant powered by a Mastra agent.";
|
|
89
113
|
/**
|
|
@@ -120,7 +144,7 @@ var DisclosureReminder = class {
|
|
|
120
144
|
function prependText(source, text) {
|
|
121
145
|
const prefix = text.endsWith(" ") ? text : `${text} `;
|
|
122
146
|
const reader = source.getReader();
|
|
123
|
-
return new ReadableStream({
|
|
147
|
+
return new ReadableStream$1({
|
|
124
148
|
start(controller) {
|
|
125
149
|
controller.enqueue(prefix);
|
|
126
150
|
},
|
|
@@ -199,7 +223,7 @@ function createAgentReplyGenerator(options) {
|
|
|
199
223
|
console.warn("@mastra/livekit: onTurnComplete hook threw", error);
|
|
200
224
|
});
|
|
201
225
|
};
|
|
202
|
-
return new ReadableStream({
|
|
226
|
+
return new ReadableStream$1({
|
|
203
227
|
start: async (controller) => {
|
|
204
228
|
try {
|
|
205
229
|
const result = await agent.stream(ctx.messages, mergedOptions);
|
|
@@ -210,13 +234,20 @@ function createAgentReplyGenerator(options) {
|
|
|
210
234
|
replyText += chunk.payload.text;
|
|
211
235
|
controller.enqueue(chunk.payload.text);
|
|
212
236
|
}
|
|
213
|
-
} else if (chunk.type === "
|
|
237
|
+
} else if (chunk.type === "text-end" || chunk.type === "step-finish") controller.enqueue(VOICE_TEXT_FLUSH);
|
|
238
|
+
else if (chunk.type === "tool-result") ctx.metrics?.toolEnd(chunk.payload.toolCallId);
|
|
239
|
+
else if (chunk.type === "tool-call") {
|
|
214
240
|
const toolCall = {
|
|
215
241
|
toolCallId: chunk.payload.toolCallId,
|
|
216
242
|
toolName: chunk.payload.toolName,
|
|
217
243
|
args: chunk.payload.args
|
|
218
244
|
};
|
|
219
245
|
toolCalls.push(toolCall);
|
|
246
|
+
ctx.metrics?.toolStart({
|
|
247
|
+
toolCallId: toolCall.toolCallId,
|
|
248
|
+
toolName: toolCall.toolName
|
|
249
|
+
});
|
|
250
|
+
controller.enqueue(VOICE_TEXT_FLUSH);
|
|
220
251
|
try {
|
|
221
252
|
onToolCall?.(toolCall);
|
|
222
253
|
} catch (error) {
|
|
@@ -229,7 +260,10 @@ function createAgentReplyGenerator(options) {
|
|
|
229
260
|
} catch (error) {
|
|
230
261
|
console.warn("@mastra/livekit: toolFeedback hook threw", error);
|
|
231
262
|
}
|
|
232
|
-
if (filler)
|
|
263
|
+
if (filler) {
|
|
264
|
+
controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
|
|
265
|
+
controller.enqueue(VOICE_TEXT_FLUSH);
|
|
266
|
+
}
|
|
233
267
|
}
|
|
234
268
|
} else if (chunk.type === "finish") {
|
|
235
269
|
const output = chunk.payload.output;
|
|
@@ -302,6 +336,7 @@ var MastraVoiceAgent = class extends voice.Agent {
|
|
|
302
336
|
requestContext;
|
|
303
337
|
streamOptions;
|
|
304
338
|
replyGenerator;
|
|
339
|
+
onTurnMetrics;
|
|
305
340
|
reminder;
|
|
306
341
|
constructor(options) {
|
|
307
342
|
if (options.agent && options.generate) throw new Error("@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.");
|
|
@@ -314,6 +349,7 @@ var MastraVoiceAgent = class extends voice.Agent {
|
|
|
314
349
|
tts: options.tts,
|
|
315
350
|
turnHandling: options.turnHandling
|
|
316
351
|
});
|
|
352
|
+
this.onTurnMetrics = options.onTurnMetrics;
|
|
317
353
|
this.memory = options.memory ?? false;
|
|
318
354
|
this.requestContext = toRequestContext(options.requestContext);
|
|
319
355
|
this.streamOptions = options.streamOptions;
|
|
@@ -333,23 +369,164 @@ var MastraVoiceAgent = class extends voice.Agent {
|
|
|
333
369
|
async llmNode(chatCtx, _toolCtx, _modelSettings) {
|
|
334
370
|
const messages = this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);
|
|
335
371
|
if (messages.length === 0) return null;
|
|
336
|
-
const
|
|
372
|
+
const ctx = {
|
|
337
373
|
messages,
|
|
338
374
|
chatCtx,
|
|
339
375
|
memory: this.memory,
|
|
340
376
|
requestContext: this.requestContext,
|
|
341
377
|
tracingContext: this.streamOptions?.tracingContext
|
|
342
|
-
}
|
|
378
|
+
};
|
|
379
|
+
const { reply, identity } = await instrumentVoiceReply(this.replyGenerator, ctx, this.onTurnMetrics);
|
|
343
380
|
if (!reply) return null;
|
|
344
381
|
const reminder = this.reminder?.due();
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
382
|
+
if (reminder) this.reminder?.markDelivered();
|
|
383
|
+
return mapVoiceStream(reminder ? prependText(reply, reminder) : reply, (chunk) => {
|
|
384
|
+
if (typeof chunk === "string") return {
|
|
385
|
+
id: identity.attemptId,
|
|
386
|
+
delta: {
|
|
387
|
+
role: "assistant",
|
|
388
|
+
content: chunk,
|
|
389
|
+
extra: { [VOICE_TURN_METADATA]: identity }
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
else return FlushSentinel;
|
|
393
|
+
});
|
|
348
394
|
}
|
|
349
395
|
};
|
|
350
396
|
function createMastraVoiceAgent(options) {
|
|
351
397
|
return new MastraVoiceAgent(options);
|
|
352
398
|
}
|
|
399
|
+
/** @internal Wrap every reply source, including custom generators, with one terminal metric. */
|
|
400
|
+
async function instrumentVoiceReply(generator, ctx, onTurnMetrics) {
|
|
401
|
+
const identity = {
|
|
402
|
+
turnId: ctx.turnId ?? ctx.messages.findLast((message) => message.role === "user")?.id ?? crypto.randomUUID(),
|
|
403
|
+
attemptId: crypto.randomUUID()
|
|
404
|
+
};
|
|
405
|
+
const metrics = createGenerationMetrics(identity, onTurnMetrics);
|
|
406
|
+
let reply;
|
|
407
|
+
try {
|
|
408
|
+
reply = await generator({
|
|
409
|
+
...ctx,
|
|
410
|
+
...identity,
|
|
411
|
+
metrics
|
|
412
|
+
});
|
|
413
|
+
} catch (error) {
|
|
414
|
+
metrics.end("failed");
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
if (!reply) {
|
|
418
|
+
metrics.end("completed");
|
|
419
|
+
return {
|
|
420
|
+
reply: null,
|
|
421
|
+
identity
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const reader = reply.getReader();
|
|
425
|
+
let cancelled = false;
|
|
426
|
+
return {
|
|
427
|
+
identity,
|
|
428
|
+
reply: new ReadableStream$1({
|
|
429
|
+
async pull(controller) {
|
|
430
|
+
try {
|
|
431
|
+
const { done, value } = await reader.read();
|
|
432
|
+
if (cancelled) return;
|
|
433
|
+
if (done) {
|
|
434
|
+
metrics.end("completed");
|
|
435
|
+
controller.close();
|
|
436
|
+
} else {
|
|
437
|
+
if (typeof value === "string" && value) metrics.text();
|
|
438
|
+
controller.enqueue(value);
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (cancelled) return;
|
|
442
|
+
metrics.end("failed");
|
|
443
|
+
controller.error(error);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
cancel(reason) {
|
|
447
|
+
cancelled = true;
|
|
448
|
+
metrics.end("interrupted");
|
|
449
|
+
return reader.cancel(reason);
|
|
450
|
+
}
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/speech.ts
|
|
456
|
+
/**
|
|
457
|
+
* Use from a custom Agent.llmNode when using MastraLLM. LiveKit's LLMStream only accepts
|
|
458
|
+
* ChatChunks; this node translates our boundary metadata into LiveKit's supported marker.
|
|
459
|
+
*/
|
|
460
|
+
async function mastraLLMNode(agent, chatCtx, toolCtx, modelSettings) {
|
|
461
|
+
const source = await voice.Agent.default.llmNode(agent, chatCtx, toolCtx, modelSettings);
|
|
462
|
+
if (!source) return null;
|
|
463
|
+
return mapVoiceStream(source, (chunk) => {
|
|
464
|
+
if (typeof chunk === "object" && chunk.delta?.extra?.["mastra.voice.flush"] === true) return FlushSentinel;
|
|
465
|
+
else return chunk;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function speechResult(handle, outcome) {
|
|
469
|
+
const messages = handle.chatItems.filter((item) => item.type === "message" && item.role === "assistant");
|
|
470
|
+
const identities = messages.map((message) => message.extra[VOICE_TURN_METADATA]);
|
|
471
|
+
const identity = identities[0];
|
|
472
|
+
const sameIdentity = identities.every((value) => value?.turnId === identity?.turnId && value?.attemptId === identity?.attemptId);
|
|
473
|
+
const starts = messages.map((message) => message.metrics.startedSpeakingAt).filter((n) => typeof n === "number" && Number.isFinite(n));
|
|
474
|
+
const stops = messages.map((message) => message.metrics.stoppedSpeakingAt).filter((n) => typeof n === "number" && Number.isFinite(n));
|
|
475
|
+
const firstAudio = messages.map((message) => message.metrics.e2eLatency).find((n) => typeof n === "number" && Number.isFinite(n));
|
|
476
|
+
const speechStartedAt = starts.length ? Math.min(...starts) * 1e3 : void 0;
|
|
477
|
+
const speechEndedAt = stops.length ? Math.max(...stops) * 1e3 : void 0;
|
|
478
|
+
const firstAudioMs = firstAudio === void 0 ? void 0 : firstAudio * 1e3;
|
|
479
|
+
const playedText = messages.map((message) => message.textContent).join("");
|
|
480
|
+
return {
|
|
481
|
+
version: 1,
|
|
482
|
+
phase: "speech",
|
|
483
|
+
turnId: sameIdentity && typeof identity?.turnId === "string" ? identity.turnId : handle.id,
|
|
484
|
+
attemptId: sameIdentity && typeof identity?.attemptId === "string" ? identity.attemptId : void 0,
|
|
485
|
+
speechId: handle.id,
|
|
486
|
+
outcome,
|
|
487
|
+
measurement: "server-playout",
|
|
488
|
+
speechStartedAt,
|
|
489
|
+
speechEndedAt,
|
|
490
|
+
firstAudioMs,
|
|
491
|
+
completionMs: outcome === "completed" && firstAudioMs !== void 0 && speechStartedAt !== void 0 && speechEndedAt !== void 0 ? firstAudioMs + speechEndedAt - speechStartedAt : void 0,
|
|
492
|
+
playedText: messages.length ? playedText : void 0,
|
|
493
|
+
transcriptSource: messages.length ? "livekit" : "unavailable"
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Attach before session.start(). Observes generation-independent playback completion;
|
|
498
|
+
* never writes memory. Return value detaches listeners and finalizes pending speech as cancelled.
|
|
499
|
+
* LiveKit's transcript can be estimated. Server output cannot confirm remote human hearing.
|
|
500
|
+
*/
|
|
501
|
+
function observeVoiceSession(session, options) {
|
|
502
|
+
const pending = /* @__PURE__ */ new Set();
|
|
503
|
+
let detached = false;
|
|
504
|
+
const finish = (handle, outcome) => {
|
|
505
|
+
if (!pending.delete(handle)) return;
|
|
506
|
+
const result = speechResult(handle, outcome);
|
|
507
|
+
const { playedText: _text, transcriptSource: _source, ...metrics } = result;
|
|
508
|
+
notifyVoiceObserver(options.onTurnMetrics, metrics);
|
|
509
|
+
notifyVoiceObserver(options.onSpeechComplete, result);
|
|
510
|
+
};
|
|
511
|
+
const onSpeech = ({ speechHandle }) => {
|
|
512
|
+
if (detached || pending.has(speechHandle)) return;
|
|
513
|
+
pending.add(speechHandle);
|
|
514
|
+
speechHandle.waitForPlayout().then(() => {
|
|
515
|
+
finish(speechHandle, speechHandle.exception() ? "failed" : speechHandle.interrupted ? "interrupted" : "completed");
|
|
516
|
+
}, () => finish(speechHandle, "failed"));
|
|
517
|
+
};
|
|
518
|
+
const detach = (outcome) => {
|
|
519
|
+
if (detached) return;
|
|
520
|
+
detached = true;
|
|
521
|
+
session.off(voice.AgentSessionEventTypes.SpeechCreated, onSpeech);
|
|
522
|
+
session.off(voice.AgentSessionEventTypes.Close, onClose);
|
|
523
|
+
for (const handle of pending) finish(handle, outcome);
|
|
524
|
+
};
|
|
525
|
+
const onClose = (event) => detach(event.error ? "failed" : "cancelled");
|
|
526
|
+
session.on(voice.AgentSessionEventTypes.SpeechCreated, onSpeech);
|
|
527
|
+
session.on(voice.AgentSessionEventTypes.Close, onClose);
|
|
528
|
+
return () => detach("cancelled");
|
|
529
|
+
}
|
|
353
530
|
//#endregion
|
|
354
531
|
//#region src/remote.ts
|
|
355
532
|
const DEFAULT_API_PREFIX = "/api";
|
|
@@ -478,7 +655,7 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
478
655
|
requestContext: serializeRequestContext(ctx.requestContext),
|
|
479
656
|
...extraBody
|
|
480
657
|
};
|
|
481
|
-
return new ReadableStream({
|
|
658
|
+
return new ReadableStream$1({
|
|
482
659
|
start: async (controller) => {
|
|
483
660
|
let retryable = true;
|
|
484
661
|
try {
|
|
@@ -543,6 +720,13 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
543
720
|
}
|
|
544
721
|
break;
|
|
545
722
|
}
|
|
723
|
+
case "text-end":
|
|
724
|
+
case "step-finish":
|
|
725
|
+
controller.enqueue(VOICE_TEXT_FLUSH);
|
|
726
|
+
break;
|
|
727
|
+
case "tool-result":
|
|
728
|
+
if (typeof payload.toolCallId === "string") ctx.metrics?.toolEnd(payload.toolCallId);
|
|
729
|
+
break;
|
|
546
730
|
case "tool-call": {
|
|
547
731
|
clearWatchdog();
|
|
548
732
|
const toolCall = {
|
|
@@ -551,6 +735,11 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
551
735
|
args: payload.args
|
|
552
736
|
};
|
|
553
737
|
toolCalls.push(toolCall);
|
|
738
|
+
ctx.metrics?.toolStart({
|
|
739
|
+
toolCallId: toolCall.toolCallId,
|
|
740
|
+
toolName: toolCall.toolName
|
|
741
|
+
});
|
|
742
|
+
controller.enqueue(VOICE_TEXT_FLUSH);
|
|
554
743
|
try {
|
|
555
744
|
onToolCall?.(toolCall);
|
|
556
745
|
} catch (error) {
|
|
@@ -563,7 +752,10 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
563
752
|
} catch (error) {
|
|
564
753
|
console.warn("@mastra/livekit: toolFeedback hook threw", error);
|
|
565
754
|
}
|
|
566
|
-
if (filler)
|
|
755
|
+
if (filler) {
|
|
756
|
+
controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
|
|
757
|
+
controller.enqueue(VOICE_TEXT_FLUSH);
|
|
758
|
+
}
|
|
567
759
|
}
|
|
568
760
|
break;
|
|
569
761
|
}
|
|
@@ -625,6 +817,6 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
625
817
|
};
|
|
626
818
|
}
|
|
627
819
|
//#endregion
|
|
628
|
-
export {
|
|
820
|
+
export { createAgentReplyGenerator as a, chatContextToMessages as c, MastraVoiceAgent as i, extractNewTurnMessages as l, mastraLLMNode as n, createMastraVoiceAgent as o, observeVoiceSession as r, instrumentVoiceReply as s, createRemoteAgentReplyGenerator as t };
|
|
629
821
|
|
|
630
|
-
//# sourceMappingURL=remote-
|
|
822
|
+
//# sourceMappingURL=remote-CIMCGicy.js.map
|