import { ReadableStream } from 'node:stream/web'; import { FlushSentinel, llm, voice } from '@livekit/agents'; import type { Agent as MastraAgent, AgentExecutionOptionsBase, AgentMemoryOption } from '@mastra/core/agent'; import type { TracingContext } from '@mastra/core/observability'; import { RequestContext } from '@mastra/core/request-context'; import type { VoiceTurnMessage } from './messages.js'; import { createGenerationMetrics } from './turn-metrics.js'; import type { VoiceReplyChunk, VoiceTurnMetricsHook } from './turn-metrics.js'; /** Default spoken text for periodic AI re-disclosure. See {@link MastraVoiceAgentOptions.greetingReminder}. */ export declare const DEFAULT_DISCLOSURE_REMINDER = "Just a reminder, you're speaking with an AI assistant."; /** * Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once * `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`. * Time is injectable so the interval logic is deterministically testable. */ export declare class DisclosureReminder { private readonly everyMs; private readonly text; private lastAt; constructor(everyMs: number, text: string, now?: number); /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock — * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */ due(now?: number): string | undefined; /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */ markDelivered(now?: number): void; } /** * Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so * TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the * wrapper cancels `source` — so barge-in still aborts the underlying generation. */ export declare function prependText(source: ReadableStream, text: string): ReadableStream; export type MastraStreamOptions = Partial>; export interface VoiceToolCall { toolCallId: string; toolName: string; args?: unknown; } /** * Token usage for one turn, captured from the model's `finish` chunk. Field names mirror LiveKit's * `CompletionUsage` so the plugin can forward it into `metrics_collected` without remapping. */ export interface VoiceTurnUsage { /** Tokens in the prompt (LiveKit `promptTokens`). */ promptTokens: number; /** Tokens in the completion (LiveKit `completionTokens`). */ completionTokens: number; /** Cached prompt tokens (LiveKit `promptCachedTokens`). */ promptCachedTokens: number; /** Total tokens for the turn. */ totalTokens: number; } /** * Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the * LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts. * Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape * (`inputTokens.total`/`outputTokens.total`). */ export declare function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined; export interface MastraVoiceAgentMemory { thread: string; resource?: string; /** * Per-call Mastra memory config forwarded to the agent as `memory.options` (see core's * `AgentMemoryOption`). The notable use is `{ readOnly: true }`: the agent still reads thread * history but persists nothing, which keeps LiveKit's preemptive (speculative) turns from writing * partial user and assistant messages to the thread. With `readOnly` the caller owns persistence * of committed turns (e.g. from `onTurnComplete` via `memory.saveMessages`). */ options?: AgentMemoryOption['options']; } /** * Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per * detected user turn; the bridge builds this context and asks the generator for the reply. */ export interface VoiceTurnContext { /** Stable user-turn identifier, supplied by the worker or plugin. */ turnId?: string; /** Unique generation attempt identifier, including speculative replies and retries. */ attemptId?: string; /** @internal Bridge-side timing collector. */ metrics?: ReturnType; /** * The messages to generate a reply from. With Mastra Memory on, only the messages new since * the agent last spoke (history comes from the thread); with memory off, the full session. * * For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(..., * { memory })` inside a step so the agent backfills history from the thread (no duplication). A * stateless workflow that wants the entire transcript every turn should read `chatCtx` instead * (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from. */ messages: VoiceTurnMessage[]; /** The raw LiveKit chat context, for generators that want the full transcript or message parts. */ chatCtx: llm.ChatContext; /** Resolved memory mapping for the call, or `false` when memory is disabled. */ memory: MastraVoiceAgentMemory | false; /** Request context forwarded to generation. */ requestContext?: RequestContext; /** Voice-call span context, so each turn's generation nests under the call trace. */ tracingContext?: TracingContext; /** * Internal, per-turn side channel for token usage. A generator invokes this once, when the * `finish` chunk carries usage, so the caller (e.g. `MastraLLMStream`) can attribute usage to * exactly this turn — kept on the context (not on generator options) so overlapping turns from * preemptive generation can't misattribute usage. Fire-and-forget; the generator does not await it. */ onUsage?: (usage: VoiceTurnUsage) => void; } /** * What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes. */ export interface VoiceTurnResult { /** The assistant reply text streamed this turn, accumulated from the model's text deltas. */ text: string; /** Tool calls the agent made during the turn, in order. */ toolCalls: VoiceToolCall[]; /** True when barge-in cut the turn short before it finished streaming. */ interrupted: boolean; /** Token usage for the turn when the model reported it in its `finish` chunk. */ usage?: VoiceTurnUsage; } /** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */ export interface VoiceTurnCompleteContext extends VoiceTurnContext { /** The reply the agent produced this turn. */ result: VoiceTurnResult; } /** * Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio * path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory * maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A * thrown error or rejected promise is logged, not propagated. Because the resolved `memory` * mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking * `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}. */ export type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise; /** * Produces a stream of text deltas for one conversational turn, or `null` to stay silent. * Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying * generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and * `createWorkflowReplyGenerator` (a Mastra workflow). */ export type VoiceReplyGenerator = (ctx: VoiceTurnContext) => ReadableStream | null | Promise | null>; export interface AgentReplyGeneratorOptions { /** The Mastra agent that generates replies. Tools and memory run inside this agent. */ agent: MastraAgent; /** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */ streamOptions?: MastraStreamOptions; /** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */ toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void; /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */ onToolCall?: (toolCall: VoiceToolCall) => void; /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */ onTurnComplete?: VoiceTurnCompleteHook; } /** * A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model, * tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled, * which aborts the in-flight `agent.stream()`. */ export declare function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator; export interface MastraVoiceAgentOptions { /** Receives per-attempt generation metrics. Playback metrics come from observeVoiceSession. */ onTurnMetrics?: VoiceTurnMetricsHook; /** * The Mastra agent that generates replies. Tools and memory run inside this agent. Provide * either this or {@link MastraVoiceAgentOptions.generate}. */ agent?: MastraAgent; /** * A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of * `agent` to drive replies with a workflow or any custom generator. */ generate?: VoiceReplyGenerator; /** * Conversation persistence. When set, only messages new since the agent last spoke are * sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit * in-session context is sent on every turn instead. */ memory?: MastraVoiceAgentMemory | false; /** Request context entries forwarded to generation. */ requestContext?: RequestContext | Record; /** * Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. "Let * me look that up.") to speak it while the tool runs; it also appears in the transcript. Return * nothing to stay silent. Applies to the agent generator built here; the workflow generator * takes its own equivalent via `createWorkflowReplyGenerator`. */ toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void; /** * Called as each tool call starts mid-reply (before the tool result is known), the building block * for tool-driven side effects — analytics, agent-initiated hang-up — without waiting for the turn * to finish. Runs synchronously on the stream; keep it cheap and non-throwing. Applies to the agent * generator built here; the workflow generator surfaces tool calls via `onTurnComplete` instead. */ onToolCall?: (toolCall: VoiceToolCall) => void; /** * Called once per turn after the reply has finished streaming to text-to-speech. Runs off the * audio path and fire-and-forget — the turn does not await it — so post-turn memory * maintenance, CRM writes, or analytics never delay the caller or the next turn. The context * carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so * this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error * or rejected promise is logged, not propagated. Applies to the agent generator built here; the * workflow generator takes its own via `createWorkflowReplyGenerator`. */ onTurnComplete?: VoiceTurnCompleteHook; /** * Periodic AI re-disclosure. When set, once `everyMs` has elapsed since the last disclosure the * NEXT turn's reply is prefixed with `text` (spoken at the turn boundary, never mid-turn), so long * calls keep re-disclosing the AI status. Applies to the agent and workflow/custom generators. The * worker derives this from `configuration.greeting.repeatEvery` / `repeatText`; `text` defaults to * {@link DEFAULT_DISCLOSURE_REMINDER}. */ greetingReminder?: { everyMs: number; text?: string; }; /** Extra options merged into every `agent.stream()` call (agent generator only). */ streamOptions?: MastraStreamOptions; /** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */ instructions?: string; id?: voice.AgentOptions['id']; stt?: voice.AgentOptions['stt']; vad?: voice.AgentOptions['vad']; tts?: voice.AgentOptions['tts']; turnHandling?: voice.AgentOptions['turnHandling']; } /** * A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow. * * LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls * `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator} * which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts * the in-flight generation. */ export declare class MastraVoiceAgent extends voice.Agent { readonly mastraAgent?: MastraAgent; readonly memory: MastraVoiceAgentMemory | false; readonly requestContext?: RequestContext; readonly streamOptions?: MastraStreamOptions; private readonly replyGenerator; private readonly onTurnMetrics?; private readonly reminder?; constructor(options: MastraVoiceAgentOptions); llmNode(chatCtx: llm.ChatContext, _toolCtx: llm.ToolContext, _modelSettings: voice.ModelSettings): Promise | null>; } export declare function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent; /** @internal Wrap every reply source, including custom generators, with one terminal metric. */ export declare function instrumentVoiceReply(generator: VoiceReplyGenerator, ctx: VoiceTurnContext, onTurnMetrics?: VoiceTurnMetricsHook): Promise<{ reply: ReadableStream | null; identity: { turnId: string; attemptId: string; }; }>; //# sourceMappingURL=bridge.d.ts.map