@livekit/agents-plugin-protoface 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +15 -0
  3. package/dist/api.cjs +153 -0
  4. package/dist/api.cjs.map +1 -0
  5. package/dist/api.d.cts +42 -0
  6. package/dist/api.d.ts +42 -0
  7. package/dist/api.d.ts.map +1 -0
  8. package/dist/api.js +133 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/api.test.cjs +55 -0
  11. package/dist/api.test.cjs.map +1 -0
  12. package/dist/api.test.d.cts +2 -0
  13. package/dist/api.test.d.ts +2 -0
  14. package/dist/api.test.d.ts.map +1 -0
  15. package/dist/api.test.js +54 -0
  16. package/dist/api.test.js.map +1 -0
  17. package/dist/avatar.cjs +160 -0
  18. package/dist/avatar.cjs.map +1 -0
  19. package/dist/avatar.d.cts +54 -0
  20. package/dist/avatar.d.ts +54 -0
  21. package/dist/avatar.d.ts.map +1 -0
  22. package/dist/avatar.js +139 -0
  23. package/dist/avatar.js.map +1 -0
  24. package/dist/avatar.test.cjs +107 -0
  25. package/dist/avatar.test.cjs.map +1 -0
  26. package/dist/avatar.test.d.cts +2 -0
  27. package/dist/avatar.test.d.ts +2 -0
  28. package/dist/avatar.test.d.ts.map +1 -0
  29. package/dist/avatar.test.js +106 -0
  30. package/dist/avatar.test.js.map +1 -0
  31. package/dist/index.cjs +36 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +3 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/log.cjs +30 -0
  39. package/dist/log.cjs.map +1 -0
  40. package/dist/log.d.cts +3 -0
  41. package/dist/log.d.ts +3 -0
  42. package/dist/log.d.ts.map +1 -0
  43. package/dist/log.js +6 -0
  44. package/dist/log.js.map +1 -0
  45. package/package.json +51 -0
  46. package/src/api.test.ts +65 -0
  47. package/src/api.ts +182 -0
  48. package/src/avatar.test.ts +132 -0
  49. package/src/avatar.ts +204 -0
  50. package/src/index.ts +19 -0
  51. package/src/log.ts +7 -0
package/src/api.ts ADDED
@@ -0,0 +1,182 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import {
5
+ type APIConnectOptions,
6
+ APIConnectionError,
7
+ APIStatusError,
8
+ APITimeoutError,
9
+ DEFAULT_API_CONNECT_OPTIONS,
10
+ intervalForRetry,
11
+ } from '@livekit/agents';
12
+ import { log } from './log.js';
13
+
14
+ /** @public */
15
+ export const DEFAULT_API_URL = 'https://api.protoface.com';
16
+
17
+ const USER_AGENT = `@livekit/agents-plugin-protoface/${__PACKAGE_VERSION__}`;
18
+
19
+ /** @public */
20
+ export class ProtofaceException extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = 'ProtofaceException';
24
+ }
25
+ }
26
+
27
+ /** @public */
28
+ export interface ProtofaceAPIOptions {
29
+ /** Protoface API key. Falls back to the `PROTOFACE_API_KEY` env var. */
30
+ apiKey?: string | null;
31
+ /** Override the Protoface API base URL. */
32
+ apiUrl?: string | null;
33
+ /** API retry/timeout options. */
34
+ connOptions?: APIConnectOptions;
35
+ }
36
+
37
+ /** @public */
38
+ export interface StartSessionOptions {
39
+ avatarId: string;
40
+ transport: Record<string, unknown>;
41
+ maxDurationMs?: number | null;
42
+ }
43
+
44
+ /** @public */
45
+ export type ProtofaceSession = Record<string, unknown> & { id?: string };
46
+
47
+ /**
48
+ * Async client for the Protoface session API.
49
+ *
50
+ * @public
51
+ */
52
+ export class ProtofaceAPI {
53
+ private apiKey: string;
54
+ private apiUrl: string;
55
+ private connOptions: APIConnectOptions;
56
+
57
+ #logger = log();
58
+
59
+ constructor(options: ProtofaceAPIOptions = {}) {
60
+ const apiKey = options.apiKey ?? process.env.PROTOFACE_API_KEY ?? '';
61
+ if (!apiKey) {
62
+ throw new ProtofaceException(
63
+ 'apiKey must be set by passing it to ProtofaceAPI or setting the PROTOFACE_API_KEY environment variable',
64
+ );
65
+ }
66
+
67
+ this.apiKey = apiKey;
68
+ this.apiUrl = trimTrailingSlashes(
69
+ options.apiUrl ?? process.env.PROTOFACE_API_URL ?? DEFAULT_API_URL,
70
+ );
71
+ this.connOptions = options.connOptions ?? DEFAULT_API_CONNECT_OPTIONS;
72
+ }
73
+
74
+ async startSession(options: StartSessionOptions): Promise<ProtofaceSession> {
75
+ const body: Record<string, unknown> = {
76
+ avatar_id: options.avatarId,
77
+ transport: options.transport,
78
+ };
79
+ if (options.maxDurationMs != null) {
80
+ body.max_duration_seconds = options.maxDurationMs / 1000;
81
+ }
82
+
83
+ return (await this.json('POST', '/v1/sessions', body)) as ProtofaceSession;
84
+ }
85
+
86
+ async endSession(sessionId: string): Promise<Record<string, unknown>> {
87
+ return (await this.json('POST', `/v1/sessions/${sessionId}/end`)) as Record<string, unknown>;
88
+ }
89
+
90
+ private async json(
91
+ method: string,
92
+ path: string,
93
+ body?: Record<string, unknown>,
94
+ ): Promise<Record<string, unknown>> {
95
+ const url = `${this.apiUrl}${path}`;
96
+ let lastError: unknown;
97
+
98
+ for (let i = 0; i <= this.connOptions.maxRetry; i++) {
99
+ try {
100
+ const response = await fetch(url, {
101
+ method,
102
+ headers: {
103
+ accept: 'application/json',
104
+ authorization: `Bearer ${this.apiKey}`,
105
+ 'content-type': 'application/json',
106
+ 'user-agent': USER_AGENT,
107
+ },
108
+ body: body == null ? undefined : JSON.stringify(body),
109
+ signal: AbortSignal.timeout(this.connOptions.timeoutMs),
110
+ });
111
+ const payload = await readPayload(response);
112
+ if (response.ok) {
113
+ if (!isRecord(payload)) {
114
+ throw new APIStatusError({
115
+ message: 'Protoface API returned a non-object JSON response',
116
+ options: { statusCode: response.status, body: { payload }, retryable: false },
117
+ });
118
+ }
119
+ return payload;
120
+ }
121
+
122
+ throw new APIStatusError({
123
+ message: 'Protoface API returned an error',
124
+ options: { statusCode: response.status, body: isRecord(payload) ? payload : { payload } },
125
+ });
126
+ } catch (error) {
127
+ if (error instanceof APIStatusError && !error.retryable) {
128
+ throw error;
129
+ }
130
+ lastError = normalizeConnectionError(error);
131
+ }
132
+
133
+ if (i < this.connOptions.maxRetry) {
134
+ this.#logger.warn(
135
+ { attempt: i + 1, method, path },
136
+ 'protoface api request failed, retrying',
137
+ );
138
+ await new Promise((resolve) => setTimeout(resolve, intervalForRetry(this.connOptions, i)));
139
+ }
140
+ }
141
+
142
+ throw new APIConnectionError({
143
+ message: 'Failed to call Protoface API after all retries.',
144
+ options: { body: isRecord(lastError) ? lastError : null },
145
+ });
146
+ }
147
+ }
148
+
149
+ async function readPayload(response: Response): Promise<unknown> {
150
+ const text = await response.text();
151
+ if (!text) {
152
+ return {};
153
+ }
154
+
155
+ try {
156
+ return JSON.parse(text) as unknown;
157
+ } catch {
158
+ return { raw: text };
159
+ }
160
+ }
161
+
162
+ function isRecord(value: unknown): value is Record<string, unknown> {
163
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
164
+ }
165
+
166
+ function normalizeConnectionError(error: unknown): unknown {
167
+ if (error instanceof APIStatusError) {
168
+ return error;
169
+ }
170
+ if (error instanceof DOMException && error.name === 'TimeoutError') {
171
+ return new APITimeoutError({ message: 'Protoface API request timed out.' });
172
+ }
173
+ return error;
174
+ }
175
+
176
+ function trimTrailingSlashes(value: string): string {
177
+ let end = value.length;
178
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
179
+ end -= 1;
180
+ }
181
+ return value.slice(0, end);
182
+ }
@@ -0,0 +1,132 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { voice } from '@livekit/agents';
5
+ import type { Room } from '@livekit/rtc-node';
6
+ import { TrackKind } from '@livekit/rtc-node';
7
+ import { EventEmitter } from 'node:events';
8
+ import { afterEach, describe, expect, it, vi } from 'vitest';
9
+ import { ProtofaceAPI } from './api.js';
10
+ import { AvatarSession } from './avatar.js';
11
+
12
+ type DataStreamAudioOutputInternals = {
13
+ destinationIdentity: string;
14
+ sampleRate: number;
15
+ waitRemoteTrack?: TrackKind;
16
+ };
17
+
18
+ function fakeRoom(): Room {
19
+ const emitter = new EventEmitter();
20
+ const remoteParticipant = {
21
+ identity: 'protoface-avatar-agent',
22
+ trackPublications: new Map([['video', { kind: TrackKind.KIND_VIDEO }]]),
23
+ };
24
+
25
+ return {
26
+ name: 'test-room',
27
+ isConnected: true,
28
+ localParticipant: {
29
+ identity: 'local-agent',
30
+ registerRpcMethod: vi.fn(),
31
+ },
32
+ remoteParticipants: new Map([[remoteParticipant.identity, remoteParticipant]]),
33
+ on: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => {
34
+ emitter.on(event, listener);
35
+ return emitter;
36
+ }),
37
+ off: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => {
38
+ emitter.off(event, listener);
39
+ return emitter;
40
+ }),
41
+ } as unknown as Room;
42
+ }
43
+
44
+ function fakeAgentSession(): voice.AgentSession {
45
+ const emitter = new EventEmitter();
46
+ return {
47
+ _started: true,
48
+ output: { audio: null as voice.AudioOutput | null },
49
+ on: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => {
50
+ emitter.on(event, listener);
51
+ return emitter;
52
+ }),
53
+ off: vi.fn((event: string | symbol, listener: (...args: unknown[]) => void) => {
54
+ emitter.off(event, listener);
55
+ return emitter;
56
+ }),
57
+ } as unknown as voice.AgentSession;
58
+ }
59
+
60
+ describe('Protoface AvatarSession', () => {
61
+ afterEach(() => {
62
+ vi.restoreAllMocks();
63
+ voice.DataStreamAudioOutput._playbackFinishedRpcRegistered = false;
64
+ voice.DataStreamAudioOutput._playbackFinishedHandlers = {};
65
+ voice.DataStreamAudioOutput._playbackStartedRpcRegistered = false;
66
+ voice.DataStreamAudioOutput._playbackStartedHandlers = {};
67
+ });
68
+
69
+ it('calls base AvatarSession.start first', async () => {
70
+ const sentinel = new Error('super-start-called');
71
+ const superStartSpy = vi
72
+ .spyOn(voice.AvatarSession.prototype, 'start')
73
+ .mockRejectedValue(sentinel);
74
+
75
+ const avatar = new AvatarSession({ apiKey: 'protoface-key' });
76
+
77
+ await expect(avatar.start(fakeAgentSession(), fakeRoom())).rejects.toThrow(
78
+ 'super-start-called',
79
+ );
80
+ expect(superStartSpy).toHaveBeenCalledTimes(1);
81
+ });
82
+
83
+ it('starts a Protoface session and routes agent audio to the avatar', async () => {
84
+ vi.spyOn(voice.AvatarSession.prototype, 'start').mockResolvedValue(undefined);
85
+ vi.spyOn(voice.AvatarSession.prototype, 'aclose').mockResolvedValue(undefined);
86
+ const startSessionSpy = vi
87
+ .spyOn(ProtofaceAPI.prototype, 'startSession')
88
+ .mockResolvedValue({ id: 'protoface-session-1' });
89
+ const endSessionSpy = vi.spyOn(ProtofaceAPI.prototype, 'endSession').mockResolvedValue({});
90
+
91
+ const avatar = new AvatarSession({
92
+ apiKey: 'protoface-key',
93
+ avatarId: 'avatar-1',
94
+ maxDurationMs: 120_000,
95
+ });
96
+ const agentSession = fakeAgentSession();
97
+
98
+ await avatar.start(agentSession, fakeRoom(), {
99
+ livekitUrl: 'wss://livekit.example.com',
100
+ livekitApiKey: 'livekit-api-key',
101
+ livekitApiSecret: 'livekit-api-secret',
102
+ });
103
+
104
+ expect(avatar.sessionId).toBe('protoface-session-1');
105
+ expect(startSessionSpy).toHaveBeenCalledWith({
106
+ avatarId: 'avatar-1',
107
+ transport: expect.objectContaining({
108
+ type: 'livekit',
109
+ url: 'wss://livekit.example.com',
110
+ room_name: 'test-room',
111
+ worker_identity: 'protoface-avatar-agent',
112
+ audio_source: 'data_stream',
113
+ worker_token: expect.any(String),
114
+ }),
115
+ maxDurationMs: 120_000,
116
+ });
117
+
118
+ const audioOutput = agentSession.output.audio;
119
+ expect(audioOutput).toBeInstanceOf(voice.DataStreamAudioOutput);
120
+ expect((audioOutput as unknown as DataStreamAudioOutputInternals).destinationIdentity).toBe(
121
+ 'protoface-avatar-agent',
122
+ );
123
+ expect((audioOutput as unknown as DataStreamAudioOutputInternals).sampleRate).toBe(16000);
124
+ expect((audioOutput as unknown as DataStreamAudioOutputInternals).waitRemoteTrack).toBe(
125
+ TrackKind.KIND_VIDEO,
126
+ );
127
+
128
+ await avatar.aclose();
129
+ expect(endSessionSpy).toHaveBeenCalledWith('protoface-session-1');
130
+ expect(avatar.sessionId).toBeNull();
131
+ });
132
+ });
package/src/avatar.ts ADDED
@@ -0,0 +1,204 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import {
5
+ type APIConnectOptions,
6
+ DEFAULT_API_CONNECT_OPTIONS,
7
+ getJobContext,
8
+ voice,
9
+ } from '@livekit/agents';
10
+ import type { Room } from '@livekit/rtc-node';
11
+ import { TrackKind } from '@livekit/rtc-node';
12
+ import type { VideoGrant } from 'livekit-server-sdk';
13
+ import { AccessToken } from 'livekit-server-sdk';
14
+ import { ProtofaceAPI, ProtofaceException } from './api.js';
15
+ import { log } from './log.js';
16
+
17
+ /** @public */
18
+ export const DEFAULT_STOCK_AVATAR_ID = 'av_stock_001';
19
+
20
+ const ATTRIBUTE_PUBLISH_ON_BEHALF = 'lk.publish_on_behalf';
21
+ const SAMPLE_RATE = 16000;
22
+ const AVATAR_AGENT_IDENTITY = 'protoface-avatar-agent';
23
+ const AVATAR_AGENT_NAME = 'protoface-avatar-agent';
24
+
25
+ /** @public */
26
+ export interface AvatarSessionOptions {
27
+ /** Protoface avatar ID to render. Defaults to the stable stock avatar ID `av_stock_001`. */
28
+ avatarId?: string;
29
+ /** Override the Protoface API base URL. */
30
+ apiUrl?: string | null;
31
+ /** Protoface API key. Falls back to the `PROTOFACE_API_KEY` env var. */
32
+ apiKey?: string | null;
33
+ /** Optional maximum session duration in milliseconds. */
34
+ maxDurationMs?: number | null;
35
+ /** Identity for the avatar participant. Defaults to `protoface-avatar-agent`. */
36
+ avatarParticipantIdentity?: string | null;
37
+ /** Display name for the avatar participant. Defaults to `protoface-avatar-agent`. */
38
+ avatarParticipantName?: string | null;
39
+ /** API retry/timeout options. */
40
+ connOptions?: APIConnectOptions;
41
+ }
42
+
43
+ /**
44
+ * Optional LiveKit credentials for {@link AvatarSession.start}; falls back to env vars.
45
+ *
46
+ * @public
47
+ */
48
+ export interface StartOptions {
49
+ livekitUrl?: string | null;
50
+ livekitApiKey?: string | null;
51
+ livekitApiSecret?: string | null;
52
+ }
53
+
54
+ /**
55
+ * A Protoface avatar session for LiveKit Agents.
56
+ *
57
+ * @public
58
+ */
59
+ export class AvatarSession extends voice.AvatarSession {
60
+ private avatarId: string;
61
+ private maxDurationMs?: number | null;
62
+ private avatarParticipantIdentity: string;
63
+ private avatarParticipantName: string;
64
+ private api: ProtofaceAPI;
65
+ private sessionIdValue: string | null = null;
66
+
67
+ #logger = log();
68
+
69
+ constructor(options: AvatarSessionOptions = {}) {
70
+ super();
71
+ this.avatarId = options.avatarId ?? DEFAULT_STOCK_AVATAR_ID;
72
+ this.maxDurationMs = options.maxDurationMs;
73
+ this.avatarParticipantIdentity = options.avatarParticipantIdentity || AVATAR_AGENT_IDENTITY;
74
+ this.avatarParticipantName = options.avatarParticipantName || AVATAR_AGENT_NAME;
75
+ this.api = new ProtofaceAPI({
76
+ apiKey: options.apiKey,
77
+ apiUrl: options.apiUrl,
78
+ connOptions: options.connOptions ?? DEFAULT_API_CONNECT_OPTIONS,
79
+ });
80
+ }
81
+
82
+ override get avatarIdentity(): string {
83
+ return this.avatarParticipantIdentity;
84
+ }
85
+
86
+ override get provider(): string {
87
+ return 'protoface';
88
+ }
89
+
90
+ /** Protoface session ID after `start()` succeeds, otherwise `null`. */
91
+ get sessionId(): string | null {
92
+ return this.sessionIdValue;
93
+ }
94
+
95
+ async start(
96
+ agentSession: voice.AgentSession,
97
+ room: Room,
98
+ options: StartOptions = {},
99
+ ): Promise<void> {
100
+ if (this.sessionIdValue !== null) {
101
+ throw new Error('AvatarSession.start() called twice; create a new AvatarSession.');
102
+ }
103
+
104
+ await super.start(agentSession, room);
105
+
106
+ const livekitUrl = options.livekitUrl ?? process.env.LIVEKIT_URL;
107
+ const livekitApiKey = options.livekitApiKey ?? process.env.LIVEKIT_API_KEY;
108
+ const livekitApiSecret = options.livekitApiSecret ?? process.env.LIVEKIT_API_SECRET;
109
+ if (!livekitUrl || !livekitApiKey || !livekitApiSecret) {
110
+ throw new ProtofaceException(
111
+ 'livekitUrl, livekitApiKey, and livekitApiSecret must be set by arguments or environment variables',
112
+ );
113
+ }
114
+
115
+ const workerToken = await this.mintWorkerToken({ room, livekitApiKey, livekitApiSecret });
116
+ const session = await this.api.startSession({
117
+ avatarId: this.avatarId,
118
+ transport: {
119
+ type: 'livekit',
120
+ url: livekitUrl,
121
+ room_name: room.name,
122
+ worker_token: workerToken,
123
+ worker_identity: this.avatarParticipantIdentity,
124
+ audio_source: 'data_stream',
125
+ },
126
+ maxDurationMs: this.maxDurationMs,
127
+ });
128
+
129
+ if (typeof session.id !== 'string') {
130
+ throw new ProtofaceException('Protoface API response missing session id');
131
+ }
132
+
133
+ this.sessionIdValue = session.id;
134
+ this.#logger.debug(
135
+ { sessionId: this.sessionIdValue, avatarId: this.avatarId },
136
+ 'protoface session started',
137
+ );
138
+
139
+ agentSession.output.audio = new voice.DataStreamAudioOutput({
140
+ room,
141
+ destinationIdentity: this.avatarParticipantIdentity,
142
+ sampleRate: SAMPLE_RATE,
143
+ waitRemoteTrack: TrackKind.KIND_VIDEO,
144
+ });
145
+ }
146
+
147
+ async aclose(): Promise<void> {
148
+ const sessionId = this.sessionIdValue;
149
+ this.sessionIdValue = null;
150
+ try {
151
+ if (sessionId !== null) {
152
+ try {
153
+ await this.api.endSession(sessionId);
154
+ } catch (error) {
155
+ this.#logger.warn({ error: String(error), sessionId }, 'failed to end protoface session');
156
+ }
157
+ }
158
+ } finally {
159
+ await super.aclose();
160
+ }
161
+ }
162
+
163
+ private async mintWorkerToken({
164
+ room,
165
+ livekitApiKey,
166
+ livekitApiSecret,
167
+ }: {
168
+ room: Room;
169
+ livekitApiKey: string;
170
+ livekitApiSecret: string;
171
+ }): Promise<string> {
172
+ let localParticipantIdentity = '';
173
+ try {
174
+ const jobCtx = getJobContext();
175
+ localParticipantIdentity = jobCtx.agent?.identity || '';
176
+ } catch {
177
+ // Fall back to the connected room below when no job context is available.
178
+ }
179
+
180
+ if (!localParticipantIdentity && room.isConnected && room.localParticipant) {
181
+ localParticipantIdentity = room.localParticipant.identity;
182
+ }
183
+
184
+ if (!localParticipantIdentity) {
185
+ throw new ProtofaceException('failed to get local participant identity');
186
+ }
187
+
188
+ const token = new AccessToken(livekitApiKey, livekitApiSecret, {
189
+ identity: this.avatarParticipantIdentity,
190
+ name: this.avatarParticipantName,
191
+ });
192
+ token.kind = 'agent';
193
+ token.attributes = { [ATTRIBUTE_PUBLISH_ON_BEHALF]: localParticipantIdentity };
194
+ token.addGrant({
195
+ roomJoin: true,
196
+ room: room.name,
197
+ canPublish: true,
198
+ canSubscribe: true,
199
+ canPublishData: true,
200
+ } as VideoGrant);
201
+
202
+ return token.toJwt();
203
+ }
204
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { Plugin } from '@livekit/agents';
5
+
6
+ export * from './api.js';
7
+ export * from './avatar.js';
8
+
9
+ class ProtofacePlugin extends Plugin {
10
+ constructor() {
11
+ super({
12
+ title: 'protoface',
13
+ version: __PACKAGE_VERSION__,
14
+ package: __PACKAGE_NAME__,
15
+ });
16
+ }
17
+ }
18
+
19
+ Plugin.registerPlugin(new ProtofacePlugin());
package/src/log.ts ADDED
@@ -0,0 +1,7 @@
1
+ // SPDX-FileCopyrightText: 2026 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { log as agentsLog } from '@livekit/agents';
5
+ import type { Logger } from 'pino';
6
+
7
+ export const log = (): Logger => agentsLog().child({ plugin: 'protoface' });