@mastra/livekit 0.3.1 → 0.3.2
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 +9 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm-plugin.d.ts +4 -3
- package/dist/llm-plugin.d.ts.map +1 -1
- package/dist/plugin-entry.cjs +1 -1
- package/dist/plugin-entry.cjs.map +1 -1
- package/dist/plugin-entry.js +1 -1
- package/dist/plugin-entry.js.map +1 -1
- package/dist/{remote-BZ7eyB1q.cjs → remote-DlCC6aCn.cjs} +3 -2
- package/dist/remote-DlCC6aCn.cjs.map +1 -0
- package/dist/{remote-D7n50m8S.js → remote-bPj_0lAr.js} +3 -2
- package/dist/remote-bPj_0lAr.js.map +1 -0
- package/dist/remote.d.ts.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/worker-entry.cjs +1 -1
- package/dist/worker-entry.cjs.map +1 -1
- package/dist/worker-entry.js +1 -1
- package/dist/worker-entry.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/remote-BZ7eyB1q.cjs.map +0 -1
- package/dist/remote-D7n50m8S.js.map +0 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReadableStream } from 'node:stream/web';
|
|
2
2
|
import { llm, voice } from '@livekit/agents';
|
|
3
|
-
import type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';
|
|
3
|
+
import type { Agent as MastraAgent, AgentExecutionOptionsBase, AgentMemoryOption } from '@mastra/core/agent';
|
|
4
4
|
import type { TracingContext } from '@mastra/core/observability';
|
|
5
5
|
import { RequestContext } from '@mastra/core/request-context';
|
|
6
6
|
import type { VoiceTurnMessage } from './messages.js';
|
|
@@ -59,6 +59,14 @@ export declare function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined
|
|
|
59
59
|
export interface MastraVoiceAgentMemory {
|
|
60
60
|
thread: string;
|
|
61
61
|
resource?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Per-call Mastra memory config forwarded to the agent as `memory.options` (see core's
|
|
64
|
+
* `AgentMemoryOption`). The notable use is `{ readOnly: true }`: the agent still reads thread
|
|
65
|
+
* history but persists nothing, which keeps LiveKit's preemptive (speculative) turns from writing
|
|
66
|
+
* partial user and assistant messages to the thread. With `readOnly` the caller owns persistence
|
|
67
|
+
* of committed turns (e.g. from `onTurnComplete` via `memory.saveMessages`).
|
|
68
|
+
*/
|
|
69
|
+
options?: AgentMemoryOption['options'];
|
|
62
70
|
}
|
|
63
71
|
/**
|
|
64
72
|
* Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["AccessToken","RoomConfiguration","RoomAgentDispatch","serializeSessionMetadata","AgentDispatchClient","serializeSessionMetadata","DEFAULT_DESCRIPTION","z","z"],"sources":["../src/routes.ts","../src/dispatch.ts","../src/consent.ts","../src/end-call.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';\nimport type { ContextWithMastra, ApiRoute } from '@mastra/core/server';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\n/** Response body of the connection-details route. Matches LiveKit's frontend starter contract. */\nexport interface LiveKitConnectionDetails {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n}\n\nexport interface ConnectionRequestArgs {\n body: Record<string, unknown>;\n context: ContextWithMastra;\n}\n\nexport interface LiveKitConnectionRouteOptions {\n /**\n * Route path. Defaults to `/voice/livekit/connection-details`. Mastra reserves the\n * `/api` prefix for built-in routes, so custom paths must not start with `/api`.\n */\n path?: string;\n /** LiveKit server URL (`wss://...`). Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n /** LiveKit agent name for explicit dispatch. Must match the worker's `agentName`. */\n agentName?: string;\n /** Token time-to-live. Defaults to `'15m'`. */\n ttl?: string | number;\n /** Defaults to `true` (Mastra custom routes require auth unless opted out). */\n requiresAuth?: boolean;\n roomName?: string | ((args: ConnectionRequestArgs) => string);\n participantIdentity?: string | ((args: ConnectionRequestArgs) => string);\n /**\n * Session metadata delivered to the worker via agent dispatch. Defaults to passing\n * through `agentId`, `threadId`, and `resourceId` from the request body.\n */\n metadata?: (args: ConnectionRequestArgs) => LiveKitSessionMetadata | Promise<LiveKitSessionMetadata>;\n}\n\nfunction stringField(body: Record<string, unknown>, key: string): string | undefined {\n const value = body[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * An ApiRoute for `server.apiRoutes` that mints a LiveKit access token with the Mastra\n * voice agent dispatched into the room. Frontends call it to join a voice session:\n *\n * ```ts\n * export const mastra = new Mastra({\n * agents: { support },\n * server: { apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })] },\n * });\n * ```\n */\nexport function liveKitConnectionRoute(options: LiveKitConnectionRouteOptions = {}): ApiRoute {\n const handler = async (c: ContextWithMastra) => {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl || !apiKey || !apiSecret) {\n return c.json(\n {\n error:\n 'LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +\n '(or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute).',\n },\n 500,\n );\n }\n\n const body: Record<string, unknown> = await c.req\n .json()\n .then((parsed: unknown) =>\n parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {},\n )\n .catch(() => ({}));\n const args: ConnectionRequestArgs = { body, context: c };\n\n const metadata: LiveKitSessionMetadata = options.metadata\n ? await options.metadata(args)\n : {\n agentId: stringField(body, 'agentId'),\n threadId: stringField(body, 'threadId'),\n resourceId: stringField(body, 'resourceId'),\n };\n\n const roomName =\n typeof options.roomName === 'function'\n ? options.roomName(args)\n : (options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`);\n const identity =\n typeof options.participantIdentity === 'function'\n ? options.participantIdentity(args)\n : (options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`);\n // One memory thread per room unless the caller pins a thread explicitly.\n metadata.threadId ??= roomName;\n\n const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? '15m' });\n token.addGrant({\n room: roomName,\n roomJoin: true,\n canPublish: true,\n canSubscribe: true,\n canPublishData: true,\n canUpdateOwnMetadata: true,\n });\n token.roomConfig = new RoomConfiguration({\n agents: [\n new RoomAgentDispatch({\n agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,\n metadata: serializeSessionMetadata(metadata),\n }),\n ],\n });\n\n const details: LiveKitConnectionDetails = {\n serverUrl,\n roomName,\n participantName: identity,\n participantToken: await token.toJwt(),\n };\n return c.json(details);\n };\n\n return {\n path: options.path ?? '/voice/livekit/connection-details',\n method: 'POST',\n requiresAuth: options.requiresAuth,\n handler,\n } as ApiRoute;\n}\n","import { AgentDispatchClient } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\nexport interface DispatchVoiceSessionOptions {\n /** Room to dispatch the agent into (created on demand). */\n roomName: string;\n /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */\n agentName?: string;\n metadata?: LiveKitSessionMetadata;\n /** Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n}\n\nfunction toHttpUrl(url: string): string {\n return url.replace(/^ws/, 'http');\n}\n\n/**\n * Programmatically dispatches a Mastra voice agent into a LiveKit room — for\n * server-initiated sessions such as outbound calls or joining an existing room.\n */\nexport async function dispatchVoiceSession(options: DispatchVoiceSessionOptions) {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl) {\n throw new Error('@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.');\n }\n if (!apiKey || !apiSecret) {\n throw new Error(\n '@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession.',\n );\n }\n const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);\n return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {\n metadata: serializeSessionMetadata(options.metadata ?? {}),\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** A consent decision captured from the caller during a call. */\nexport interface ConsentGrant {\n /**\n * The consent item, matching a key in the worker's `configuration.consentPolicy` (e.g.\n * `'summaryStorage'`).\n */\n item: string;\n /** `true` if the caller agreed, `false` if they declined. */\n granted: boolean;\n /** The caller the grant belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread the grant was captured on, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface ConsentToolOptions {\n /**\n * Persist the caller's decision to YOUR system of record (database, CRM, consent ledger). Called\n * each time the caller answers a consent question — keep it quick, it runs inside the turn. This\n * tool owns extracting the caller's identity from the tool execution context; you own storage, so\n * consent lives in a durable, compliant place rather than an opaque plugin store. Read those\n * grants back at `onCallEnd` (or before any consent-gated step) to enforce the requirement.\n */\n onGrant: (grant: ConsentGrant) => void | Promise<void>;\n /** Tool id the agent calls. Defaults to `'recordConsent'`. */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /** Restrict the accepted consent items to this set (becomes an enum). Defaults to any string. */\n items?: readonly string[];\n}\n\nconst DEFAULT_DESCRIPTION =\n \"Record the caller's consent decision for a data-processing item (for example, storing a summary \" +\n 'of the call). Call this as soon as the caller answers a consent question: granted = true if they ' +\n 'agree, false if they decline. Do not proceed with an activity that requires consent until you ' +\n 'have recorded the caller granting it.';\n\n/**\n * Builds a Mastra tool that captures the caller's consent decisions at runtime — the companion to\n * the worker's `configuration.consentPolicy`, which only DECLARES which consents a call needs. Add\n * it to the agent that answers the call; on each call the tool reads the caller's `resourceId` /\n * `threadId` from its execution context and hands the decision to your `onGrant` store. The runtime\n * grants live only where `onGrant` put them: enforce consent by reading them back from your own\n * store — at `onCallEnd` or before any consent-gated action — cross-checking against the declared\n * requirements in `VoiceCallEndArgs.configuration.consentPolicy` (the configuration carries the\n * policy, never the grants).\n *\n * ```ts\n * // agent tools\n * recordConsent: createConsentTool({\n * items: ['summaryStorage'],\n * onGrant: async ({ item, granted, resourceId }) => {\n * if (resourceId) await db.saveConsent(resourceId, item, granted);\n * },\n * }),\n * ```\n */\nexport function createConsentTool(options: ConsentToolOptions) {\n const items = options.items;\n const itemSchema = items && items.length > 0 ? z.enum([...items] as [string, ...string[]]) : z.string();\n return createTool({\n id: options.id ?? 'recordConsent',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n item: itemSchema.describe(\n \"The consent item the caller is answering for (matches the deployment's required consents).\",\n ),\n granted: z.boolean().describe('True if the caller agreed, false if they declined.'),\n }),\n execute: async ({ item, granted }, { agent }) => {\n await options.onGrant({ item, granted, resourceId: agent?.resourceId, threadId: agent?.threadId });\n return { recorded: true as const, item, granted };\n },\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** Details of the agent's decision to end the call, handed to {@link EndCallToolOptions.onEndCall}. */\nexport interface EndCallRequest {\n /** Short reason the agent gave for ending the call, if any (e.g. \"caller said goodbye\"). */\n reason?: string;\n /** The caller the call belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface EndCallToolOptions {\n /**\n * Tool id the agent calls to end the call. Must match the worker's\n * `configuration.endCall.tool` (both default to `'endCall'`), since that's the name the worker\n * watches for to hang up.\n */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /**\n * Optional bookkeeping hook, called when the agent invokes the tool (records the reason, marks\n * the call resolved in your CRM, …). Runs inside the turn — keep it quick. It does NOT hang up\n * the call; the worker does that once the agent's final words finish playing. The tool reads the\n * caller's identity from its execution context and passes it here.\n */\n onEndCall?: (request: EndCallRequest) => void | Promise<void>;\n}\n\nconst DEFAULT_DESCRIPTION =\n 'End the phone call. Call this only after you have said goodbye and there is nothing left to do — ' +\n 'for example the caller says goodbye, or the task is complete and you have wrapped up. Say your ' +\n 'closing line first, then call this as your final action; the call hangs up once your words finish ' +\n 'playing. Your closing line must be a final statement, never a question — if you are asking the ' +\n 'caller anything (for example whether they need anything else), do NOT call this in that reply; ' +\n 'wait for their answer. Do not call it if the caller still needs something.';\n\n/**\n * Builds a Mastra tool the agent calls to end the call itself (say goodbye → hang up). It is the\n * agent-visible half of agent-initiated hang-up: the tool only SIGNALS intent (and runs optional\n * bookkeeping) — it can't reach the LiveKit room from inside `agent.stream()`. The worker owns the\n * hang-up: with `configuration.endCall` set, it watches each turn for this tool, waits for the\n * agent's closing words to finish playing, then disconnects (running `onCallEnd` on the way out).\n * Keeping the tool inside the single agent means it stays visible/editable in Studio and works on\n * the agent and workflow reply paths alike.\n *\n * ```ts\n * // agent tools\n * endCall: createEndCallTool({\n * onEndCall: ({ reason, resourceId }) => log.info('agent ended call', { reason, resourceId }),\n * }),\n * // worker\n * createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } });\n * ```\n */\nexport function createEndCallTool(options: EndCallToolOptions = {}) {\n return createTool({\n id: options.id ?? 'endCall',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n reason: z\n .string()\n .nullish()\n .describe('Short reason the call is ending, e.g. \"caller said goodbye\" or \"task complete\".'),\n }),\n outputSchema: z.object({ ended: z.boolean() }),\n execute: async ({ reason }, { agent }) => {\n try {\n await options.onEndCall?.({\n reason: reason ?? undefined,\n resourceId: agent?.resourceId,\n threadId: agent?.threadId,\n });\n } catch (error) {\n // Bookkeeping is best-effort — the tool must still signal `{ ended: true }` so the worker's\n // end-call detector runs and the call actually hangs up.\n console.warn('@mastra/livekit: onEndCall hook threw', error);\n }\n return { ended: true as const };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["AccessToken","RoomConfiguration","RoomAgentDispatch","serializeSessionMetadata","AgentDispatchClient","serializeSessionMetadata","DEFAULT_DESCRIPTION","z","z"],"sources":["../src/routes.ts","../src/dispatch.ts","../src/consent.ts","../src/end-call.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';\nimport type { ContextWithMastra, ApiRoute } from '@mastra/core/server';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\n/** Response body of the connection-details route. Matches LiveKit's frontend starter contract. */\nexport interface LiveKitConnectionDetails {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n}\n\nexport interface ConnectionRequestArgs {\n body: Record<string, unknown>;\n context: ContextWithMastra;\n}\n\nexport interface LiveKitConnectionRouteOptions {\n /**\n * Route path. Defaults to `/voice/livekit/connection-details`. Mastra reserves the\n * `/api` prefix for built-in routes, so custom paths must not start with `/api`.\n */\n path?: string;\n /** LiveKit server URL (`wss://...`). Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n /** LiveKit agent name for explicit dispatch. Must match the worker's `agentName`. */\n agentName?: string;\n /** Token time-to-live. Defaults to `'15m'`. */\n ttl?: string | number;\n /** Defaults to `true` (Mastra custom routes require auth unless opted out). */\n requiresAuth?: boolean;\n roomName?: string | ((args: ConnectionRequestArgs) => string);\n participantIdentity?: string | ((args: ConnectionRequestArgs) => string);\n /**\n * Session metadata delivered to the worker via agent dispatch. Defaults to passing\n * through `agentId`, `threadId`, and `resourceId` from the request body.\n */\n metadata?: (args: ConnectionRequestArgs) => LiveKitSessionMetadata | Promise<LiveKitSessionMetadata>;\n}\n\nfunction stringField(body: Record<string, unknown>, key: string): string | undefined {\n const value = body[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * An ApiRoute for `server.apiRoutes` that mints a LiveKit access token with the Mastra\n * voice agent dispatched into the room. Frontends call it to join a voice session:\n *\n * ```ts\n * export const mastra = new Mastra({\n * agents: { support },\n * server: { apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })] },\n * });\n * ```\n */\nexport function liveKitConnectionRoute(options: LiveKitConnectionRouteOptions = {}): ApiRoute {\n const handler = async (c: ContextWithMastra) => {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl || !apiKey || !apiSecret) {\n return c.json(\n {\n error:\n 'LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +\n '(or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute).',\n },\n 500,\n );\n }\n\n const body: Record<string, unknown> = await c.req\n .json()\n .then((parsed: unknown) =>\n parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {},\n )\n .catch(() => ({}));\n const args: ConnectionRequestArgs = { body, context: c };\n\n const metadata: LiveKitSessionMetadata = options.metadata\n ? await options.metadata(args)\n : {\n agentId: stringField(body, 'agentId'),\n threadId: stringField(body, 'threadId'),\n resourceId: stringField(body, 'resourceId'),\n };\n\n const roomName =\n typeof options.roomName === 'function'\n ? options.roomName(args)\n : (options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`);\n const identity =\n typeof options.participantIdentity === 'function'\n ? options.participantIdentity(args)\n : (options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`);\n // One memory thread per room unless the caller pins a thread explicitly.\n metadata.threadId ??= roomName;\n\n const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? '15m' });\n token.addGrant({\n room: roomName,\n roomJoin: true,\n canPublish: true,\n canSubscribe: true,\n canPublishData: true,\n canUpdateOwnMetadata: true,\n });\n token.roomConfig = new RoomConfiguration({\n agents: [\n new RoomAgentDispatch({\n agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,\n metadata: serializeSessionMetadata(metadata),\n }),\n ],\n });\n\n const details: LiveKitConnectionDetails = {\n serverUrl,\n roomName,\n participantName: identity,\n participantToken: await token.toJwt(),\n };\n return c.json(details);\n };\n\n return {\n // Studio calls this default path and /api/system/packages reports it — change all three together.\n path: options.path ?? '/voice/livekit/connection-details',\n method: 'POST',\n requiresAuth: options.requiresAuth,\n handler,\n } as ApiRoute;\n}\n","import { AgentDispatchClient } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\nexport interface DispatchVoiceSessionOptions {\n /** Room to dispatch the agent into (created on demand). */\n roomName: string;\n /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */\n agentName?: string;\n metadata?: LiveKitSessionMetadata;\n /** Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n}\n\nfunction toHttpUrl(url: string): string {\n return url.replace(/^ws/, 'http');\n}\n\n/**\n * Programmatically dispatches a Mastra voice agent into a LiveKit room — for\n * server-initiated sessions such as outbound calls or joining an existing room.\n */\nexport async function dispatchVoiceSession(options: DispatchVoiceSessionOptions) {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl) {\n throw new Error('@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.');\n }\n if (!apiKey || !apiSecret) {\n throw new Error(\n '@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession.',\n );\n }\n const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);\n return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {\n metadata: serializeSessionMetadata(options.metadata ?? {}),\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** A consent decision captured from the caller during a call. */\nexport interface ConsentGrant {\n /**\n * The consent item, matching a key in the worker's `configuration.consentPolicy` (e.g.\n * `'summaryStorage'`).\n */\n item: string;\n /** `true` if the caller agreed, `false` if they declined. */\n granted: boolean;\n /** The caller the grant belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread the grant was captured on, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface ConsentToolOptions {\n /**\n * Persist the caller's decision to YOUR system of record (database, CRM, consent ledger). Called\n * each time the caller answers a consent question — keep it quick, it runs inside the turn. This\n * tool owns extracting the caller's identity from the tool execution context; you own storage, so\n * consent lives in a durable, compliant place rather than an opaque plugin store. Read those\n * grants back at `onCallEnd` (or before any consent-gated step) to enforce the requirement.\n */\n onGrant: (grant: ConsentGrant) => void | Promise<void>;\n /** Tool id the agent calls. Defaults to `'recordConsent'`. */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /** Restrict the accepted consent items to this set (becomes an enum). Defaults to any string. */\n items?: readonly string[];\n}\n\nconst DEFAULT_DESCRIPTION =\n \"Record the caller's consent decision for a data-processing item (for example, storing a summary \" +\n 'of the call). Call this as soon as the caller answers a consent question: granted = true if they ' +\n 'agree, false if they decline. Do not proceed with an activity that requires consent until you ' +\n 'have recorded the caller granting it.';\n\n/**\n * Builds a Mastra tool that captures the caller's consent decisions at runtime — the companion to\n * the worker's `configuration.consentPolicy`, which only DECLARES which consents a call needs. Add\n * it to the agent that answers the call; on each call the tool reads the caller's `resourceId` /\n * `threadId` from its execution context and hands the decision to your `onGrant` store. The runtime\n * grants live only where `onGrant` put them: enforce consent by reading them back from your own\n * store — at `onCallEnd` or before any consent-gated action — cross-checking against the declared\n * requirements in `VoiceCallEndArgs.configuration.consentPolicy` (the configuration carries the\n * policy, never the grants).\n *\n * ```ts\n * // agent tools\n * recordConsent: createConsentTool({\n * items: ['summaryStorage'],\n * onGrant: async ({ item, granted, resourceId }) => {\n * if (resourceId) await db.saveConsent(resourceId, item, granted);\n * },\n * }),\n * ```\n */\nexport function createConsentTool(options: ConsentToolOptions) {\n const items = options.items;\n const itemSchema = items && items.length > 0 ? z.enum([...items] as [string, ...string[]]) : z.string();\n return createTool({\n id: options.id ?? 'recordConsent',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n item: itemSchema.describe(\n \"The consent item the caller is answering for (matches the deployment's required consents).\",\n ),\n granted: z.boolean().describe('True if the caller agreed, false if they declined.'),\n }),\n execute: async ({ item, granted }, { agent }) => {\n await options.onGrant({ item, granted, resourceId: agent?.resourceId, threadId: agent?.threadId });\n return { recorded: true as const, item, granted };\n },\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** Details of the agent's decision to end the call, handed to {@link EndCallToolOptions.onEndCall}. */\nexport interface EndCallRequest {\n /** Short reason the agent gave for ending the call, if any (e.g. \"caller said goodbye\"). */\n reason?: string;\n /** The caller the call belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface EndCallToolOptions {\n /**\n * Tool id the agent calls to end the call. Must match the worker's\n * `configuration.endCall.tool` (both default to `'endCall'`), since that's the name the worker\n * watches for to hang up.\n */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /**\n * Optional bookkeeping hook, called when the agent invokes the tool (records the reason, marks\n * the call resolved in your CRM, …). Runs inside the turn — keep it quick. It does NOT hang up\n * the call; the worker does that once the agent's final words finish playing. The tool reads the\n * caller's identity from its execution context and passes it here.\n */\n onEndCall?: (request: EndCallRequest) => void | Promise<void>;\n}\n\nconst DEFAULT_DESCRIPTION =\n 'End the phone call. Call this only after you have said goodbye and there is nothing left to do — ' +\n 'for example the caller says goodbye, or the task is complete and you have wrapped up. Say your ' +\n 'closing line first, then call this as your final action; the call hangs up once your words finish ' +\n 'playing. Your closing line must be a final statement, never a question — if you are asking the ' +\n 'caller anything (for example whether they need anything else), do NOT call this in that reply; ' +\n 'wait for their answer. Do not call it if the caller still needs something.';\n\n/**\n * Builds a Mastra tool the agent calls to end the call itself (say goodbye → hang up). It is the\n * agent-visible half of agent-initiated hang-up: the tool only SIGNALS intent (and runs optional\n * bookkeeping) — it can't reach the LiveKit room from inside `agent.stream()`. The worker owns the\n * hang-up: with `configuration.endCall` set, it watches each turn for this tool, waits for the\n * agent's closing words to finish playing, then disconnects (running `onCallEnd` on the way out).\n * Keeping the tool inside the single agent means it stays visible/editable in Studio and works on\n * the agent and workflow reply paths alike.\n *\n * ```ts\n * // agent tools\n * endCall: createEndCallTool({\n * onEndCall: ({ reason, resourceId }) => log.info('agent ended call', { reason, resourceId }),\n * }),\n * // worker\n * createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } });\n * ```\n */\nexport function createEndCallTool(options: EndCallToolOptions = {}) {\n return createTool({\n id: options.id ?? 'endCall',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n reason: z\n .string()\n .nullish()\n .describe('Short reason the call is ending, e.g. \"caller said goodbye\" or \"task complete\".'),\n }),\n outputSchema: z.object({ ended: z.boolean() }),\n execute: async ({ reason }, { agent }) => {\n try {\n await options.onEndCall?.({\n reason: reason ?? undefined,\n resourceId: agent?.resourceId,\n threadId: agent?.threadId,\n });\n } catch (error) {\n // Bookkeeping is best-effort — the tool must still signal `{ ended: true }` so the worker's\n // end-call detector runs and the call actually hangs up.\n console.warn('@mastra/livekit: onEndCall hook threw', error);\n }\n return { ended: true as const };\n },\n });\n}\n"],"mappings":"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"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["DEFAULT_DESCRIPTION"],"sources":["../src/routes.ts","../src/dispatch.ts","../src/consent.ts","../src/end-call.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';\nimport type { ContextWithMastra, ApiRoute } from '@mastra/core/server';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\n/** Response body of the connection-details route. Matches LiveKit's frontend starter contract. */\nexport interface LiveKitConnectionDetails {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n}\n\nexport interface ConnectionRequestArgs {\n body: Record<string, unknown>;\n context: ContextWithMastra;\n}\n\nexport interface LiveKitConnectionRouteOptions {\n /**\n * Route path. Defaults to `/voice/livekit/connection-details`. Mastra reserves the\n * `/api` prefix for built-in routes, so custom paths must not start with `/api`.\n */\n path?: string;\n /** LiveKit server URL (`wss://...`). Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n /** LiveKit agent name for explicit dispatch. Must match the worker's `agentName`. */\n agentName?: string;\n /** Token time-to-live. Defaults to `'15m'`. */\n ttl?: string | number;\n /** Defaults to `true` (Mastra custom routes require auth unless opted out). */\n requiresAuth?: boolean;\n roomName?: string | ((args: ConnectionRequestArgs) => string);\n participantIdentity?: string | ((args: ConnectionRequestArgs) => string);\n /**\n * Session metadata delivered to the worker via agent dispatch. Defaults to passing\n * through `agentId`, `threadId`, and `resourceId` from the request body.\n */\n metadata?: (args: ConnectionRequestArgs) => LiveKitSessionMetadata | Promise<LiveKitSessionMetadata>;\n}\n\nfunction stringField(body: Record<string, unknown>, key: string): string | undefined {\n const value = body[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * An ApiRoute for `server.apiRoutes` that mints a LiveKit access token with the Mastra\n * voice agent dispatched into the room. Frontends call it to join a voice session:\n *\n * ```ts\n * export const mastra = new Mastra({\n * agents: { support },\n * server: { apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })] },\n * });\n * ```\n */\nexport function liveKitConnectionRoute(options: LiveKitConnectionRouteOptions = {}): ApiRoute {\n const handler = async (c: ContextWithMastra) => {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl || !apiKey || !apiSecret) {\n return c.json(\n {\n error:\n 'LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +\n '(or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute).',\n },\n 500,\n );\n }\n\n const body: Record<string, unknown> = await c.req\n .json()\n .then((parsed: unknown) =>\n parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {},\n )\n .catch(() => ({}));\n const args: ConnectionRequestArgs = { body, context: c };\n\n const metadata: LiveKitSessionMetadata = options.metadata\n ? await options.metadata(args)\n : {\n agentId: stringField(body, 'agentId'),\n threadId: stringField(body, 'threadId'),\n resourceId: stringField(body, 'resourceId'),\n };\n\n const roomName =\n typeof options.roomName === 'function'\n ? options.roomName(args)\n : (options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`);\n const identity =\n typeof options.participantIdentity === 'function'\n ? options.participantIdentity(args)\n : (options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`);\n // One memory thread per room unless the caller pins a thread explicitly.\n metadata.threadId ??= roomName;\n\n const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? '15m' });\n token.addGrant({\n room: roomName,\n roomJoin: true,\n canPublish: true,\n canSubscribe: true,\n canPublishData: true,\n canUpdateOwnMetadata: true,\n });\n token.roomConfig = new RoomConfiguration({\n agents: [\n new RoomAgentDispatch({\n agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,\n metadata: serializeSessionMetadata(metadata),\n }),\n ],\n });\n\n const details: LiveKitConnectionDetails = {\n serverUrl,\n roomName,\n participantName: identity,\n participantToken: await token.toJwt(),\n };\n return c.json(details);\n };\n\n return {\n path: options.path ?? '/voice/livekit/connection-details',\n method: 'POST',\n requiresAuth: options.requiresAuth,\n handler,\n } as ApiRoute;\n}\n","import { AgentDispatchClient } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\nexport interface DispatchVoiceSessionOptions {\n /** Room to dispatch the agent into (created on demand). */\n roomName: string;\n /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */\n agentName?: string;\n metadata?: LiveKitSessionMetadata;\n /** Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n}\n\nfunction toHttpUrl(url: string): string {\n return url.replace(/^ws/, 'http');\n}\n\n/**\n * Programmatically dispatches a Mastra voice agent into a LiveKit room — for\n * server-initiated sessions such as outbound calls or joining an existing room.\n */\nexport async function dispatchVoiceSession(options: DispatchVoiceSessionOptions) {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl) {\n throw new Error('@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.');\n }\n if (!apiKey || !apiSecret) {\n throw new Error(\n '@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession.',\n );\n }\n const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);\n return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {\n metadata: serializeSessionMetadata(options.metadata ?? {}),\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** A consent decision captured from the caller during a call. */\nexport interface ConsentGrant {\n /**\n * The consent item, matching a key in the worker's `configuration.consentPolicy` (e.g.\n * `'summaryStorage'`).\n */\n item: string;\n /** `true` if the caller agreed, `false` if they declined. */\n granted: boolean;\n /** The caller the grant belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread the grant was captured on, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface ConsentToolOptions {\n /**\n * Persist the caller's decision to YOUR system of record (database, CRM, consent ledger). Called\n * each time the caller answers a consent question — keep it quick, it runs inside the turn. This\n * tool owns extracting the caller's identity from the tool execution context; you own storage, so\n * consent lives in a durable, compliant place rather than an opaque plugin store. Read those\n * grants back at `onCallEnd` (or before any consent-gated step) to enforce the requirement.\n */\n onGrant: (grant: ConsentGrant) => void | Promise<void>;\n /** Tool id the agent calls. Defaults to `'recordConsent'`. */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /** Restrict the accepted consent items to this set (becomes an enum). Defaults to any string. */\n items?: readonly string[];\n}\n\nconst DEFAULT_DESCRIPTION =\n \"Record the caller's consent decision for a data-processing item (for example, storing a summary \" +\n 'of the call). Call this as soon as the caller answers a consent question: granted = true if they ' +\n 'agree, false if they decline. Do not proceed with an activity that requires consent until you ' +\n 'have recorded the caller granting it.';\n\n/**\n * Builds a Mastra tool that captures the caller's consent decisions at runtime — the companion to\n * the worker's `configuration.consentPolicy`, which only DECLARES which consents a call needs. Add\n * it to the agent that answers the call; on each call the tool reads the caller's `resourceId` /\n * `threadId` from its execution context and hands the decision to your `onGrant` store. The runtime\n * grants live only where `onGrant` put them: enforce consent by reading them back from your own\n * store — at `onCallEnd` or before any consent-gated action — cross-checking against the declared\n * requirements in `VoiceCallEndArgs.configuration.consentPolicy` (the configuration carries the\n * policy, never the grants).\n *\n * ```ts\n * // agent tools\n * recordConsent: createConsentTool({\n * items: ['summaryStorage'],\n * onGrant: async ({ item, granted, resourceId }) => {\n * if (resourceId) await db.saveConsent(resourceId, item, granted);\n * },\n * }),\n * ```\n */\nexport function createConsentTool(options: ConsentToolOptions) {\n const items = options.items;\n const itemSchema = items && items.length > 0 ? z.enum([...items] as [string, ...string[]]) : z.string();\n return createTool({\n id: options.id ?? 'recordConsent',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n item: itemSchema.describe(\n \"The consent item the caller is answering for (matches the deployment's required consents).\",\n ),\n granted: z.boolean().describe('True if the caller agreed, false if they declined.'),\n }),\n execute: async ({ item, granted }, { agent }) => {\n await options.onGrant({ item, granted, resourceId: agent?.resourceId, threadId: agent?.threadId });\n return { recorded: true as const, item, granted };\n },\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** Details of the agent's decision to end the call, handed to {@link EndCallToolOptions.onEndCall}. */\nexport interface EndCallRequest {\n /** Short reason the agent gave for ending the call, if any (e.g. \"caller said goodbye\"). */\n reason?: string;\n /** The caller the call belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface EndCallToolOptions {\n /**\n * Tool id the agent calls to end the call. Must match the worker's\n * `configuration.endCall.tool` (both default to `'endCall'`), since that's the name the worker\n * watches for to hang up.\n */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /**\n * Optional bookkeeping hook, called when the agent invokes the tool (records the reason, marks\n * the call resolved in your CRM, …). Runs inside the turn — keep it quick. It does NOT hang up\n * the call; the worker does that once the agent's final words finish playing. The tool reads the\n * caller's identity from its execution context and passes it here.\n */\n onEndCall?: (request: EndCallRequest) => void | Promise<void>;\n}\n\nconst DEFAULT_DESCRIPTION =\n 'End the phone call. Call this only after you have said goodbye and there is nothing left to do — ' +\n 'for example the caller says goodbye, or the task is complete and you have wrapped up. Say your ' +\n 'closing line first, then call this as your final action; the call hangs up once your words finish ' +\n 'playing. Your closing line must be a final statement, never a question — if you are asking the ' +\n 'caller anything (for example whether they need anything else), do NOT call this in that reply; ' +\n 'wait for their answer. Do not call it if the caller still needs something.';\n\n/**\n * Builds a Mastra tool the agent calls to end the call itself (say goodbye → hang up). It is the\n * agent-visible half of agent-initiated hang-up: the tool only SIGNALS intent (and runs optional\n * bookkeeping) — it can't reach the LiveKit room from inside `agent.stream()`. The worker owns the\n * hang-up: with `configuration.endCall` set, it watches each turn for this tool, waits for the\n * agent's closing words to finish playing, then disconnects (running `onCallEnd` on the way out).\n * Keeping the tool inside the single agent means it stays visible/editable in Studio and works on\n * the agent and workflow reply paths alike.\n *\n * ```ts\n * // agent tools\n * endCall: createEndCallTool({\n * onEndCall: ({ reason, resourceId }) => log.info('agent ended call', { reason, resourceId }),\n * }),\n * // worker\n * createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } });\n * ```\n */\nexport function createEndCallTool(options: EndCallToolOptions = {}) {\n return createTool({\n id: options.id ?? 'endCall',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n reason: z\n .string()\n .nullish()\n .describe('Short reason the call is ending, e.g. \"caller said goodbye\" or \"task complete\".'),\n }),\n outputSchema: z.object({ ended: z.boolean() }),\n execute: async ({ reason }, { agent }) => {\n try {\n await options.onEndCall?.({\n reason: reason ?? undefined,\n resourceId: agent?.resourceId,\n threadId: agent?.threadId,\n });\n } catch (error) {\n // Bookkeeping is best-effort — the tool must still signal `{ ended: true }` so the worker's\n // end-call detector runs and the call actually hangs up.\n console.warn('@mastra/livekit: onEndCall hook threw', error);\n }\n return { ended: true as const };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["DEFAULT_DESCRIPTION"],"sources":["../src/routes.ts","../src/dispatch.ts","../src/consent.ts","../src/end-call.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { RoomAgentDispatch, RoomConfiguration } from '@livekit/protocol';\nimport type { ContextWithMastra, ApiRoute } from '@mastra/core/server';\nimport { AccessToken } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\n/** Response body of the connection-details route. Matches LiveKit's frontend starter contract. */\nexport interface LiveKitConnectionDetails {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n}\n\nexport interface ConnectionRequestArgs {\n body: Record<string, unknown>;\n context: ContextWithMastra;\n}\n\nexport interface LiveKitConnectionRouteOptions {\n /**\n * Route path. Defaults to `/voice/livekit/connection-details`. Mastra reserves the\n * `/api` prefix for built-in routes, so custom paths must not start with `/api`.\n */\n path?: string;\n /** LiveKit server URL (`wss://...`). Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n /** LiveKit agent name for explicit dispatch. Must match the worker's `agentName`. */\n agentName?: string;\n /** Token time-to-live. Defaults to `'15m'`. */\n ttl?: string | number;\n /** Defaults to `true` (Mastra custom routes require auth unless opted out). */\n requiresAuth?: boolean;\n roomName?: string | ((args: ConnectionRequestArgs) => string);\n participantIdentity?: string | ((args: ConnectionRequestArgs) => string);\n /**\n * Session metadata delivered to the worker via agent dispatch. Defaults to passing\n * through `agentId`, `threadId`, and `resourceId` from the request body.\n */\n metadata?: (args: ConnectionRequestArgs) => LiveKitSessionMetadata | Promise<LiveKitSessionMetadata>;\n}\n\nfunction stringField(body: Record<string, unknown>, key: string): string | undefined {\n const value = body[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/**\n * An ApiRoute for `server.apiRoutes` that mints a LiveKit access token with the Mastra\n * voice agent dispatched into the room. Frontends call it to join a voice session:\n *\n * ```ts\n * export const mastra = new Mastra({\n * agents: { support },\n * server: { apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })] },\n * });\n * ```\n */\nexport function liveKitConnectionRoute(options: LiveKitConnectionRouteOptions = {}): ApiRoute {\n const handler = async (c: ContextWithMastra) => {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl || !apiKey || !apiSecret) {\n return c.json(\n {\n error:\n 'LiveKit is not configured. Set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET ' +\n '(or pass serverUrl/apiKey/apiSecret to liveKitConnectionRoute).',\n },\n 500,\n );\n }\n\n const body: Record<string, unknown> = await c.req\n .json()\n .then((parsed: unknown) =>\n parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {},\n )\n .catch(() => ({}));\n const args: ConnectionRequestArgs = { body, context: c };\n\n const metadata: LiveKitSessionMetadata = options.metadata\n ? await options.metadata(args)\n : {\n agentId: stringField(body, 'agentId'),\n threadId: stringField(body, 'threadId'),\n resourceId: stringField(body, 'resourceId'),\n };\n\n const roomName =\n typeof options.roomName === 'function'\n ? options.roomName(args)\n : (options.roomName ?? `mastra-voice-${randomUUID().slice(0, 8)}`);\n const identity =\n typeof options.participantIdentity === 'function'\n ? options.participantIdentity(args)\n : (options.participantIdentity ?? metadata.resourceId ?? `user-${randomUUID().slice(0, 8)}`);\n // One memory thread per room unless the caller pins a thread explicitly.\n metadata.threadId ??= roomName;\n\n const token = new AccessToken(apiKey, apiSecret, { identity, ttl: options.ttl ?? '15m' });\n token.addGrant({\n room: roomName,\n roomJoin: true,\n canPublish: true,\n canSubscribe: true,\n canPublishData: true,\n canUpdateOwnMetadata: true,\n });\n token.roomConfig = new RoomConfiguration({\n agents: [\n new RoomAgentDispatch({\n agentName: options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME,\n metadata: serializeSessionMetadata(metadata),\n }),\n ],\n });\n\n const details: LiveKitConnectionDetails = {\n serverUrl,\n roomName,\n participantName: identity,\n participantToken: await token.toJwt(),\n };\n return c.json(details);\n };\n\n return {\n // Studio calls this default path and /api/system/packages reports it — change all three together.\n path: options.path ?? '/voice/livekit/connection-details',\n method: 'POST',\n requiresAuth: options.requiresAuth,\n handler,\n } as ApiRoute;\n}\n","import { AgentDispatchClient } from 'livekit-server-sdk';\nimport { DEFAULT_LIVEKIT_AGENT_NAME } from './constants';\nimport { serializeSessionMetadata } from './metadata';\nimport type { LiveKitSessionMetadata } from './metadata';\n\nexport interface DispatchVoiceSessionOptions {\n /** Room to dispatch the agent into (created on demand). */\n roomName: string;\n /** Must match the worker's `agentName`. Defaults to `'mastra-voice'`. */\n agentName?: string;\n metadata?: LiveKitSessionMetadata;\n /** Defaults to `LIVEKIT_URL`. */\n serverUrl?: string;\n /** Defaults to `LIVEKIT_API_KEY`. */\n apiKey?: string;\n /** Defaults to `LIVEKIT_API_SECRET`. */\n apiSecret?: string;\n}\n\nfunction toHttpUrl(url: string): string {\n return url.replace(/^ws/, 'http');\n}\n\n/**\n * Programmatically dispatches a Mastra voice agent into a LiveKit room — for\n * server-initiated sessions such as outbound calls or joining an existing room.\n */\nexport async function dispatchVoiceSession(options: DispatchVoiceSessionOptions) {\n const serverUrl = options.serverUrl ?? process.env.LIVEKIT_URL;\n const apiKey = options.apiKey ?? process.env.LIVEKIT_API_KEY;\n const apiSecret = options.apiSecret ?? process.env.LIVEKIT_API_SECRET;\n if (!serverUrl) {\n throw new Error('@mastra/livekit: set LIVEKIT_URL or pass serverUrl to dispatchVoiceSession.');\n }\n if (!apiKey || !apiSecret) {\n throw new Error(\n '@mastra/livekit: set LIVEKIT_API_KEY and LIVEKIT_API_SECRET or pass apiKey/apiSecret to dispatchVoiceSession.',\n );\n }\n const client = new AgentDispatchClient(toHttpUrl(serverUrl), apiKey, apiSecret);\n return client.createDispatch(options.roomName, options.agentName ?? DEFAULT_LIVEKIT_AGENT_NAME, {\n metadata: serializeSessionMetadata(options.metadata ?? {}),\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** A consent decision captured from the caller during a call. */\nexport interface ConsentGrant {\n /**\n * The consent item, matching a key in the worker's `configuration.consentPolicy` (e.g.\n * `'summaryStorage'`).\n */\n item: string;\n /** `true` if the caller agreed, `false` if they declined. */\n granted: boolean;\n /** The caller the grant belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread the grant was captured on, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface ConsentToolOptions {\n /**\n * Persist the caller's decision to YOUR system of record (database, CRM, consent ledger). Called\n * each time the caller answers a consent question — keep it quick, it runs inside the turn. This\n * tool owns extracting the caller's identity from the tool execution context; you own storage, so\n * consent lives in a durable, compliant place rather than an opaque plugin store. Read those\n * grants back at `onCallEnd` (or before any consent-gated step) to enforce the requirement.\n */\n onGrant: (grant: ConsentGrant) => void | Promise<void>;\n /** Tool id the agent calls. Defaults to `'recordConsent'`. */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /** Restrict the accepted consent items to this set (becomes an enum). Defaults to any string. */\n items?: readonly string[];\n}\n\nconst DEFAULT_DESCRIPTION =\n \"Record the caller's consent decision for a data-processing item (for example, storing a summary \" +\n 'of the call). Call this as soon as the caller answers a consent question: granted = true if they ' +\n 'agree, false if they decline. Do not proceed with an activity that requires consent until you ' +\n 'have recorded the caller granting it.';\n\n/**\n * Builds a Mastra tool that captures the caller's consent decisions at runtime — the companion to\n * the worker's `configuration.consentPolicy`, which only DECLARES which consents a call needs. Add\n * it to the agent that answers the call; on each call the tool reads the caller's `resourceId` /\n * `threadId` from its execution context and hands the decision to your `onGrant` store. The runtime\n * grants live only where `onGrant` put them: enforce consent by reading them back from your own\n * store — at `onCallEnd` or before any consent-gated action — cross-checking against the declared\n * requirements in `VoiceCallEndArgs.configuration.consentPolicy` (the configuration carries the\n * policy, never the grants).\n *\n * ```ts\n * // agent tools\n * recordConsent: createConsentTool({\n * items: ['summaryStorage'],\n * onGrant: async ({ item, granted, resourceId }) => {\n * if (resourceId) await db.saveConsent(resourceId, item, granted);\n * },\n * }),\n * ```\n */\nexport function createConsentTool(options: ConsentToolOptions) {\n const items = options.items;\n const itemSchema = items && items.length > 0 ? z.enum([...items] as [string, ...string[]]) : z.string();\n return createTool({\n id: options.id ?? 'recordConsent',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n item: itemSchema.describe(\n \"The consent item the caller is answering for (matches the deployment's required consents).\",\n ),\n granted: z.boolean().describe('True if the caller agreed, false if they declined.'),\n }),\n execute: async ({ item, granted }, { agent }) => {\n await options.onGrant({ item, granted, resourceId: agent?.resourceId, threadId: agent?.threadId });\n return { recorded: true as const, item, granted };\n },\n });\n}\n","// Server-safe: only depends on `@mastra/core` + zod, never the `@livekit/agents` runtime — so it\n// can be added to agents defined in server/shared code and re-exported from the root entry\n// (`index.ts`). `index.test.ts` enforces that boundary.\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/** Details of the agent's decision to end the call, handed to {@link EndCallToolOptions.onEndCall}. */\nexport interface EndCallRequest {\n /** Short reason the agent gave for ending the call, if any (e.g. \"caller said goodbye\"). */\n reason?: string;\n /** The caller the call belongs to (the memory `resource`), when the call is memory-scoped. */\n resourceId?: string;\n /** The call thread, when the call is memory-scoped. */\n threadId?: string;\n}\n\nexport interface EndCallToolOptions {\n /**\n * Tool id the agent calls to end the call. Must match the worker's\n * `configuration.endCall.tool` (both default to `'endCall'`), since that's the name the worker\n * watches for to hang up.\n */\n id?: string;\n /** Override the description the model sees when deciding to call the tool. */\n description?: string;\n /**\n * Optional bookkeeping hook, called when the agent invokes the tool (records the reason, marks\n * the call resolved in your CRM, …). Runs inside the turn — keep it quick. It does NOT hang up\n * the call; the worker does that once the agent's final words finish playing. The tool reads the\n * caller's identity from its execution context and passes it here.\n */\n onEndCall?: (request: EndCallRequest) => void | Promise<void>;\n}\n\nconst DEFAULT_DESCRIPTION =\n 'End the phone call. Call this only after you have said goodbye and there is nothing left to do — ' +\n 'for example the caller says goodbye, or the task is complete and you have wrapped up. Say your ' +\n 'closing line first, then call this as your final action; the call hangs up once your words finish ' +\n 'playing. Your closing line must be a final statement, never a question — if you are asking the ' +\n 'caller anything (for example whether they need anything else), do NOT call this in that reply; ' +\n 'wait for their answer. Do not call it if the caller still needs something.';\n\n/**\n * Builds a Mastra tool the agent calls to end the call itself (say goodbye → hang up). It is the\n * agent-visible half of agent-initiated hang-up: the tool only SIGNALS intent (and runs optional\n * bookkeeping) — it can't reach the LiveKit room from inside `agent.stream()`. The worker owns the\n * hang-up: with `configuration.endCall` set, it watches each turn for this tool, waits for the\n * agent's closing words to finish playing, then disconnects (running `onCallEnd` on the way out).\n * Keeping the tool inside the single agent means it stays visible/editable in Studio and works on\n * the agent and workflow reply paths alike.\n *\n * ```ts\n * // agent tools\n * endCall: createEndCallTool({\n * onEndCall: ({ reason, resourceId }) => log.info('agent ended call', { reason, resourceId }),\n * }),\n * // worker\n * createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } });\n * ```\n */\nexport function createEndCallTool(options: EndCallToolOptions = {}) {\n return createTool({\n id: options.id ?? 'endCall',\n description: options.description ?? DEFAULT_DESCRIPTION,\n inputSchema: z.object({\n reason: z\n .string()\n .nullish()\n .describe('Short reason the call is ending, e.g. \"caller said goodbye\" or \"task complete\".'),\n }),\n outputSchema: z.object({ ended: z.boolean() }),\n execute: async ({ reason }, { agent }) => {\n try {\n await options.onEndCall?.({\n reason: reason ?? undefined,\n resourceId: agent?.resourceId,\n threadId: agent?.threadId,\n });\n } catch (error) {\n // Bookkeeping is best-effort — the tool must still signal `{ ended: true }` so the worker's\n // end-call detector runs and the call actually hangs up.\n console.warn('@mastra/livekit: onEndCall hook threw', error);\n }\n return { ended: true as const };\n },\n });\n}\n"],"mappings":"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"}
|
package/dist/llm-plugin.d.ts
CHANGED
|
@@ -23,9 +23,10 @@ export interface MastraLLMOptions {
|
|
|
23
23
|
* only messages new since the agent last spoke are sent each turn and Mastra Memory supplies
|
|
24
24
|
* history. When omitted/false, the full LiveKit chat context is sent every turn.
|
|
25
25
|
*
|
|
26
|
-
* NOTE: incompatible with the session's `preemptiveGeneration` option — a speculative turn
|
|
27
|
-
*
|
|
28
|
-
* using `memory
|
|
26
|
+
* NOTE: incompatible with the session's `preemptiveGeneration` option — a speculative turn
|
|
27
|
+
* persists a partial user message and a partial reply before being discarded. Leave preemptive
|
|
28
|
+
* generation off when using `memory`, or set `memory.options.readOnly` and persist committed
|
|
29
|
+
* turns yourself from the session's conversation-item events.
|
|
29
30
|
*
|
|
30
31
|
* The plugin cannot detect the combination at runtime, so this stays a documented constraint
|
|
31
32
|
* rather than a warning: the LLM interface never receives the session (so the option can't be
|
package/dist/llm-plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llm-plugin.d.ts","sourceRoot":"","sources":["../src/llm-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EAGtB,MAAM,UAAU,CAAC;AAGlB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD,YAAY,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,oEAAoE;IACpE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAE/B
|
|
1
|
+
{"version":3,"file":"llm-plugin.d.ts","sourceRoot":"","sources":["../src/llm-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,GAAG,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EAGtB,MAAM,UAAU,CAAC;AAGlB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD,YAAY,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,oEAAoE;IACpE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAE/B;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,sBAAsB,GAAG,KAAK,CAAC;IACxC,4EAA4E;IAC5E,cAAc,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1D,qGAAqG;IACrG,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACtE,qGAAqG;IACrG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,mGAAmG;IACnG,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAiCD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,SAAU,SAAQ,GAAG,CAAC,GAAG;;IAapC,YAAY,OAAO,EAAE,gBAAgB,EAqCpC;IAED,KAAK,IAAI,MAAM,CAEd;IAED,IAAa,KAAK,IAAI,MAAM,CAE3B;IAED,IAAa,QAAQ,IAAI,MAAM,CAE9B;IAED;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAUf,IAAI,CAAC,EACZ,OAAO,EACP,OAAO,EACP,WAAyC,GAC1C,EAAE;QACD,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC;QACzB,OAAO,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC;QAC1B,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,UAAU,CAAC,EAAE,GAAG,CAAC,UAAU,CAAC;QAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACvC,GAAG,GAAG,CAAC,SAAS,CAoBhB;IAED,+FAA+F;IACtF,OAAO,IAAI,IAAI,CAAG;CAC5B;AAED,UAAU,sBAAsB;IAC9B,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC;IACzB,OAAO,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC;IAC1B,WAAW,EAAE,iBAAiB,CAAC;IAC/B,SAAS,EAAE,mBAAmB,CAAC;IAC/B,MAAM,EAAE,sBAAsB,GAAG,KAAK,CAAC;IACvC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;;;;;GAKG;AACH,cAAM,eAAgB,SAAQ,GAAG,CAAC,SAAS;;IAKzC,YAAY,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,sBAAsB,EAKhE;IAED,UAAgB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA+CnC;CACF;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/plugin-entry.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_remote = require("./remote-
|
|
2
|
+
const require_remote = require("./remote-DlCC6aCn.cjs");
|
|
3
3
|
let _livekit_agents = require("@livekit/agents");
|
|
4
4
|
let _mastra_core_request_context = require("@mastra/core/request-context");
|
|
5
5
|
//#region src/llm-plugin.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-entry.cjs","names":["RequestContext","llm","#model","#memory","#requestContext","#staticGenerator","#remoteOptions","createAgentReplyGenerator","createRemoteAgentReplyGenerator","DEFAULT_API_CONNECT_OPTIONS","#warnedToolCtx","#generator","chatContextToMessages","extractNewTurnMessages"],"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 that\n * completes before being discarded pollutes the thread. Leave preemptive generation off when\n * using `memory`.\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.cjs","names":["RequestContext","llm","#model","#memory","#requestContext","#staticGenerator","#remoteOptions","createAgentReplyGenerator","createRemoteAgentReplyGenerator","DEFAULT_API_CONNECT_OPTIONS","#warnedToolCtx","#generator","chatContextToMessages","extractNewTurnMessages"],"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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"}
|
package/dist/plugin-entry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as chatContextToMessages, o as extractNewTurnMessages, r as createAgentReplyGenerator, t as createRemoteAgentReplyGenerator } from "./remote-
|
|
1
|
+
import { a as chatContextToMessages, o as extractNewTurnMessages, r as createAgentReplyGenerator, t as createRemoteAgentReplyGenerator } from "./remote-bPj_0lAr.js";
|
|
2
2
|
import { DEFAULT_API_CONNECT_OPTIONS, llm } from "@livekit/agents";
|
|
3
3
|
import { RequestContext } from "@mastra/core/request-context";
|
|
4
4
|
//#region src/llm-plugin.ts
|
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 that\n * completes before being discarded pollutes the thread. Leave preemptive generation off when\n * using `memory`.\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","#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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"}
|
|
@@ -472,7 +472,8 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
472
472
|
messages: ctx.messages,
|
|
473
473
|
memory: ctx.memory ? {
|
|
474
474
|
thread: ctx.memory.thread,
|
|
475
|
-
resource: ctx.memory.resource ?? ctx.memory.thread
|
|
475
|
+
resource: ctx.memory.resource ?? ctx.memory.thread,
|
|
476
|
+
...ctx.memory.options ? { options: ctx.memory.options } : {}
|
|
476
477
|
} : void 0,
|
|
477
478
|
requestContext: serializeRequestContext(ctx.requestContext),
|
|
478
479
|
...extraBody
|
|
@@ -661,4 +662,4 @@ Object.defineProperty(exports, "extractNewTurnMessages", {
|
|
|
661
662
|
}
|
|
662
663
|
});
|
|
663
664
|
|
|
664
|
-
//# sourceMappingURL=remote-
|
|
665
|
+
//# sourceMappingURL=remote-DlCC6aCn.cjs.map
|