From abde27d1a9624989f0c18f0899974bf88bb554c0 Mon Sep 17 00:00:00 2001 From: Brian Yin Date: Wed, 13 May 2026 16:36:29 -0700 Subject: [PATCH 1/5] changes verified --- agents/src/job.test.ts | 69 +++++++++ agents/src/job.ts | 19 ++- agents/src/voice/agent_session.test.ts | 53 +++++++ agents/src/voice/agent_session.ts | 12 +- agents/src/voice/room_io/room_io.test.ts | 184 ++++++++++++++++++++++- agents/src/voice/room_io/room_io.ts | 53 ++++++- 6 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 agents/src/job.test.ts create mode 100644 agents/src/voice/agent_session.test.ts diff --git a/agents/src/job.test.ts b/agents/src/job.test.ts new file mode 100644 index 000000000..e953cebe6 --- /dev/null +++ b/agents/src/job.test.ts @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { Room } from '@livekit/rtc-node'; +import { describe, expect, it, vi } from 'vitest'; +import type { InferenceExecutor } from './ipc/inference_executor.js'; +import { JobContext, type JobProcess, type RunningJobInfo } from './job.js'; + +const { deleteRoomMock, roomServiceClientMock } = vi.hoisted(() => ({ + deleteRoomMock: vi.fn(async () => {}), + roomServiceClientMock: vi.fn(function RoomServiceClient() { + return { deleteRoom: deleteRoomMock }; + }), +})); + +vi.mock('livekit-server-sdk', () => ({ + RoomServiceClient: roomServiceClientMock, +})); + +function createJobContext() { + const room = { + name: 'connected-room', + on: vi.fn(), + off: vi.fn(), + isConnected: false, + remoteParticipants: new Map(), + }; + + return new JobContext( + {} as unknown as JobProcess, + { + acceptArguments: { + name: 'agent', + identity: 'agent', + metadata: '', + }, + job: { + id: 'job-id', + room: { name: 'assigned-room' }, + }, + url: 'wss://example.livekit.cloud', + token: 'token', + workerId: 'worker-id', + } as unknown as RunningJobInfo, + room as unknown as Room, + vi.fn(), + vi.fn(), + {} as unknown as InferenceExecutor, + ); +} + +describe('JobContext.deleteRoom', () => { + it('deletes the connected room by default using the job URL', async () => { + const ctx = createJobContext(); + + await ctx.deleteRoom(); + + expect(roomServiceClientMock).toHaveBeenCalledWith('wss://example.livekit.cloud'); + expect(deleteRoomMock).toHaveBeenCalledWith('connected-room'); + }); + + it('deletes the provided room name when specified', async () => { + const ctx = createJobContext(); + + await ctx.deleteRoom('other-room'); + + expect(deleteRoomMock).toHaveBeenCalledWith('other-room'); + }); +}); diff --git a/agents/src/job.ts b/agents/src/job.ts index ec7b6859e..1cf11024f 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -11,6 +11,7 @@ import type { } from '@livekit/rtc-node'; import { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node'; import { ThrowsPromise } from '@livekit/throws-transformer/throws'; +import { RoomServiceClient } from 'livekit-server-sdk'; import { AsyncLocalStorage } from 'node:async_hooks'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -29,7 +30,7 @@ const jobContextStorage = new AsyncLocalStorage>(); * Returns the current job context. * * @param required - If true (default), throws when no context is found. If false, returns undefined. - * @throws {Error} if no job context is found and required is true + * @throws Error if no job context is found and required is true */ export function getJobContext>( required?: true, @@ -272,6 +273,22 @@ export class JobContext> { this.connected = true; } + /** Deletes the room and disconnects all participants. */ + async deleteRoom(roomName?: string): Promise { + const targetRoomName = roomName ?? this.#room.name; + if (!targetRoomName) { + this.#logger.warn('cannot delete room because room name is missing'); + return; + } + + try { + const client = new RoomServiceClient(this.#info.url); + await client.deleteRoom(targetRoomName); + } catch (error) { + this.#logger.warn({ error, roomName: targetRoomName }, 'error while deleting room'); + } + } + makeSessionReport(session?: AgentSession): SessionReport { const targetSession = session || this._primaryAgentSession; diff --git a/agents/src/voice/agent_session.test.ts b/agents/src/voice/agent_session.test.ts new file mode 100644 index 000000000..5f16276fd --- /dev/null +++ b/agents/src/voice/agent_session.test.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { describe, expect, it, vi } from 'vitest'; +import { AgentSession } from './agent_session.js'; +import { AgentSessionEventTypes, CloseReason } from './events.js'; + +type CloseImplInner = (reason: CloseReason, error: null, drain: boolean) => Promise; + +describe('AgentSession close lifecycle', () => { + it('emits close before closing RoomIO so RoomIO close handlers can run', async () => { + let closeEmitted = false; + const roomIO = { + close: vi.fn(async () => { + expect(closeEmitted).toBe(true); + }), + }; + const session = { + started: true, + closing: false, + activity: undefined, + _recorderIO: undefined, + input: { audio: null }, + output: { audio: null, transcription: null }, + sessionHost: undefined, + _roomIO: roomIO, + sessionSpan: undefined, + _userSpeakingSpan: undefined, + agentSpeakingSpan: undefined, + logger: { info: vi.fn() }, + llmErrorCounts: 0, + ttsErrorCounts: 0, + _userState: 'listening', + _agentState: 'listening', + rootSpanContext: undefined, + _cancelUserAwayTimer: vi.fn(), + _onAecWarmupExpired: vi.fn(), + off: vi.fn(), + emit: vi.fn((event: AgentSessionEventTypes) => { + if (event === AgentSessionEventTypes.Close) { + closeEmitted = true; + } + }), + }; + + const closeImplInner = (AgentSession.prototype as unknown as { closeImplInner: CloseImplInner }) + .closeImplInner; + await closeImplInner.call(session, CloseReason.USER_INITIATED, null, false); + + expect(roomIO.close).toHaveBeenCalledTimes(1); + expect(closeEmitted).toBe(true); + }); +}); diff --git a/agents/src/voice/agent_session.ts b/agents/src/voice/agent_session.ts index d5382562d..06f1843f3 100644 --- a/agents/src/voice/agent_session.ts +++ b/agents/src/voice/agent_session.ts @@ -1325,12 +1325,6 @@ export class AgentSession< this.output.audio = null; this.output.transcription = null; - await this.sessionHost?.close(); - this.sessionHost = undefined; - - await this._roomIO?.close(); - this._roomIO = undefined; - await this.activity?.close(); this.activity = undefined; @@ -1359,6 +1353,12 @@ export class AgentSession< this.llmErrorCounts = 0; this.ttsErrorCounts = 0; + await this.sessionHost?.close(); + this.sessionHost = undefined; + + await this._roomIO?.close(); + this._roomIO = undefined; + this.logger.info({ reason, error }, 'AgentSession closed'); } } diff --git a/agents/src/voice/room_io/room_io.test.ts b/agents/src/voice/room_io/room_io.test.ts index b0a27bf97..594879972 100644 --- a/agents/src/voice/room_io/room_io.test.ts +++ b/agents/src/voice/room_io/room_io.test.ts @@ -1,8 +1,15 @@ // SPDX-FileCopyrightText: 2026 LiveKit, Inc. // // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it } from 'vitest'; +import { EventEmitter } from 'node:events'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as jobModule from '../../job.js'; import { IdentityTransform } from '../../stream/identity_transform.js'; +import { DEFAULT_API_CONNECT_OPTIONS } from '../../types.js'; +import { AgentSessionEventTypes, CloseReason, createCloseEvent } from '../events.js'; +import { RoomIO } from './room_io.js'; + +type RoomIOArgs = ConstructorParameters[0]; /** * Regression tests proving WritableStream.close() rejects when the writer is @@ -36,3 +43,178 @@ describe('RoomIO WritableStream close guard', () => { await expect(writer.close()).rejects.toThrow(); }); }); + +function createFakeRoom() { + const emitter = new EventEmitter(); + + return { + name: 'test-room', + isConnected: false, + remoteParticipants: new Map(), + localParticipant: { identity: 'agent' }, + on: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => { + emitter.on(event, listener); + return emitter; + }), + off: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => { + emitter.off(event, listener); + return emitter; + }), + registerTextStreamHandler: vi.fn(), + unregisterTextStreamHandler: vi.fn(), + }; +} + +function createFakeSession() { + const emitter = new EventEmitter(); + + return { + input: { audio: null }, + output: { audio: null, transcription: null }, + currentAgent: undefined, + llm: undefined, + on: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => { + emitter.on(event, listener); + return emitter; + }), + off: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => { + emitter.off(event, listener); + return emitter; + }), + emit: (event: string | symbol, value: unknown) => emitter.emit(event, value), + _closeSoon: vi.fn(), + }; +} + +describe('RoomIO deleteRoomOnClose', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + it('does not delete the room by default when the session closes', async () => { + const deleteRoom = vi.fn(async () => {}); + vi.spyOn(jobModule, 'getJobContext').mockReturnValue({ + deleteRoom, + } as unknown as ReturnType); + const room = createFakeRoom(); + const session = createFakeSession(); + const roomIO = new RoomIO({ + agentSession: session as unknown as RoomIOArgs['agentSession'], + room: room as unknown as RoomIOArgs['room'], + inputOptions: { + audioEnabled: false, + textEnabled: false, + }, + outputOptions: { + audioEnabled: false, + transcriptionEnabled: false, + }, + }); + + roomIO.start(); + session.emit(AgentSessionEventTypes.Close, createCloseEvent(CloseReason.USER_INITIATED, null)); + await roomIO.close(); + + expect(deleteRoom).not.toHaveBeenCalled(); + }); + + it('deletes the room once when deleteRoomOnClose is enabled and the session closes', async () => { + const deleteRoom = vi.fn(async () => {}); + vi.spyOn(jobModule, 'getJobContext').mockReturnValue({ + deleteRoom, + } as unknown as ReturnType); + const room = createFakeRoom(); + const session = createFakeSession(); + const roomIO = new RoomIO({ + agentSession: session as unknown as RoomIOArgs['agentSession'], + room: room as unknown as RoomIOArgs['room'], + inputOptions: { + audioEnabled: false, + textEnabled: false, + deleteRoomOnClose: true, + }, + outputOptions: { + audioEnabled: false, + transcriptionEnabled: false, + }, + }); + + roomIO.start(); + session.emit(AgentSessionEventTypes.Close, createCloseEvent(CloseReason.USER_INITIATED, null)); + session.emit(AgentSessionEventTypes.Close, createCloseEvent(CloseReason.USER_INITIATED, null)); + await roomIO.close(); + + expect(deleteRoom).toHaveBeenCalledTimes(1); + expect(deleteRoom).toHaveBeenCalledWith(room.name); + }); + + it('uses the job context captured at construction when close runs outside job context', async () => { + const deleteRoom = vi.fn(async () => {}); + vi.spyOn(jobModule, 'getJobContext') + .mockReturnValueOnce({ + deleteRoom, + } as unknown as ReturnType) + .mockReturnValue(undefined); + const room = createFakeRoom(); + const session = createFakeSession(); + const roomIO = new RoomIO({ + agentSession: session as unknown as RoomIOArgs['agentSession'], + room: room as unknown as RoomIOArgs['room'], + inputOptions: { + audioEnabled: false, + textEnabled: false, + deleteRoomOnClose: true, + }, + outputOptions: { + audioEnabled: false, + transcriptionEnabled: false, + }, + }); + + roomIO.start(); + session.emit(AgentSessionEventTypes.Close, createCloseEvent(CloseReason.USER_INITIATED, null)); + await roomIO.close(); + + expect(deleteRoom).toHaveBeenCalledTimes(1); + expect(deleteRoom).toHaveBeenCalledWith(room.name); + }); + + it('waits up to the API timeout for an in-flight room deletion during close', async () => { + vi.useFakeTimers(); + const deleteRoom = vi.fn(() => new Promise(() => {})); + vi.spyOn(jobModule, 'getJobContext').mockReturnValue({ + deleteRoom, + } as unknown as ReturnType); + const room = createFakeRoom(); + const session = createFakeSession(); + const roomIO = new RoomIO({ + agentSession: session as unknown as RoomIOArgs['agentSession'], + room: room as unknown as RoomIOArgs['room'], + inputOptions: { + audioEnabled: false, + textEnabled: false, + deleteRoomOnClose: true, + }, + outputOptions: { + audioEnabled: false, + transcriptionEnabled: false, + }, + }); + + roomIO.start(); + session.emit(AgentSessionEventTypes.Close, createCloseEvent(CloseReason.USER_INITIATED, null)); + + let closed = false; + const closePromise = roomIO.close().then(() => { + closed = true; + }); + + await vi.advanceTimersByTimeAsync(DEFAULT_API_CONNECT_OPTIONS.timeoutMs - 1); + expect(closed).toBe(false); + + await vi.advanceTimersByTimeAsync(1); + await closePromise; + expect(closed).toBe(true); + }); +}); diff --git a/agents/src/voice/room_io/room_io.ts b/agents/src/voice/room_io/room_io.ts index b19bf6fa0..da1cdabff 100644 --- a/agents/src/voice/room_io/room_io.ts +++ b/agents/src/voice/room_io/room_io.ts @@ -18,10 +18,12 @@ import { } from '@livekit/rtc-node'; import type { WritableStreamDefaultWriter } from 'node:stream/web'; import { ATTRIBUTE_PUBLISH_ON_BEHALF, TOPIC_CHAT } from '../../constants.js'; +import { getJobContext, type JobContext } from '../../job.js'; import { RealtimeModel } from '../../llm/index.js'; import { log } from '../../log.js'; import { IdentityTransform } from '../../stream/identity_transform.js'; -import { Future, Task, waitForAbort } from '../../utils.js'; +import { DEFAULT_API_CONNECT_OPTIONS } from '../../types.js'; +import { Future, IdleTimeoutError, Task, waitForAbort, waitUntilTimeout } from '../../utils.js'; import { type AgentSession } from '../agent_session.js'; import { AgentSessionEventTypes, @@ -84,6 +86,8 @@ export interface RoomInputOptions { CLIENT_INITIATED, ROOM_DELETED, or USER_REJECTED. */ closeOnDisconnect: boolean; + /** Delete the room when the AgentSession is closed. Default to false. */ + deleteRoomOnClose: boolean; } export interface RoomOutputOptions { @@ -122,6 +126,7 @@ const DEFAULT_ROOM_INPUT_OPTIONS: RoomInputOptions = { videoEnabled: false, textInputCallback: DEFAULT_TEXT_INPUT_CALLBACK, closeOnDisconnect: true, + deleteRoomOnClose: false, }; const DEFAULT_ROOM_OUTPUT_OPTIONS: RoomOutputOptions = { @@ -156,6 +161,8 @@ export class RoomIO { private userTranscriptWriter: WritableStreamDefaultWriter; private forwardUserTranscriptTask?: Task; private initTask?: Task; + private deleteRoomTask?: Task; + private jobContext?: JobContext; private logger = log(); @@ -177,6 +184,7 @@ export class RoomIO { this.inputOptions = { ...DEFAULT_ROOM_INPUT_OPTIONS, ...inputOptions }; this.outputOptions = { ...DEFAULT_ROOM_OUTPUT_OPTIONS, ...outputOptions }; this.userTranscriptWriter = this.userTranscriptStream.writable.getWriter(); + this.jobContext = getJobContext(false); this.participantIdentity = participant ? typeof participant === 'string' @@ -292,6 +300,36 @@ export class RoomIO { this.transcriptionSynchronizer.enabled = !nativeTranscriptSync; }; + private onAgentSessionClose = () => { + if (!this.inputOptions.deleteRoomOnClose || this.deleteRoomTask) { + return; + } + + let jobContext = this.jobContext; + if (!jobContext) { + jobContext = getJobContext(false); + } + if (!jobContext) { + return; + } + this.logger.info( + { room: this.room.name }, + 'deleting room on agent session close ' + + '(disable via `RoomInputOptions.deleteRoomOnClose=false`)', + ); + + const deleteRoomTask = Task.from(async () => { + await jobContext.deleteRoom(this.room.name); + }); + + this.deleteRoomTask = deleteRoomTask; + deleteRoomTask.addDoneCallback(() => { + if (this.deleteRoomTask === deleteRoomTask) { + this.deleteRoomTask = undefined; + } + }); + }; + private onAgentStateChanged = async (ev: AgentStateChangedEvent) => { if (this.room.isConnected && this.room.localParticipant) { await this.room.localParticipant.setAttributes({ @@ -546,6 +584,7 @@ export class RoomIO { this.agentSession.on(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged); this.agentSession.on(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed); + this.agentSession.on(AgentSessionEventTypes.Close, this.onAgentSessionClose); this.agentSession.on( AgentSessionEventTypes.ConversationItemAdded, this.onConversationItemAdded, @@ -558,6 +597,7 @@ export class RoomIO { this.room.off(RoomEvent.ParticipantDisconnected, this.onParticipantDisconnected); this.agentSession.off(AgentSessionEventTypes.UserInputTranscribed, this.onUserInputTranscribed); this.agentSession.off(AgentSessionEventTypes.AgentStateChanged, this.onAgentStateChanged); + this.agentSession.off(AgentSessionEventTypes.Close, this.onAgentSessionClose); this.agentSession.off( AgentSessionEventTypes.ConversationItemAdded, this.onConversationItemAdded, @@ -583,5 +623,16 @@ export class RoomIO { await this.audioInput?.close(); await this.participantAudioOutput?.close(); await this.transcriptionSynchronizer?.close(); + + if (this.deleteRoomTask) { + try { + await waitUntilTimeout(this.deleteRoomTask.result, DEFAULT_API_CONNECT_OPTIONS.timeoutMs); + } catch (error) { + if (!(error instanceof IdleTimeoutError)) { + throw error; + } + this.logger.warn({ room: this.room.name }, 'automatic room deletion timed out'); + } + } } } From bf736998c9ca33df08401966ee9f7d5b14790270 Mon Sep 17 00:00:00 2001 From: Brian Yin Date: Wed, 13 May 2026 16:36:58 -0700 Subject: [PATCH 2/5] Create proud-cooks-pump.md --- .changeset/proud-cooks-pump.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/proud-cooks-pump.md diff --git a/.changeset/proud-cooks-pump.md b/.changeset/proud-cooks-pump.md new file mode 100644 index 000000000..85dbd731b --- /dev/null +++ b/.changeset/proud-cooks-pump.md @@ -0,0 +1,5 @@ +--- +"@livekit/agents": patch +--- + +brianyin/agt-2866-delete-room-on-session-close From bd0e5732c189628dec7ecd6fdec5dc106c0e7dd5 Mon Sep 17 00:00:00 2001 From: Brian Yin Date: Wed, 13 May 2026 16:40:32 -0700 Subject: [PATCH 3/5] lint --- agents/src/voice/room_io/room_io.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agents/src/voice/room_io/room_io.ts b/agents/src/voice/room_io/room_io.ts index da1cdabff..fd6541344 100644 --- a/agents/src/voice/room_io/room_io.ts +++ b/agents/src/voice/room_io/room_io.ts @@ -18,7 +18,7 @@ import { } from '@livekit/rtc-node'; import type { WritableStreamDefaultWriter } from 'node:stream/web'; import { ATTRIBUTE_PUBLISH_ON_BEHALF, TOPIC_CHAT } from '../../constants.js'; -import { getJobContext, type JobContext } from '../../job.js'; +import { type JobContext, getJobContext } from '../../job.js'; import { RealtimeModel } from '../../llm/index.js'; import { log } from '../../log.js'; import { IdentityTransform } from '../../stream/identity_transform.js'; From 355a74c4f0936201e0d0fcce6a36d842151e1510 Mon Sep 17 00:00:00 2001 From: Brian Yin Date: Wed, 13 May 2026 16:41:51 -0700 Subject: [PATCH 4/5] Delete agent_session.test.ts --- agents/src/voice/agent_session.test.ts | 53 -------------------------- 1 file changed, 53 deletions(-) delete mode 100644 agents/src/voice/agent_session.test.ts diff --git a/agents/src/voice/agent_session.test.ts b/agents/src/voice/agent_session.test.ts deleted file mode 100644 index 5f16276fd..000000000 --- a/agents/src/voice/agent_session.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2026 LiveKit, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it, vi } from 'vitest'; -import { AgentSession } from './agent_session.js'; -import { AgentSessionEventTypes, CloseReason } from './events.js'; - -type CloseImplInner = (reason: CloseReason, error: null, drain: boolean) => Promise; - -describe('AgentSession close lifecycle', () => { - it('emits close before closing RoomIO so RoomIO close handlers can run', async () => { - let closeEmitted = false; - const roomIO = { - close: vi.fn(async () => { - expect(closeEmitted).toBe(true); - }), - }; - const session = { - started: true, - closing: false, - activity: undefined, - _recorderIO: undefined, - input: { audio: null }, - output: { audio: null, transcription: null }, - sessionHost: undefined, - _roomIO: roomIO, - sessionSpan: undefined, - _userSpeakingSpan: undefined, - agentSpeakingSpan: undefined, - logger: { info: vi.fn() }, - llmErrorCounts: 0, - ttsErrorCounts: 0, - _userState: 'listening', - _agentState: 'listening', - rootSpanContext: undefined, - _cancelUserAwayTimer: vi.fn(), - _onAecWarmupExpired: vi.fn(), - off: vi.fn(), - emit: vi.fn((event: AgentSessionEventTypes) => { - if (event === AgentSessionEventTypes.Close) { - closeEmitted = true; - } - }), - }; - - const closeImplInner = (AgentSession.prototype as unknown as { closeImplInner: CloseImplInner }) - .closeImplInner; - await closeImplInner.call(session, CloseReason.USER_INITIATED, null, false); - - expect(roomIO.close).toHaveBeenCalledTimes(1); - expect(closeEmitted).toBe(true); - }); -}); From 392e5aceb3701c8a1c56243859c8007977136d3b Mon Sep 17 00:00:00 2001 From: Brian Yin Date: Wed, 13 May 2026 16:57:38 -0700 Subject: [PATCH 5/5] pass api info --- agents/src/job.test.ts | 27 ++++++++++++++++++++++++--- agents/src/job.ts | 5 ++++- agents/src/worker.ts | 2 ++ examples/src/basic_agent.ts | 1 + 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/agents/src/job.test.ts b/agents/src/job.test.ts index e953cebe6..89af75815 100644 --- a/agents/src/job.test.ts +++ b/agents/src/job.test.ts @@ -17,7 +17,7 @@ vi.mock('livekit-server-sdk', () => ({ RoomServiceClient: roomServiceClientMock, })); -function createJobContext() { +function createJobContext(infoOverrides: Partial = {}) { const room = { name: 'connected-room', on: vi.fn(), @@ -41,6 +41,7 @@ function createJobContext() { url: 'wss://example.livekit.cloud', token: 'token', workerId: 'worker-id', + ...infoOverrides, } as unknown as RunningJobInfo, room as unknown as Room, vi.fn(), @@ -50,12 +51,32 @@ function createJobContext() { } describe('JobContext.deleteRoom', () => { - it('deletes the connected room by default using the job URL', async () => { + it('deletes the connected room by default using the job URL and credentials', async () => { + const ctx = createJobContext({ + apiKey: 'api-key', + apiSecret: 'api-secret', + }); + + await ctx.deleteRoom(); + + expect(roomServiceClientMock).toHaveBeenCalledWith( + 'wss://example.livekit.cloud', + 'api-key', + 'api-secret', + ); + expect(deleteRoomMock).toHaveBeenCalledWith('connected-room'); + }); + + it('falls back to environment credentials when job credentials are absent', async () => { const ctx = createJobContext(); await ctx.deleteRoom(); - expect(roomServiceClientMock).toHaveBeenCalledWith('wss://example.livekit.cloud'); + expect(roomServiceClientMock).toHaveBeenCalledWith( + 'wss://example.livekit.cloud', + undefined, + undefined, + ); expect(deleteRoomMock).toHaveBeenCalledWith('connected-room'); }); diff --git a/agents/src/job.ts b/agents/src/job.ts index 1cf11024f..01d6459c6 100644 --- a/agents/src/job.ts +++ b/agents/src/job.ts @@ -85,6 +85,8 @@ export type RunningJobInfo = { url: string; token: string; workerId: string; + apiKey?: string; + apiSecret?: string; }; /** Attempted to add a function callback, but the function already exists. */ @@ -282,8 +284,9 @@ export class JobContext> { } try { - const client = new RoomServiceClient(this.#info.url); + const client = new RoomServiceClient(this.#info.url, this.#info.apiKey, this.#info.apiSecret); await client.deleteRoom(targetRoomName); + this.#logger.info({ roomName: targetRoomName }, 'room deleted'); } catch (error) { this.#logger.warn({ error, roomName: targetRoomName }, 'error while deleting room'); } diff --git a/agents/src/worker.ts b/agents/src/worker.ts index 7f3a1427b..b966e885d 100644 --- a/agents/src/worker.ts +++ b/agents/src/worker.ts @@ -772,6 +772,8 @@ export class AgentServer { url: asgn.url || this.#opts.wsURL, token: asgn.token, workerId: this.id, + apiKey: this.#opts.apiKey, + apiSecret: this.#opts.apiSecret, }); } catch (e) { this.#logger.child({ requestId: req.id }).error(e, 'error launching job'); diff --git a/examples/src/basic_agent.ts b/examples/src/basic_agent.ts index 2f0768d5e..645a204d5 100644 --- a/examples/src/basic_agent.ts +++ b/examples/src/basic_agent.ts @@ -118,6 +118,7 @@ export default defineAgent({ agent, room: ctx.room, inputOptions: { + deleteRoomOnClose: true, noiseCancellation: BackgroundVoiceCancellation(), }, });