{"version":3,"sources":["../../src/voice/agent_activity_interrupted_commit.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { AudioFrame } from '@livekit/rtc-node';\nimport { ReadableStream } from 'node:stream/web';\nimport { describe, expect, it, vi } from 'vitest';\nimport { initializeLogger } from '../log.js';\nimport { Future } from '../utils.js';\nimport { Agent } from './agent.js';\nimport { AgentSession } from './agent_session.js';\nimport { AgentSessionEventTypes, type ConversationItemAddedEvent } from './events.js';\nimport { AudioOutput } from './io.js';\nimport { FakeLLM } from './testing/fake_llm.js';\n\nfunction frame(durationMs = 20, sampleRate = 24000): AudioFrame {\n  const samples = Math.floor((sampleRate * durationMs) / 1000);\n  return new AudioFrame(new Int16Array(samples), sampleRate, 1, samples);\n}\n\n// Reports the first frame as played, then waits for interruption to finish playout.\nclass InterruptibleOutput extends AudioOutput {\n  onFirstFrame?: () => void;\n  synchronizedTranscript?: string;\n  private started = false;\n  constructor() {\n    super(24000);\n  }\n  async captureFrame(f: AudioFrame): Promise<void> {\n    await super.captureFrame(f);\n    if (!this.started) {\n      this.started = true;\n      this.onPlaybackStarted(Date.now());\n      this.onFirstFrame?.();\n    }\n  }\n  flush(): void {\n    super.flush();\n  }\n  clearBuffer(): void {\n    this.onPlaybackFinished({\n      playbackPosition: 0.02,\n      interrupted: true,\n      synchronizedTranscript: this.synchronizedTranscript,\n    });\n  }\n}\n\nclass SayTimestampOutput extends AudioOutput {\n  onFirstFrame?: () => void;\n  private firstSegment = true;\n\n  constructor() {\n    super(24000);\n  }\n\n  async captureFrame(f: AudioFrame): Promise<void> {\n    const previousSegments = this.capturedPlayoutSegments;\n    await super.captureFrame(f);\n    if (this.capturedPlayoutSegments === previousSegments) return;\n\n    this.onPlaybackStarted(Date.now());\n    if (this.firstSegment) {\n      this.onFirstFrame?.();\n    }\n  }\n\n  flush(): void {\n    super.flush();\n    if (!this.firstSegment) {\n      this.onPlaybackFinished({ playbackPosition: 0.2, interrupted: false });\n    }\n  }\n\n  clearBuffer(): void {\n    this.firstSegment = false;\n    this.onPlaybackFinished({ playbackPosition: 0.02, interrupted: true });\n  }\n}\n\n// Audio sink that mimics a DataStream avatar output (waitPlaybackStart: true):\n// frames are accepted faster than real time and playback-started is only\n// reported LATER, via an out-of-band notification (the `lk.playback_started`\n// RPC from the remote avatar worker) — typically after forwarding has already\n// completed. `notifyPlaybackStarted` simulates that RPC arriving.\nclass DeferredStartOutput extends AudioOutput {\n  onFirstFrameCaptured?: () => void;\n  playbackPosition = 0;\n  private captured = false;\n  constructor() {\n    super(24000);\n  }\n  async captureFrame(f: AudioFrame): Promise<void> {\n    await super.captureFrame(f);\n    if (!this.captured) {\n      this.captured = true;\n      this.onFirstFrameCaptured?.();\n    }\n  }\n  // The remote avatar reports playback start well after frames were forwarded.\n  notifyPlaybackStarted(): void {\n    this.onPlaybackStarted(Date.now());\n  }\n  flush(): void {\n    super.flush();\n  }\n  clearBuffer(): void {\n    this.onPlaybackFinished({ playbackPosition: this.playbackPosition, interrupted: true });\n  }\n}\n\n// Mimics ParticipantAudioOutput's pause gate (room_io/_output.ts): a frame\n// forwarded while the output is paused awaits the gate and BAILS on interruption\n// before `super.captureFrame` counts the segment (#1662). `forwardAudio` sets\n// `startedForwardingAt` before awaiting captureFrame, so \"forwarding started\"\n// must not count as playback evidence — nothing of this segment was captured.\n// `clearBuffer` releases gated frames without reporting a playback event (like\n// the real output when no segment is pending), leaving the PREVIOUS segment's\n// lastPlaybackEvent as what waitForPlayout returns.\nclass PausedGateOutput extends AudioOutput {\n  onGateBlocked?: () => void;\n  private gateFut: Future | null = null;\n  private interruptedFut = new Future();\n  constructor() {\n    super(24000);\n  }\n  pause(): void {\n    this.gateFut = new Future();\n  }\n  async captureFrame(f: AudioFrame): Promise<void> {\n    if (this.gateFut && !this.gateFut.done) {\n      this.onGateBlocked?.();\n      // Neither future is ever rejected in this test; catch satisfies throws-check.\n      await Promise.race([this.gateFut.await, this.interruptedFut.await]).catch(() => {});\n      if (this.interruptedFut.done) {\n        return;\n      }\n    }\n    await super.captureFrame(f);\n  }\n  flush(): void {\n    super.flush();\n  }\n  clearBuffer(): void {\n    this.interruptedFut.resolve();\n  }\n}\n\nclass BlockingInterruptedOutput extends AudioOutput {\n  onFirstFrameCaptured?: () => void;\n  clearBufferCalled = new Future<void>();\n  maxPlaybackStartedListeners = 0;\n  private firstSegmentFinished = false;\n\n  constructor() {\n    super(24000);\n  }\n\n  async captureFrame(f: AudioFrame): Promise<void> {\n    await super.captureFrame(f);\n    this.maxPlaybackStartedListeners = Math.max(\n      this.maxPlaybackStartedListeners,\n      this.listenerCount(AudioOutput.EVENT_PLAYBACK_STARTED),\n    );\n    if (this.capturedPlayoutSegments === 1) {\n      this.onFirstFrameCaptured?.();\n    } else {\n      this.onPlaybackStarted(Date.now());\n    }\n  }\n\n  flush(): void {\n    super.flush();\n    if (this.capturedPlayoutSegments > 1) {\n      this.onPlaybackFinished({ playbackPosition: 0.02, interrupted: false });\n    }\n  }\n\n  clearBuffer(): void {\n    this.clearBufferCalled.resolve();\n  }\n\n  finishInterruptedSegment(): void {\n    if (!this.firstSegmentFinished) {\n      this.firstSegmentFinished = true;\n      // DataStreamAudioReceiver serializes its RPC queue, so a delayed\n      // PLAYBACK_STARTED for this segment is delivered before PLAYBACK_FINISHED.\n      this.onPlaybackStarted(Date.now());\n      this.onPlaybackFinished({ playbackPosition: 0, interrupted: true });\n    }\n  }\n}\n\n// Agent that synthesizes a few real frames for whatever text it receives, so the\n// audio path produces a first frame (no TTS provider needed).\nclass FrameAgent extends Agent {\n  constructor() {\n    super({ instructions: 'test' });\n  }\n  async ttsNode(): Promise<ReadableStream<AudioFrame> | null> {\n    return new ReadableStream<AudioFrame>({\n      start(controller) {\n        for (let i = 0; i < 10; i++) controller.enqueue(frame());\n        controller.close();\n      },\n    });\n  }\n}\n\nclass SayOnEnterAgent extends FrameAgent {\n  async onEnter(): Promise<void> {\n    const audio = new ReadableStream<AudioFrame>({\n      start(controller) {\n        for (let i = 0; i < 10; i++) controller.enqueue(frame());\n        controller.close();\n      },\n    });\n    this.session.say('I saw your application for a Honda', { audio });\n  }\n}\n\n// Agent whose TTS produces no frames and stays open long enough that an\n// interruption (anchored to ttsNode being invoked, fired well before the\n// close) always lands mid-forwarding with zero audio captured for the segment.\nclass NoFrameAgent extends Agent {\n  onTtsStarted?: () => void;\n  constructor() {\n    super({ instructions: 'test' });\n  }\n  async ttsNode(): Promise<ReadableStream<AudioFrame> | null> {\n    this.onTtsStarted?.();\n    return new ReadableStream<AudioFrame>({\n      start(controller) {\n        setTimeout(() => controller.close(), 500);\n      },\n    });\n  }\n}\n\ndescribe('AgentActivity interrupted-speech commit', () => {\n  initializeLogger({ pretty: false, level: 'silent' });\n\n  it('timestamps an interrupted say transcript at speech start', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'Who is this?', content: 'This is John.' }]),\n    });\n    const audioOut = new SayTimestampOutput();\n    session.output.audio = audioOut;\n    let reply: ReturnType<AgentSession['generateReply']> | undefined;\n    audioOut.onFirstFrame = () => {\n      setTimeout(() => {\n        session.interrupt();\n        reply = session.generateReply({ userInput: 'Who is this?' });\n      }, 10);\n    };\n\n    const agent = new SayOnEnterAgent();\n    await session.start({ agent });\n    try {\n      await vi.waitFor(() => expect(reply).toBeDefined());\n      await reply!.waitForPlayout();\n\n      const messages = agent.chatCtx.items.filter((item) => item.type === 'message');\n      expect(messages.map((message) => message.role)).toEqual([\n        'system',\n        'assistant',\n        'user',\n        'assistant',\n      ]);\n      expect(messages[1]!.interrupted).toBe(true);\n      const startedSpeakingAt = messages[1]!.metrics?.startedSpeakingAt;\n      expect(startedSpeakingAt).toBeDefined();\n      expect(messages[1]!.createdAt).toBe(startedSpeakingAt! * 1_000);\n      expect(messages[2]!.textContent).toBe('Who is this?');\n      expect(messages[3]!.textContent).toBe('This is John.');\n    } finally {\n      await session.close();\n    }\n  });\n\n  it('commits an interrupted reply to chat ctx when no synchronized transcript is available', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A fairly long spoken reply.' }]),\n    });\n    const audioOut = new InterruptibleOutput();\n    session.output.audio = audioOut;\n    // Interrupt the reply as soon as the first audio frame is played out.\n    audioOut.onFirstFrame = () => session.interrupt({ force: true });\n\n    const assistantMessages: { content: string; interrupted: boolean }[] = [];\n    session.on(AgentSessionEventTypes.ConversationItemAdded, (ev: ConversationItemAddedEvent) => {\n      if (ev.item.type === 'message' && ev.item.role === 'assistant') {\n        assistantMessages.push({\n          content: ev.item.textContent ?? '',\n          interrupted: !!ev.item.interrupted,\n        });\n      }\n    });\n\n    await session.start({ agent: new FrameAgent() });\n    try {\n      const handle = session.generateReply({ userInput: 'hello' });\n      await handle.waitForPlayout();\n      await vi.waitFor(() => expect(assistantMessages.length).toBeGreaterThan(0));\n\n      // The interrupted-but-heard reply must reach chat ctx (pre-fix it was\n      // dropped because the partial branch returned `synchronizedTranscript ?? ''`,\n      // committing an empty string and skipping the message).\n      const msg = assistantMessages[0]!;\n      expect(msg.interrupted).toBe(true);\n      expect(msg.content.length).toBeGreaterThan(0);\n      expect('A fairly long spoken reply.'.startsWith(msg.content)).toBe(true);\n    } finally {\n      await session.close();\n    }\n  });\n\n  it.each([\n    { name: 'empty', transcript: '', expected: [] },\n    { name: 'partial', transcript: 'A fairly', expected: ['A fairly'] },\n  ])('uses the $name synchronized transcript on interruption', async ({ transcript, expected }) => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A fairly long spoken reply.' }]),\n    });\n    const audioOut = new InterruptibleOutput();\n    audioOut.synchronizedTranscript = transcript;\n    audioOut.onFirstFrame = () => session.interrupt({ force: true });\n    session.output.audio = audioOut;\n    const agent = new FrameAgent();\n\n    await session.start({ agent });\n    try {\n      await session.generateReply({ userInput: 'hello' }).waitForPlayout();\n      await session.close();\n\n      for (const chatCtx of [agent.chatCtx, session.history]) {\n        const assistantMessages = chatCtx.items\n          .filter((item) => item.type === 'message' && item.role === 'assistant')\n          .map((item) => item.textContent);\n        expect(assistantMessages).toEqual(expected);\n      }\n    } finally {\n      await session.close();\n    }\n  });\n\n  it('commits an interrupted reply when playback start is reported after forwarding completes (avatar output)', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A fairly long spoken reply.' }]),\n    });\n    const audioOut = new DeferredStartOutput();\n    session.output.audio = audioOut;\n\n    // Simulate the avatar pipeline: playback starts (RPC arrives) shortly after\n    // the frames were forwarded, then the user interrupts mid-playback.\n    audioOut.onFirstFrameCaptured = () => {\n      setTimeout(() => {\n        audioOut.playbackPosition = 0.12;\n        audioOut.notifyPlaybackStarted();\n        setTimeout(() => session.interrupt({ force: true }), 50);\n      }, 100);\n    };\n\n    const assistantMessages: { content: string; interrupted: boolean }[] = [];\n    session.on(AgentSessionEventTypes.ConversationItemAdded, (ev: ConversationItemAddedEvent) => {\n      if (ev.item.type === 'message' && ev.item.role === 'assistant') {\n        assistantMessages.push({\n          content: ev.item.textContent ?? '',\n          interrupted: !!ev.item.interrupted,\n        });\n      }\n    });\n\n    await session.start({ agent: new FrameAgent() });\n    try {\n      const handle = session.generateReply({ userInput: 'hello' });\n      await handle.waitForPlayout();\n      await vi.waitFor(() => expect(assistantMessages.length).toBeGreaterThan(0));\n\n      // Pre-fix, forwardAudio rejected firstFrameFut the moment forwarding\n      // finished (before the deferred playback-started notification), so the\n      // audibly-played reply was classified \"skipped\" and silently dropped from\n      // the chat context (phantom utterance).\n      const msg = assistantMessages[0]!;\n      expect(msg.interrupted).toBe(true);\n      expect(msg.content.length).toBeGreaterThan(0);\n      expect('A fairly long spoken reply.'.startsWith(msg.content)).toBe(true);\n    } finally {\n      await session.close();\n    }\n  });\n\n  it('commits an interrupted reply when only a playback position is reported (no playback-started signal)', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A fairly long spoken reply.' }]),\n    });\n    const audioOut = new DeferredStartOutput();\n    session.output.audio = audioOut;\n\n    // The remote avatar never reports playback-started (or the RPC races with\n    // the interruption), but its playback-finished response carries a non-zero\n    // position — proof the user heard part of the reply.\n    audioOut.onFirstFrameCaptured = () => {\n      setTimeout(() => {\n        audioOut.playbackPosition = 0.25;\n        session.interrupt({ force: true });\n      }, 100);\n    };\n\n    const assistantMessages: { content: string; interrupted: boolean }[] = [];\n    session.on(AgentSessionEventTypes.ConversationItemAdded, (ev: ConversationItemAddedEvent) => {\n      if (ev.item.type === 'message' && ev.item.role === 'assistant') {\n        assistantMessages.push({\n          content: ev.item.textContent ?? '',\n          interrupted: !!ev.item.interrupted,\n        });\n      }\n    });\n\n    await session.start({ agent: new FrameAgent() });\n    try {\n      const handle = session.generateReply({ userInput: 'hello' });\n      await handle.waitForPlayout();\n      await vi.waitFor(() => expect(assistantMessages.length).toBeGreaterThan(0));\n\n      const msg = assistantMessages[0]!;\n      expect(msg.interrupted).toBe(true);\n      expect(msg.content.length).toBeGreaterThan(0);\n      expect('A fairly long spoken reply.'.startsWith(msg.content)).toBe(true);\n    } finally {\n      await session.close();\n    }\n  });\n\n  it('does not commit an interrupted reply that forwarded no audio when a stale playback position exists', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A reply that was never spoken.' }]),\n    });\n    const audioOut = new DeferredStartOutput();\n    session.output.audio = audioOut;\n\n    // A previously played segment leaves a non-zero lastPlaybackEvent behind.\n    // waitForPlayout returns it immediately for a segment that captured no\n    // frames — the playback position must not be attributed to that segment.\n    await audioOut.captureFrame(frame());\n    audioOut.flush();\n    audioOut.onPlaybackFinished({ playbackPosition: 0.5, interrupted: false });\n\n    const assistantMessages: { content: string; interrupted: boolean }[] = [];\n    session.on(AgentSessionEventTypes.ConversationItemAdded, (ev: ConversationItemAddedEvent) => {\n      if (ev.item.type === 'message' && ev.item.role === 'assistant') {\n        assistantMessages.push({\n          content: ev.item.textContent ?? '',\n          interrupted: !!ev.item.interrupted,\n        });\n      }\n    });\n\n    const agent = new NoFrameAgent();\n    // Anchor the interruption to TTS starting, so it always lands while the\n    // segment is mid-forwarding (zero frames captured, stream still open).\n    agent.onTtsStarted = () => setTimeout(() => session.interrupt({ force: true }), 150);\n\n    await session.start({ agent });\n    try {\n      const handle = session.generateReply({ userInput: 'hello' });\n      await handle.waitForPlayout();\n\n      // Close drains the reply task fully — the interrupted-commit block (if\n      // it were to run) has fired by the time close resolves. Nothing was\n      // heard: the reply must not be committed on the strength of the stale\n      // playback position.\n      await session.close();\n      expect(assistantMessages).toHaveLength(0);\n    } finally {\n      await session.close();\n    }\n  });\n\n  it('does not commit a reply whose first frame bailed at a pause gate (stale playback position)', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([{ input: 'hello', content: 'A reply that was never spoken.' }]),\n    });\n    const audioOut = new PausedGateOutput();\n    session.output.audio = audioOut;\n\n    // A previously played segment leaves a non-zero lastPlaybackEvent behind.\n    await audioOut.captureFrame(frame());\n    audioOut.flush();\n    audioOut.onPlaybackFinished({ playbackPosition: 0.5, interrupted: false });\n\n    // The output is paused (false-interruption pause during the thinking state):\n    // the reply's first frame will block at the gate, never reaching\n    // super.captureFrame — so the segment count is never bumped and waitForPlayout\n    // returns the stale event above. Forwarding DID start (`startedForwardingAt`\n    // is set), which is exactly why forwarding-started must not gate the commit.\n    audioOut.pause();\n    // A genuine interruption arrives while the frame is blocked at the gate.\n    audioOut.onGateBlocked = () => session.interrupt({ force: true });\n\n    const assistantMessages: { content: string; interrupted: boolean }[] = [];\n    session.on(AgentSessionEventTypes.ConversationItemAdded, (ev: ConversationItemAddedEvent) => {\n      if (ev.item.type === 'message' && ev.item.role === 'assistant') {\n        assistantMessages.push({\n          content: ev.item.textContent ?? '',\n          interrupted: !!ev.item.interrupted,\n        });\n      }\n    });\n\n    await session.start({ agent: new FrameAgent() });\n    try {\n      const handle = session.generateReply({ userInput: 'hello' });\n      await handle.waitForPlayout();\n\n      // Nothing of this reply was captured, let alone played: the stale 0.5s\n      // position from the previous segment must not commit it as 'partial'.\n      await session.close();\n      expect(assistantMessages).toHaveLength(0);\n    } finally {\n      await session.close();\n    }\n  }, 15_000);\n\n  it('finishes interrupted generation cleanup before authorizing the next reply', async () => {\n    const session = new AgentSession({\n      llm: new FakeLLM([\n        { input: 'first', content: 'First reply.' },\n        { input: 'second', content: 'Second reply.' },\n      ]),\n    });\n    const audioOut = new BlockingInterruptedOutput();\n    const agent = new FrameAgent();\n    session.output.audio = audioOut;\n    let second: ReturnType<AgentSession['generateReply']> | undefined;\n    audioOut.onFirstFrameCaptured = () => {\n      session.interrupt();\n      second = session.generateReply({ userInput: 'second' });\n    };\n\n    await session.start({ agent });\n    try {\n      const first = session.generateReply({ userInput: 'first' });\n\n      await audioOut.clearBufferCalled.await;\n      await new Promise<void>((resolve) => setTimeout(resolve, 50));\n\n      // The old segment's PLAYBACK_STARTED listener must be settled and detached\n      // before the shared output accepts frames for the next reply.\n      expect(audioOut.capturedPlayoutSegments).toBe(1);\n\n      audioOut.finishInterruptedSegment();\n      await first.waitForPlayout();\n      if (!second) {\n        throw new Error('second reply was not scheduled');\n      }\n      await second.waitForPlayout();\n      expect(audioOut.capturedPlayoutSegments).toBe(2);\n      expect(audioOut.maxPlaybackStartedListeners).toBe(1);\n    } finally {\n      audioOut.finishInterruptedSegment();\n      await session.close();\n    }\n  });\n});\n"],"mappings":"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","names":[]}