From a4a25e78822e5599eeba54b6ad720b2d4870deb6 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 05:15:01 +0000 Subject: [PATCH] feat(voice/avatar): add waitForJoin helper --- .changeset/avatar-wait-cleanup.md | 5 +++ agents/src/voice/avatar/avatar_session.ts | 45 +++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .changeset/avatar-wait-cleanup.md diff --git a/.changeset/avatar-wait-cleanup.md b/.changeset/avatar-wait-cleanup.md new file mode 100644 index 000000000..d44610a65 --- /dev/null +++ b/.changeset/avatar-wait-cleanup.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents': patch +--- + +feat(voice/avatar): add avatar join waiting and cleanup participant on close diff --git a/agents/src/voice/avatar/avatar_session.ts b/agents/src/voice/avatar/avatar_session.ts index 9a0a8c1b9..a2ad4bef1 100644 --- a/agents/src/voice/avatar/avatar_session.ts +++ b/agents/src/voice/avatar/avatar_session.ts @@ -4,6 +4,7 @@ import type { Room } from '@livekit/rtc-node'; import { RoomEvent, TrackKind } from '@livekit/rtc-node'; import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter'; +import { RoomServiceClient } from 'livekit-server-sdk'; import { EventEmitter } from 'node:events'; import { getJobContext } from '../../job.js'; import { log } from '../../log.js'; @@ -88,11 +89,55 @@ export class AvatarSession extends (EventEmitter as new () => TypedEmitter { + if (!this.#waitAvatarJoinPromise) return; + if (timeout === null) { + await this.#waitAvatarJoinPromise; + return; + } + + let timer: ReturnType | undefined; + await Promise.race([ + this.#waitAvatarJoinPromise, + new Promise((_, reject) => { + timer = setTimeout( + () => reject(new Error('timed out waiting for avatar participant')), + timeout, + ); + }), + ]).finally(() => clearTimeout(timer)); + } + /** * Release any resources owned by this avatar session. Default implementation is a no-op; * subclasses can override to perform cleanup. */ async aclose(): Promise { + const roomName = this.#room?.name; + if (this.#room?.isConnected && roomName !== undefined) { + const jobCtx = getJobContext(false); + if (jobCtx !== undefined) { + try { + const client = new RoomServiceClient( + jobCtx.info.url, + jobCtx.info.apiKey, + jobCtx.info.apiSecret, + ); + await client.removeParticipant(roomName, this.avatarIdentity); + } catch (error) { + this.#logger.warn( + { error, identity: this.avatarIdentity }, + 'failed to remove avatar participant', + ); + } + } + } + if (this.#agentSession) { this.#agentSession.off( AgentSessionEventTypes.ConversationItemAdded,