@livekit/agents 1.0.1 → 1.0.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 (58) hide show
  1. package/dist/ipc/job_proc_lazy_main.cjs +4 -4
  2. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  3. package/dist/ipc/job_proc_lazy_main.js +6 -6
  4. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  5. package/dist/ipc/proc_pool.cjs +14 -2
  6. package/dist/ipc/proc_pool.cjs.map +1 -1
  7. package/dist/ipc/proc_pool.d.ts.map +1 -1
  8. package/dist/ipc/proc_pool.js +14 -2
  9. package/dist/ipc/proc_pool.js.map +1 -1
  10. package/dist/ipc/supervised_proc.cjs +32 -10
  11. package/dist/ipc/supervised_proc.cjs.map +1 -1
  12. package/dist/ipc/supervised_proc.d.cts +2 -0
  13. package/dist/ipc/supervised_proc.d.ts +2 -0
  14. package/dist/ipc/supervised_proc.d.ts.map +1 -1
  15. package/dist/ipc/supervised_proc.js +22 -10
  16. package/dist/ipc/supervised_proc.js.map +1 -1
  17. package/dist/job.cjs +20 -14
  18. package/dist/job.cjs.map +1 -1
  19. package/dist/job.d.cts +11 -5
  20. package/dist/job.d.ts +11 -5
  21. package/dist/job.d.ts.map +1 -1
  22. package/dist/job.js +17 -12
  23. package/dist/job.js.map +1 -1
  24. package/dist/llm/llm.cjs +4 -1
  25. package/dist/llm/llm.cjs.map +1 -1
  26. package/dist/llm/llm.d.ts.map +1 -1
  27. package/dist/llm/llm.js +4 -1
  28. package/dist/llm/llm.js.map +1 -1
  29. package/dist/vad.cjs +3 -0
  30. package/dist/vad.cjs.map +1 -1
  31. package/dist/vad.d.ts.map +1 -1
  32. package/dist/vad.js +3 -0
  33. package/dist/vad.js.map +1 -1
  34. package/dist/voice/agent_session.cjs +9 -2
  35. package/dist/voice/agent_session.cjs.map +1 -1
  36. package/dist/voice/agent_session.d.ts.map +1 -1
  37. package/dist/voice/agent_session.js +9 -2
  38. package/dist/voice/agent_session.js.map +1 -1
  39. package/dist/voice/audio_recognition.cjs +3 -0
  40. package/dist/voice/audio_recognition.cjs.map +1 -1
  41. package/dist/voice/audio_recognition.d.ts.map +1 -1
  42. package/dist/voice/audio_recognition.js +3 -0
  43. package/dist/voice/audio_recognition.js.map +1 -1
  44. package/dist/worker.cjs +25 -4
  45. package/dist/worker.cjs.map +1 -1
  46. package/dist/worker.d.ts.map +1 -1
  47. package/dist/worker.js +25 -4
  48. package/dist/worker.js.map +1 -1
  49. package/package.json +3 -1
  50. package/src/ipc/job_proc_lazy_main.ts +8 -6
  51. package/src/ipc/proc_pool.ts +14 -2
  52. package/src/ipc/supervised_proc.ts +23 -10
  53. package/src/job.ts +27 -12
  54. package/src/llm/llm.ts +4 -2
  55. package/src/vad.ts +3 -0
  56. package/src/voice/agent_session.ts +11 -2
  57. package/src/voice/audio_recognition.ts +5 -0
  58. package/src/worker.ts +25 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livekit/agents",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "LiveKit Agents - Node.js",
5
5
  "main": "dist/index.js",
6
6
  "require": "dist/index.cjs",
@@ -37,10 +37,12 @@
37
37
  "@livekit/mutex": "^1.1.1",
38
38
  "@livekit/protocol": "^1.29.1",
39
39
  "@livekit/typed-emitter": "^3.0.0",
40
+ "@types/pidusage": "^2.0.5",
40
41
  "commander": "^12.0.0",
41
42
  "heap-js": "^2.6.0",
42
43
  "json-schema": "^0.4.0",
43
44
  "livekit-server-sdk": "^2.9.2",
45
+ "pidusage": "^4.0.1",
44
46
  "pino": "^8.19.0",
45
47
  "pino-pretty": "^11.0.0",
46
48
  "sharp": "0.34.3",
@@ -2,14 +2,13 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { Room, RoomEvent } from '@livekit/rtc-node';
5
- import { randomUUID } from 'node:crypto';
6
5
  import { EventEmitter, once } from 'node:events';
7
6
  import { pathToFileURL } from 'node:url';
8
7
  import type { Logger } from 'pino';
9
8
  import { type Agent, isAgent } from '../generator.js';
10
- import { CurrentJobContext, JobContext, JobProcess, type RunningJobInfo } from '../job.js';
9
+ import { JobContext, JobProcess, type RunningJobInfo, runWithJobContextAsync } from '../job.js';
11
10
  import { initializeLogger, log } from '../log.js';
12
- import { Future } from '../utils.js';
11
+ import { Future, shortuuid } from '../utils.js';
13
12
  import { defaultInitializeProcessFunc } from '../worker.js';
14
13
  import type { InferenceExecutor } from './inference_executor.js';
15
14
  import type { IPCMessage } from './message.js';
@@ -50,7 +49,7 @@ class InfClient implements InferenceExecutor {
50
49
  }
51
50
 
52
51
  async doInference(method: string, data: unknown): Promise<unknown> {
53
- const requestId = 'inference_job_' + randomUUID;
52
+ const requestId = shortuuid('inference_job_');
54
53
  process.send!({ case: 'inferenceRequest', value: { requestId, method, data } });
55
54
  this.#requests[requestId] = new PendingInference();
56
55
  const resp = await this.#requests[requestId]!.promise;
@@ -88,7 +87,6 @@ const startJob = (
88
87
  };
89
88
 
90
89
  const ctx = new JobContext(proc, info, room, onConnect, onShutdown, new InfClient());
91
- new CurrentJobContext(ctx);
92
90
 
93
91
  const task = new Promise<void>(async () => {
94
92
  const unconnectedTimeout = setTimeout(() => {
@@ -99,7 +97,11 @@ const startJob = (
99
97
  );
100
98
  }
101
99
  }, 10000);
102
- func(ctx).finally(() => clearTimeout(unconnectedTimeout));
100
+
101
+ // Run the job function within the AsyncLocalStorage context
102
+ await runWithJobContextAsync(ctx, () => func(ctx)).finally(() => {
103
+ clearTimeout(unconnectedTimeout);
104
+ });
103
105
 
104
106
  await once(closeEvent, 'close').then((close) => {
105
107
  logger.debug('shutting down');
@@ -115,7 +115,12 @@ export class ProcPool {
115
115
  unlock();
116
116
  await proc.join();
117
117
  } finally {
118
- this.executors.splice(this.executors.indexOf(proc));
118
+ const procIndex = this.executors.indexOf(proc);
119
+ if (procIndex !== -1) {
120
+ this.executors.splice(procIndex, 1);
121
+ } else {
122
+ throw new Error(`proc ${proc} not found in executors`);
123
+ }
119
124
  }
120
125
  }
121
126
 
@@ -134,7 +139,14 @@ export class ProcPool {
134
139
  this.procUnlock = await this.procMutex.lock();
135
140
  const task = this.procWatchTask();
136
141
  this.tasks.push(task);
137
- task.finally(() => this.tasks.splice(this.tasks.indexOf(task)));
142
+ task.finally(() => {
143
+ const taskIndex = this.tasks.indexOf(task);
144
+ if (taskIndex !== -1) {
145
+ this.tasks.splice(taskIndex, 1);
146
+ } else {
147
+ throw new Error(`task ${task} not found in tasks`);
148
+ }
149
+ });
138
150
  }
139
151
  }
140
152
  }
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import type { ChildProcess } from 'node:child_process';
5
5
  import { once } from 'node:events';
6
+ import pidusage from 'pidusage';
6
7
  import type { RunningJobInfo } from '../job.js';
7
8
  import { log, loggerOptions } from '../log.js';
8
9
  import { Future } from '../utils.js';
@@ -25,7 +26,7 @@ export abstract class SupervisedProc {
25
26
  #runningJob?: RunningJobInfo = undefined;
26
27
  proc?: ChildProcess;
27
28
  #pingInterval?: ReturnType<typeof setInterval>;
28
- #memoryWatch?: ReturnType<typeof setInterval>;
29
+ #memoryMonitorInterval?: ReturnType<typeof setInterval>;
29
30
  #pongTimeout?: ReturnType<typeof setTimeout>;
30
31
  protected init = new Future();
31
32
  #join = new Future();
@@ -90,8 +91,8 @@ export abstract class SupervisedProc {
90
91
  this.#join.resolve();
91
92
  }, this.#opts.pingTimeout);
92
93
 
93
- this.#memoryWatch = setInterval(() => {
94
- const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);
94
+ this.#memoryMonitorInterval = setInterval(async () => {
95
+ const memoryMB = await this.getChildMemoryUsageMB();
95
96
  if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {
96
97
  this.#logger
97
98
  .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })
@@ -104,9 +105,9 @@ export abstract class SupervisedProc {
104
105
  memoryWarnMB: this.#opts.memoryWarnMB,
105
106
  memoryLimitMB: this.#opts.memoryLimitMB,
106
107
  })
107
- .error('process memory usage is high');
108
+ .warn('process memory usage is high');
108
109
  }
109
- });
110
+ }, 5000);
110
111
 
111
112
  const listener = (msg: IPCMessage) => {
112
113
  switch (msg.case) {
@@ -135,9 +136,7 @@ export abstract class SupervisedProc {
135
136
  this.#logger
136
137
  .child({ err })
137
138
  .warn('job process exited unexpectedly; this likely means the error above caused a crash');
138
- clearTimeout(this.#pongTimeout);
139
- clearInterval(this.#pingInterval);
140
- clearInterval(this.#memoryWatch);
139
+ this.clearTimers();
141
140
  this.#join.resolve();
142
141
  });
143
142
 
@@ -196,8 +195,7 @@ export abstract class SupervisedProc {
196
195
  }, this.#opts.closeTimeout);
197
196
  await this.#join.await.then(() => {
198
197
  clearTimeout(timer);
199
- clearTimeout(this.#pongTimeout);
200
- clearInterval(this.#pingInterval);
198
+ this.clearTimers();
201
199
  });
202
200
  }
203
201
 
@@ -208,4 +206,19 @@ export abstract class SupervisedProc {
208
206
  this.#runningJob = info;
209
207
  this.proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
210
208
  }
209
+
210
+ private async getChildMemoryUsageMB(): Promise<number> {
211
+ const pid = this.proc?.pid;
212
+ if (!pid) {
213
+ return 0;
214
+ }
215
+ const stats = await pidusage(pid);
216
+ return stats.memory / (1024 * 1024); // Convert bytes to MB
217
+ }
218
+
219
+ private clearTimers() {
220
+ clearTimeout(this.#pongTimeout);
221
+ clearInterval(this.#pingInterval);
222
+ clearInterval(this.#memoryMonitorInterval);
223
+ }
211
224
  }
package/src/job.ts CHANGED
@@ -10,21 +10,13 @@ import type {
10
10
  RtcConfiguration,
11
11
  } from '@livekit/rtc-node';
12
12
  import { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node';
13
+ import { AsyncLocalStorage } from 'node:async_hooks';
13
14
  import type { Logger } from 'pino';
14
15
  import type { InferenceExecutor } from './ipc/inference_executor.js';
15
16
  import { log } from './log.js';
16
17
 
17
- export class CurrentJobContext {
18
- static #current: JobContext;
19
-
20
- constructor(proc: JobContext) {
21
- CurrentJobContext.#current = proc;
22
- }
23
-
24
- static getCurrent(): JobContext {
25
- return CurrentJobContext.#current;
26
- }
27
- }
18
+ // AsyncLocalStorage for job context, similar to Python's contextvars
19
+ const jobContextStorage = new AsyncLocalStorage<JobContext>();
28
20
 
29
21
  /**
30
22
  * Returns the current job context.
@@ -32,13 +24,29 @@ export class CurrentJobContext {
32
24
  * @throws {Error} if no job context is found
33
25
  */
34
26
  export function getJobContext(): JobContext {
35
- const ctx = CurrentJobContext.getCurrent();
27
+ const ctx = jobContextStorage.getStore();
36
28
  if (!ctx) {
37
29
  throw new Error('no job context found, are you running this code inside a job entrypoint?');
38
30
  }
39
31
  return ctx;
40
32
  }
41
33
 
34
+ /**
35
+ * Runs a function within a job context, similar to Python's contextvars.
36
+ * @internal
37
+ */
38
+ export function runWithJobContext<T>(context: JobContext, fn: () => T): T {
39
+ return jobContextStorage.run(context, fn);
40
+ }
41
+
42
+ /**
43
+ * Runs an async function within a job context, similar to Python's contextvars.
44
+ * @internal
45
+ */
46
+ export function runWithJobContextAsync<T>(context: JobContext, fn: () => Promise<T>): Promise<T> {
47
+ return jobContextStorage.run(context, fn);
48
+ }
49
+
42
50
  /** Which tracks, if any, should the agent automatically subscribe to? */
43
51
  export enum AutoSubscribe {
44
52
  SUBSCRIBE_ALL,
@@ -89,6 +97,8 @@ export class JobContext {
89
97
  #logger: Logger;
90
98
  #inferenceExecutor: InferenceExecutor;
91
99
 
100
+ private connected: boolean = false;
101
+
92
102
  constructor(
93
103
  proc: JobProcess,
94
104
  info: RunningJobInfo,
@@ -191,6 +201,10 @@ export class JobContext {
191
201
  autoSubscribe: AutoSubscribe = AutoSubscribe.SUBSCRIBE_ALL,
192
202
  rtcConfig?: RtcConfiguration,
193
203
  ) {
204
+ if (this.connected) {
205
+ return;
206
+ }
207
+
194
208
  const opts = {
195
209
  e2ee,
196
210
  autoSubscribe: autoSubscribe == AutoSubscribe.SUBSCRIBE_ALL,
@@ -215,6 +229,7 @@ export class JobContext {
215
229
  });
216
230
  });
217
231
  }
232
+ this.connected = true;
218
233
  }
219
234
 
220
235
  /**
package/src/llm/llm.ts CHANGED
@@ -215,8 +215,10 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
215
215
  promptTokens: usage?.promptTokens || 0,
216
216
  promptCachedTokens: usage?.promptCachedTokens || 0,
217
217
  totalTokens: usage?.totalTokens || 0,
218
- tokensPerSecond:
219
- (usage?.completionTokens || 0) / Math.trunc(Number(duration / BigInt(1000000000))),
218
+ tokensPerSecond: (() => {
219
+ const durationSeconds = Math.trunc(Number(duration / BigInt(1000000000)));
220
+ return durationSeconds > 0 ? (usage?.completionTokens || 0) / durationSeconds : 0;
221
+ })(),
220
222
  };
221
223
  this.#llm.emit('metrics_collected', metrics);
222
224
  }
package/src/vad.ts CHANGED
@@ -167,6 +167,9 @@ export abstract class VADStream implements AsyncIterableIterator<VADEvent> {
167
167
  }
168
168
  break;
169
169
  case VADEventType.INFERENCE_DONE:
170
+ inferenceDurationTotal += value.inferenceDuration;
171
+ this.#lastActivityTime = process.hrtime.bigint();
172
+ break;
170
173
  case VADEventType.END_OF_SPEECH:
171
174
  this.#lastActivityTime = process.hrtime.bigint();
172
175
  break;
@@ -5,6 +5,7 @@ import type { AudioFrame, Room } from '@livekit/rtc-node';
5
5
  import type { TypedEventEmitter as TypedEmitter } from '@livekit/typed-emitter';
6
6
  import { EventEmitter } from 'node:events';
7
7
  import type { ReadableStream } from 'node:stream/web';
8
+ import { getJobContext } from '../job.js';
8
9
  import { ChatContext, ChatMessage } from '../llm/chat_context.js';
9
10
  import type { LLM, RealtimeModel, RealtimeModelError, ToolChoice } from '../llm/index.js';
10
11
  import type { LLMError } from '../llm/llm.js';
@@ -184,6 +185,7 @@ export class AgentSession<
184
185
  this.agent = agent;
185
186
  this._updateAgentState('initializing');
186
187
 
188
+ const tasks: Promise<void>[] = [];
187
189
  // Check for existing input/output configuration and warn if needed
188
190
  if (this.input.audio && inputOptions?.audioEnabled !== false) {
189
191
  this.logger.warn('RoomIO audio input is enabled but input.audio is already set, ignoring..');
@@ -209,7 +211,15 @@ export class AgentSession<
209
211
  });
210
212
  this.roomIO.start();
211
213
 
212
- this.updateActivity(this.agent);
214
+ const ctx = getJobContext();
215
+ if (ctx && ctx.room === room && !room.isConnected) {
216
+ this.logger.debug('Auto-connecting to room via job context');
217
+ tasks.push(ctx.connect());
218
+ }
219
+ // TODO(AJS-265): add shutdown callback to job context
220
+ tasks.push(this.updateActivity(this.agent));
221
+
222
+ await Promise.allSettled(tasks);
213
223
 
214
224
  // Log used IO configuration
215
225
  this.logger.debug(
@@ -220,7 +230,6 @@ export class AgentSession<
220
230
  `using transcript io: \`AgentSession\` -> ${this.output.transcription ? '`' + this.output.transcription.constructor.name + '`' : '(none)'}`,
221
231
  );
222
232
 
223
- this.logger.debug('AgentSession started');
224
233
  this.started = true;
225
234
  this._updateAgentState('listening');
226
235
  }
@@ -367,6 +367,11 @@ export class AudioRecognition {
367
367
  this.hooks.onStartOfSpeech(ev);
368
368
  this.speaking = true;
369
369
 
370
+ // Capture sample rate from the first VAD event if not already set
371
+ if (ev.frames.length > 0 && ev.frames[0]) {
372
+ this.sampleRate = ev.frames[0].sampleRate;
373
+ }
374
+
370
375
  this.bounceEOUTask?.cancel();
371
376
  break;
372
377
  case VADEventType.INFERENCE_DONE:
package/src/worker.ts CHANGED
@@ -190,7 +190,7 @@ export class WorkerOptions {
190
190
  port = undefined,
191
191
  logLevel = 'info',
192
192
  production = false,
193
- jobMemoryWarnMB = 300,
193
+ jobMemoryWarnMB = 500,
194
194
  jobMemoryLimitMB = 0,
195
195
  }: {
196
196
  /**
@@ -567,7 +567,14 @@ export class Worker {
567
567
  if (!msg.message.value.job) return;
568
568
  const task = this.#availability(msg.message.value);
569
569
  this.#tasks.push(task);
570
- task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
570
+ task.finally(() => {
571
+ const taskIndex = this.#tasks.indexOf(task);
572
+ if (taskIndex !== -1) {
573
+ this.#tasks.splice(taskIndex, 1);
574
+ } else {
575
+ throw new Error(`task ${task} not found in tasks`);
576
+ }
577
+ });
571
578
  break;
572
579
  }
573
580
  case 'assignment': {
@@ -585,7 +592,14 @@ export class Worker {
585
592
  case 'termination': {
586
593
  const task = this.#termination(msg.message.value);
587
594
  this.#tasks.push(task);
588
- task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
595
+ task.finally(() => {
596
+ const taskIndex = this.#tasks.indexOf(task);
597
+ if (taskIndex !== -1) {
598
+ this.#tasks.splice(taskIndex, 1);
599
+ } else {
600
+ throw new Error(`task ${task} not found in tasks`);
601
+ }
602
+ });
589
603
  break;
590
604
  }
591
605
  }
@@ -737,7 +751,14 @@ export class Worker {
737
751
 
738
752
  const task = jobRequestTask();
739
753
  this.#tasks.push(task);
740
- task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
754
+ task.finally(() => {
755
+ const taskIndex = this.#tasks.indexOf(task);
756
+ if (taskIndex !== -1) {
757
+ this.#tasks.splice(taskIndex, 1);
758
+ } else {
759
+ throw new Error(`task ${task} not found in tasks`);
760
+ }
761
+ });
741
762
  }
742
763
 
743
764
  async #termination(msg: JobTermination) {