mastra-ai/mastra · error

@mastra/livekit: MastraVoiceAgent requires `agent` or `gener

Error message

@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.

What it means

MastraVoiceAgent accepts two mutually exclusive reply sources: `agent` (a Mastra agent instance) or `generate` (a custom reply generator function). The constructor throws this error when both are provided, because the bridge cannot decide which one owns reply generation. Exactly one must be supplied.

Source

Thrown at integrations/livekit/src/bridge.ts:439

/**
 * 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 class MastraVoiceAgent extends voice.Agent {
  readonly mastraAgent?: MastraAgent;
  readonly memory: MastraVoiceAgentMemory | false;
  readonly requestContext?: RequestContext;
  readonly streamOptions?: MastraStreamOptions;
  private readonly replyGenerator: VoiceReplyGenerator;
  private readonly reminder?: DisclosureReminder;

  constructor(options: MastraVoiceAgentOptions) {
    if (options.agent && options.generate) {
      throw new Error(
        '@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.',
      );
    }
    super({
      id: options.id,
      instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,
      stt: options.stt,
      vad: options.vad,
      llm: new MastraPlaceholderLLM(),
      tts: options.tts,
      turnHandling: options.turnHandling,
    });
    this.memory = options.memory ?? false;
    this.requestContext = toRequestContext(options.requestContext);
    this.streamOptions = options.streamOptions;
    if (options.greetingReminder) {
      this.reminder = new DisclosureReminder(
        options.greetingReminder.everyMs,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Remove one of the two: keep `agent` for Mastra-agent-driven replies, or keep `generate` for custom replies.
  2. If migrating from agent to generate, delete the `agent` property from the options object.
  3. Audit config-building code (object spreads, defaults) so only one reply source is ever set.

Example fix

// before
new MastraVoiceAgent({ agent: myAgent, generate: myFn })
// after
new MastraVoiceAgent({ agent: myAgent }) // or { generate: myFn }, never both
Defensive patterns

Strategy: validation

Validate before calling

function assertValidVoiceAgentOptions(o: MastraVoiceAgentOptions): void {
  if ('agent' in o && o.agent && 'generate' in o && o.generate) {
    throw new Error('MastraVoiceAgent accepts `agent` or `generate`, not both');
  }
}

Type guard

function hasExactlyOneReplySource(o: MastraVoiceAgentOptions): boolean {
  return Boolean(o.agent) !== Boolean(o.generate); // XOR: exactly one present
}

Prevention

When it happens

Trigger: new MastraVoiceAgent({ agent, generate, ... }) where both keys are present — typically when merging config objects or adding a custom generate override on top of an existing agent-based config.

Common situations: Spreading defaults ({ ...baseOptions }) that already contain `agent` while adding `generate`; refactoring from agent-based to generate-based replies without removing the old key; conditional config building that accumulates both fields.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/fc58c979a9821c6a. Report an issue: GitHub.