{"version":3,"sources":["../../src/voice/agent_session_handoff.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { describe, expect, it, vi } from 'vitest';\nimport { AgentHandoffItem, ChatContext } from '../llm/chat_context.js';\nimport { Agent } from './agent.js';\nimport { AgentActivity, type ReusableResources } from './agent_activity.js';\nimport { AgentSession } from './agent_session.js';\n\nfunction createFakeLock() {\n  return {\n    lock: vi.fn(async () => () => {}),\n  };\n}\n\nfunction createFakeSession() {\n  return {\n    activityLock: createFakeLock(),\n    rootSpanContext: undefined,\n    agent: undefined,\n    activity: undefined,\n    nextActivity: undefined,\n    _globalRunState: undefined,\n    _chatCtx: ChatContext.empty(),\n    _conversationItemAdded: vi.fn(),\n    emit: vi.fn(),\n    logger: {\n      debug: vi.fn(),\n      warn: vi.fn(),\n    },\n    sessionOptions: {\n      turnHandling: {\n        interruption: {\n          enabled: true,\n          minDuration: 0,\n          minWords: 0,\n        },\n        endpointing: {\n          minDelay: 0,\n          maxDelay: 0,\n        },\n      },\n    },\n    interruptionDetection: undefined,\n    turnDetection: undefined,\n    vad: undefined,\n    stt: undefined,\n    llm: undefined,\n    tts: undefined,\n    useTtsAlignedTranscript: false,\n    _input: {},\n  } as unknown as AgentSession;\n}\n\ndescribe('AgentSession reusable resources handoff', () => {\n  it('passes reusable resources from drain into the next resumed activity', async () => {\n    const resources: ReusableResources = {\n      sttPipeline: { close: vi.fn(async () => {}) } as any,\n    };\n    const previousAgent = new Agent({ instructions: 'old' });\n    const nextAgent = new Agent({ instructions: 'new' });\n    const previousActivity = {\n      agent: previousAgent,\n      blockNewTurns: vi.fn(),\n      drain: vi.fn(async () => resources),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => resources),\n    };\n    const nextActivity = {\n      agent: nextAgent,\n      resume: vi.fn(async () => {}),\n      start: vi.fn(async () => {}),\n      attachAudioInput: vi.fn(),\n      _onEnterTask: undefined,\n    };\n    nextAgent._agentActivity = nextActivity as any;\n\n    const session = createFakeSession();\n    (session as any).activity = previousActivity as any;\n\n    await AgentSession.prototype._updateActivity.call(session, nextAgent, {\n      newActivity: 'resume',\n      waitOnEnter: false,\n    });\n\n    expect(previousActivity.drain).toHaveBeenCalledWith({ newActivity: nextActivity });\n    expect(nextActivity.resume).toHaveBeenCalledWith({ reuseResources: resources });\n  });\n\n  it('cleans up reusable resources if the next activity fails to start', async () => {\n    const closeFn = vi.fn(async () => {});\n    const resources: ReusableResources = {\n      sttPipeline: { close: closeFn } as any,\n    };\n    const previousAgent = new Agent({ instructions: 'old' });\n    const nextAgent = new Agent({ instructions: 'new' });\n    const previousActivity = {\n      agent: previousAgent,\n      blockNewTurns: vi.fn(),\n      drain: vi.fn(async () => resources),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => resources),\n    };\n    const nextActivity = {\n      agent: nextAgent,\n      resume: vi.fn(async () => {\n        throw new Error('resume failed');\n      }),\n      start: vi.fn(async () => {}),\n      attachAudioInput: vi.fn(),\n      _onEnterTask: undefined,\n    };\n    nextAgent._agentActivity = nextActivity as any;\n\n    const session = createFakeSession();\n    (session as any).activity = previousActivity as any;\n\n    await expect(\n      AgentSession.prototype._updateActivity.call(session, nextAgent, {\n        newActivity: 'resume',\n        waitOnEnter: false,\n      }),\n    ).rejects.toThrow('resume failed');\n\n    expect(closeFn).toHaveBeenCalledTimes(1);\n  });\n\n  it('does not cleanup reusable resources after the next activity starts successfully', async () => {\n    const closeFn = vi.fn(async () => {});\n    const resources: ReusableResources = {\n      sttPipeline: { close: closeFn } as any,\n    };\n    const previousAgent = new Agent({ instructions: 'old' });\n    const nextAgent = new Agent({ instructions: 'new' });\n    const previousActivity = {\n      agent: previousAgent,\n      blockNewTurns: vi.fn(),\n      drain: vi.fn(async () => resources),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => resources),\n    };\n    const nextActivity = {\n      agent: nextAgent,\n      resume: vi.fn(async () => {}),\n      start: vi.fn(async () => {}),\n      attachAudioInput: vi.fn(() => {\n        throw new Error('attach failed');\n      }),\n      _onEnterTask: undefined,\n    };\n    nextAgent._agentActivity = nextActivity as any;\n\n    const session = createFakeSession();\n    (session as any).activity = previousActivity as any;\n    (session as any)._input = { audio: { stream: {} } } as any;\n\n    await expect(\n      AgentSession.prototype._updateActivity.call(session, nextAgent, {\n        newActivity: 'resume',\n        waitOnEnter: false,\n      }),\n    ).rejects.toThrow('attach failed');\n\n    expect(nextActivity.resume).toHaveBeenCalledWith({ reuseResources: resources });\n    // pipeline was already transferred, so cleanup should NOT have been called\n    expect(closeFn).not.toHaveBeenCalled();\n  });\n\n  it('skips detach when the same activity object is reused', async () => {\n    const agent = new Agent({ instructions: 'same' });\n    const activity = {\n      agent,\n      drain: vi.fn(async () => undefined),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => undefined),\n      resume: vi.fn(async () => {}),\n      start: vi.fn(async () => {}),\n      attachAudioInput: vi.fn(),\n      _onEnterTask: undefined,\n    };\n    agent._agentActivity = activity as any;\n\n    const session = createFakeSession();\n    (session as any).activity = activity as any;\n\n    await AgentSession.prototype._updateActivity.call(session, agent, {\n      newActivity: 'resume',\n      waitOnEnter: false,\n    });\n\n    expect(activity.drain).not.toHaveBeenCalled();\n    expect(activity.pause).not.toHaveBeenCalled();\n    expect(activity.resume).toHaveBeenCalledWith({ reuseResources: undefined });\n  });\n\n  it('emits ConversationItemAdded with an AgentHandoffItem on handoff', async () => {\n    const previousAgent = new Agent({ instructions: 'old' });\n    const nextAgent = new Agent({ instructions: 'new' });\n    const previousActivity = {\n      agent: previousAgent,\n      blockNewTurns: vi.fn(),\n      drain: vi.fn(async () => undefined),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => undefined),\n    };\n    const nextActivity = {\n      agent: nextAgent,\n      resume: vi.fn(async () => {}),\n      start: vi.fn(async () => {}),\n      attachAudioInput: vi.fn(),\n      _onEnterTask: undefined,\n    };\n    nextAgent._agentActivity = nextActivity as any;\n\n    const session = createFakeSession();\n    (session as any).activity = previousActivity as any;\n\n    await AgentSession.prototype._updateActivity.call(session, nextAgent, {\n      newActivity: 'resume',\n      waitOnEnter: false,\n    });\n\n    expect((session as any)._conversationItemAdded).toHaveBeenCalledOnce();\n    const item = (session as any)._conversationItemAdded.mock.calls[0][0];\n    expect(item).toBeInstanceOf(AgentHandoffItem);\n    expect(item.oldAgentId).toBe(previousAgent.id);\n    expect(item.newAgentId).toBe(nextAgent.id);\n  });\n\n  it('skips starting a new activity while the session is closing', async () => {\n    const previousAgent = new Agent({ instructions: 'old' });\n    const nextAgent = new Agent({ instructions: 'new' });\n    const previousActivity = {\n      agent: previousAgent,\n      blockNewTurns: vi.fn(),\n      drain: vi.fn(async () => undefined),\n      close: vi.fn(async () => {}),\n      pause: vi.fn(async () => undefined),\n    };\n\n    const startSpy = vi.spyOn(AgentActivity.prototype, 'start').mockResolvedValue(undefined);\n\n    try {\n      const session = createFakeSession();\n      (session as any).activity = previousActivity as any;\n      (session as any).closing = true;\n\n      await AgentSession.prototype._updateActivity.call(session, nextAgent, {\n        newActivity: 'start',\n        waitOnEnter: false,\n      });\n\n      expect(previousActivity.drain).not.toHaveBeenCalled();\n      expect(previousActivity.close).not.toHaveBeenCalled();\n      expect(startSpy).not.toHaveBeenCalled();\n      expect((session as any).activity).toBe(previousActivity);\n      expect((session as any).nextActivity).toBeUndefined();\n    } finally {\n      startSpy.mockRestore();\n    }\n  });\n});\n"],"mappings":"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","names":[]}