@livekit/agents 0.6.4 → 0.7.1

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 (184) hide show
  1. package/dist/cli.cjs +8 -0
  2. package/dist/cli.cjs.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +8 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.cjs +6 -1
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/inference_runner.cjs +38 -0
  13. package/dist/inference_runner.cjs.map +1 -0
  14. package/dist/inference_runner.d.ts +11 -0
  15. package/dist/inference_runner.d.ts.map +1 -0
  16. package/dist/inference_runner.js +14 -0
  17. package/dist/inference_runner.js.map +1 -0
  18. package/dist/ipc/index.cjs +23 -0
  19. package/dist/ipc/index.cjs.map +1 -0
  20. package/dist/ipc/index.d.ts +2 -0
  21. package/dist/ipc/index.d.ts.map +1 -0
  22. package/dist/ipc/index.js +2 -0
  23. package/dist/ipc/index.js.map +1 -0
  24. package/dist/ipc/inference_executor.cjs +17 -0
  25. package/dist/ipc/inference_executor.cjs.map +1 -0
  26. package/dist/ipc/inference_executor.d.ts +4 -0
  27. package/dist/ipc/inference_executor.d.ts.map +1 -0
  28. package/dist/ipc/inference_executor.js +1 -0
  29. package/dist/ipc/inference_executor.js.map +1 -0
  30. package/dist/ipc/inference_proc_executor.cjs +97 -0
  31. package/dist/ipc/inference_proc_executor.cjs.map +1 -0
  32. package/dist/ipc/inference_proc_executor.d.ts +23 -0
  33. package/dist/ipc/inference_proc_executor.d.ts.map +1 -0
  34. package/dist/ipc/inference_proc_executor.js +72 -0
  35. package/dist/ipc/inference_proc_executor.js.map +1 -0
  36. package/dist/ipc/inference_proc_lazy_main.cjs +92 -0
  37. package/dist/ipc/inference_proc_lazy_main.cjs.map +1 -0
  38. package/dist/ipc/inference_proc_lazy_main.d.ts +2 -0
  39. package/dist/ipc/inference_proc_lazy_main.d.ts.map +1 -0
  40. package/dist/ipc/inference_proc_lazy_main.js +69 -0
  41. package/dist/ipc/inference_proc_lazy_main.js.map +1 -0
  42. package/dist/ipc/job_executor.cjs +8 -7
  43. package/dist/ipc/job_executor.cjs.map +1 -1
  44. package/dist/ipc/job_executor.d.ts +14 -15
  45. package/dist/ipc/job_executor.d.ts.map +1 -1
  46. package/dist/ipc/job_executor.js +7 -6
  47. package/dist/ipc/job_executor.js.map +1 -1
  48. package/dist/ipc/job_proc_executor.cjs +108 -0
  49. package/dist/ipc/job_proc_executor.cjs.map +1 -0
  50. package/dist/ipc/job_proc_executor.d.ts +19 -0
  51. package/dist/ipc/job_proc_executor.d.ts.map +1 -0
  52. package/dist/ipc/job_proc_executor.js +83 -0
  53. package/dist/ipc/job_proc_executor.js.map +1 -0
  54. package/dist/ipc/{job_main.cjs → job_proc_lazy_main.cjs} +46 -36
  55. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -0
  56. package/dist/ipc/job_proc_lazy_main.d.ts +2 -0
  57. package/dist/ipc/job_proc_lazy_main.d.ts.map +1 -0
  58. package/dist/ipc/{job_main.js → job_proc_lazy_main.js} +46 -11
  59. package/dist/ipc/job_proc_lazy_main.js.map +1 -0
  60. package/dist/ipc/message.cjs.map +1 -1
  61. package/dist/ipc/message.d.ts +17 -0
  62. package/dist/ipc/message.d.ts.map +1 -1
  63. package/dist/ipc/proc_pool.cjs +30 -4
  64. package/dist/ipc/proc_pool.cjs.map +1 -1
  65. package/dist/ipc/proc_pool.d.ts +5 -1
  66. package/dist/ipc/proc_pool.d.ts.map +1 -1
  67. package/dist/ipc/proc_pool.js +30 -4
  68. package/dist/ipc/proc_pool.js.map +1 -1
  69. package/dist/ipc/{proc_job_executor.cjs → supervised_proc.cjs} +57 -45
  70. package/dist/ipc/supervised_proc.cjs.map +1 -0
  71. package/dist/ipc/supervised_proc.d.ts +30 -0
  72. package/dist/ipc/supervised_proc.d.ts.map +1 -0
  73. package/dist/ipc/{proc_job_executor.js → supervised_proc.js} +53 -31
  74. package/dist/ipc/supervised_proc.js.map +1 -0
  75. package/dist/job.cjs +18 -1
  76. package/dist/job.cjs.map +1 -1
  77. package/dist/job.d.ts +9 -1
  78. package/dist/job.d.ts.map +1 -1
  79. package/dist/job.js +17 -1
  80. package/dist/job.js.map +1 -1
  81. package/dist/multimodal/agent_playout.cjs +18 -16
  82. package/dist/multimodal/agent_playout.cjs.map +1 -1
  83. package/dist/multimodal/agent_playout.d.ts +4 -4
  84. package/dist/multimodal/agent_playout.d.ts.map +1 -1
  85. package/dist/multimodal/agent_playout.js +18 -16
  86. package/dist/multimodal/agent_playout.js.map +1 -1
  87. package/dist/multimodal/multimodal_agent.cjs +12 -8
  88. package/dist/multimodal/multimodal_agent.cjs.map +1 -1
  89. package/dist/multimodal/multimodal_agent.d.ts.map +1 -1
  90. package/dist/multimodal/multimodal_agent.js +13 -9
  91. package/dist/multimodal/multimodal_agent.js.map +1 -1
  92. package/dist/pipeline/agent_output.cjs +22 -4
  93. package/dist/pipeline/agent_output.cjs.map +1 -1
  94. package/dist/pipeline/agent_output.d.ts +4 -2
  95. package/dist/pipeline/agent_output.d.ts.map +1 -1
  96. package/dist/pipeline/agent_output.js +22 -4
  97. package/dist/pipeline/agent_output.js.map +1 -1
  98. package/dist/pipeline/agent_playout.cjs +9 -3
  99. package/dist/pipeline/agent_playout.cjs.map +1 -1
  100. package/dist/pipeline/agent_playout.d.ts +4 -2
  101. package/dist/pipeline/agent_playout.d.ts.map +1 -1
  102. package/dist/pipeline/agent_playout.js +9 -3
  103. package/dist/pipeline/agent_playout.js.map +1 -1
  104. package/dist/pipeline/human_input.cjs +6 -0
  105. package/dist/pipeline/human_input.cjs.map +1 -1
  106. package/dist/pipeline/human_input.d.ts +3 -1
  107. package/dist/pipeline/human_input.d.ts.map +1 -1
  108. package/dist/pipeline/human_input.js +6 -0
  109. package/dist/pipeline/human_input.js.map +1 -1
  110. package/dist/pipeline/pipeline_agent.cjs +79 -12
  111. package/dist/pipeline/pipeline_agent.cjs.map +1 -1
  112. package/dist/pipeline/pipeline_agent.d.ts +8 -0
  113. package/dist/pipeline/pipeline_agent.d.ts.map +1 -1
  114. package/dist/pipeline/pipeline_agent.js +79 -12
  115. package/dist/pipeline/pipeline_agent.js.map +1 -1
  116. package/dist/stt/stream_adapter.cjs +16 -4
  117. package/dist/stt/stream_adapter.cjs.map +1 -1
  118. package/dist/stt/stream_adapter.d.ts.map +1 -1
  119. package/dist/stt/stream_adapter.js +16 -4
  120. package/dist/stt/stream_adapter.js.map +1 -1
  121. package/dist/tokenize/basic/basic.cjs +2 -0
  122. package/dist/tokenize/basic/basic.cjs.map +1 -1
  123. package/dist/tokenize/basic/basic.d.ts +2 -0
  124. package/dist/tokenize/basic/basic.d.ts.map +1 -1
  125. package/dist/tokenize/basic/basic.js +1 -0
  126. package/dist/tokenize/basic/basic.js.map +1 -1
  127. package/dist/tokenize/basic/index.cjs +2 -0
  128. package/dist/tokenize/basic/index.cjs.map +1 -1
  129. package/dist/tokenize/basic/index.d.ts +1 -1
  130. package/dist/tokenize/basic/index.d.ts.map +1 -1
  131. package/dist/tokenize/basic/index.js +8 -1
  132. package/dist/tokenize/basic/index.js.map +1 -1
  133. package/dist/tokenize/token_stream.cjs +5 -3
  134. package/dist/tokenize/token_stream.cjs.map +1 -1
  135. package/dist/tokenize/token_stream.d.ts.map +1 -1
  136. package/dist/tokenize/token_stream.js +5 -3
  137. package/dist/tokenize/token_stream.js.map +1 -1
  138. package/dist/transcription.cjs +203 -86
  139. package/dist/transcription.cjs.map +1 -1
  140. package/dist/transcription.d.ts +24 -17
  141. package/dist/transcription.d.ts.map +1 -1
  142. package/dist/transcription.js +201 -85
  143. package/dist/transcription.js.map +1 -1
  144. package/dist/worker.cjs +42 -9
  145. package/dist/worker.cjs.map +1 -1
  146. package/dist/worker.d.ts +5 -1
  147. package/dist/worker.d.ts.map +1 -1
  148. package/dist/worker.js +42 -9
  149. package/dist/worker.js.map +1 -1
  150. package/package.json +3 -3
  151. package/src/cli.ts +9 -0
  152. package/src/index.ts +3 -1
  153. package/src/inference_runner.ts +19 -0
  154. package/src/ipc/index.ts +5 -0
  155. package/src/ipc/inference_executor.ts +7 -0
  156. package/src/ipc/inference_proc_executor.ts +93 -0
  157. package/src/ipc/inference_proc_lazy_main.ts +90 -0
  158. package/src/ipc/job_executor.ts +15 -17
  159. package/src/ipc/job_proc_executor.ts +112 -0
  160. package/src/ipc/{job_main.ts → job_proc_lazy_main.ts} +52 -14
  161. package/src/ipc/message.ts +14 -1
  162. package/src/ipc/proc_pool.ts +33 -3
  163. package/src/ipc/{proc_job_executor.ts → supervised_proc.ts} +77 -29
  164. package/src/job.ts +21 -0
  165. package/src/multimodal/agent_playout.ts +19 -18
  166. package/src/multimodal/multimodal_agent.ts +13 -9
  167. package/src/pipeline/agent_output.ts +36 -5
  168. package/src/pipeline/agent_playout.ts +10 -1
  169. package/src/pipeline/human_input.ts +8 -0
  170. package/src/pipeline/pipeline_agent.ts +96 -11
  171. package/src/stt/stream_adapter.ts +17 -5
  172. package/src/tokenize/basic/basic.ts +2 -0
  173. package/src/tokenize/basic/index.ts +7 -1
  174. package/src/tokenize/token_stream.ts +6 -3
  175. package/src/transcription.ts +270 -96
  176. package/src/worker.ts +42 -5
  177. package/dist/ipc/job_main.cjs.map +0 -1
  178. package/dist/ipc/job_main.d.ts +0 -8
  179. package/dist/ipc/job_main.d.ts.map +0 -1
  180. package/dist/ipc/job_main.js.map +0 -1
  181. package/dist/ipc/proc_job_executor.cjs.map +0 -1
  182. package/dist/ipc/proc_job_executor.d.ts +0 -15
  183. package/dist/ipc/proc_job_executor.d.ts.map +0 -1
  184. package/dist/ipc/proc_job_executor.js.map +0 -1
@@ -0,0 +1,112 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { ChildProcess } from 'node:child_process';
5
+ import { fork } from 'node:child_process';
6
+ import type { RunningJobInfo } from '../job.js';
7
+ import { log } from '../log.js';
8
+ import type { InferenceExecutor } from './inference_executor.js';
9
+ import type { JobExecutor } from './job_executor.js';
10
+ import { JobStatus } from './job_executor.js';
11
+ import type { IPCMessage } from './message.js';
12
+ import { SupervisedProc } from './supervised_proc.js';
13
+
14
+ export class JobProcExecutor extends SupervisedProc implements JobExecutor {
15
+ #userArgs?: any;
16
+ #jobStatus?: JobStatus;
17
+ #runningJob?: RunningJobInfo;
18
+ #agent: string;
19
+ #inferenceExecutor?: InferenceExecutor;
20
+ #inferenceTasks: Promise<void>[] = [];
21
+ #logger = log();
22
+
23
+ constructor(
24
+ agent: string,
25
+ inferenceExecutor: InferenceExecutor | undefined,
26
+ initializeTimeout: number,
27
+ closeTimeout: number,
28
+ memoryWarnMB: number,
29
+ memoryLimitMB: number,
30
+ pingInterval: number,
31
+ pingTimeout: number,
32
+ highPingThreshold: number,
33
+ ) {
34
+ super(
35
+ initializeTimeout,
36
+ closeTimeout,
37
+ memoryWarnMB,
38
+ memoryLimitMB,
39
+ pingInterval,
40
+ pingTimeout,
41
+ highPingThreshold,
42
+ );
43
+ this.#agent = agent;
44
+ this.#inferenceExecutor = inferenceExecutor;
45
+ }
46
+
47
+ get status(): JobStatus {
48
+ if (this.#jobStatus) {
49
+ return this.#jobStatus;
50
+ }
51
+ throw new Error('job status not available');
52
+ }
53
+
54
+ get userArguments(): any {
55
+ return this.#userArgs;
56
+ }
57
+
58
+ set userArguments(args: any) {
59
+ this.#userArgs = args;
60
+ }
61
+
62
+ get runningJob(): RunningJobInfo | undefined {
63
+ return this.#runningJob;
64
+ }
65
+
66
+ createProcess(): ChildProcess {
67
+ return fork(new URL(import.meta.resolve('./job_proc_lazy_main.js')), [this.#agent]);
68
+ }
69
+
70
+ async mainTask(proc: ChildProcess) {
71
+ proc.on('message', (msg: IPCMessage) => {
72
+ switch (msg.case) {
73
+ case 'inferenceRequest':
74
+ this.#inferenceTasks.push(this.#doInferenceTask(proc, msg.value));
75
+ }
76
+ });
77
+ }
78
+
79
+ async #doInferenceTask(
80
+ proc: ChildProcess,
81
+ req: { method: string; requestId: string; data: unknown },
82
+ ) {
83
+ if (!this.#inferenceExecutor) {
84
+ this.#logger.warn('inference request received but no inference executor');
85
+ proc.send({
86
+ case: 'inferenceResponse',
87
+ value: { requestId: req.requestId, error: new Error('no inference executor') },
88
+ });
89
+ return;
90
+ }
91
+
92
+ try {
93
+ const data = await this.#inferenceExecutor.doInference(req.method, req.data);
94
+ proc.send({ case: 'inferenceResponse', value: { requestId: req.requestId, data } });
95
+ } catch (error) {
96
+ proc.send({ case: 'inferenceResponse', value: { requestId: req.requestId, error } });
97
+ }
98
+ }
99
+
100
+ async launchJob(info: RunningJobInfo) {
101
+ if (this.#runningJob) {
102
+ throw Error('process already has a running job');
103
+ }
104
+ if (!this.init.done) {
105
+ throw Error('process not initialized');
106
+ }
107
+ this.#jobStatus = JobStatus.RUNNING;
108
+ this.#runningJob = info;
109
+
110
+ this.proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
111
+ }
112
+ }
@@ -2,34 +2,63 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { Room, RoomEvent } from '@livekit/rtc-node';
5
- import type { ChildProcess } from 'node:child_process';
6
- import { fork } from 'node:child_process';
5
+ import { randomUUID } from 'node:crypto';
7
6
  import { EventEmitter, once } from 'node:events';
8
7
  import { pathToFileURL } from 'node:url';
9
8
  import type { Logger } from 'pino';
10
9
  import { type Agent, isAgent } from '../generator.js';
11
- import type { RunningJobInfo } from '../job.js';
12
- import { JobContext } from '../job.js';
13
- import { JobProcess } from '../job.js';
10
+ import { CurrentJobContext, JobContext, JobProcess, type RunningJobInfo } from '../job.js';
14
11
  import { initializeLogger, log } from '../log.js';
15
12
  import { defaultInitializeProcessFunc } from '../worker.js';
13
+ import type { InferenceExecutor } from './inference_executor.js';
16
14
  import type { IPCMessage } from './message.js';
17
15
 
18
16
  const ORPHANED_TIMEOUT = 15 * 1000;
19
17
 
20
- type StartArgs = {
21
- agentFile: string;
22
- // userArguments: unknown;
23
- };
24
-
25
18
  type JobTask = {
26
19
  ctx: JobContext;
27
20
  task: Promise<void>;
28
21
  };
29
22
 
30
- export const runProcess = (args: StartArgs): ChildProcess => {
31
- return fork(new URL(import.meta.url), [args.agentFile]);
32
- };
23
+ class PendingInference {
24
+ promise = new Promise<{ requestId: string; data: unknown; error?: Error }>((resolve) => {
25
+ this.resolve = resolve; // this is how JavaScript lets you resolve promises externally
26
+ });
27
+ resolve(arg: { requestId: string; data: unknown; error?: Error }) {
28
+ arg; // useless call to counteract TypeScript E6133
29
+ }
30
+ }
31
+
32
+ class InfClient implements InferenceExecutor {
33
+ #requests: { [id: string]: PendingInference } = {};
34
+
35
+ constructor() {
36
+ process.on('message', (msg: IPCMessage) => {
37
+ switch (msg.case) {
38
+ case 'inferenceResponse':
39
+ const fut = this.#requests[msg.value.requestId];
40
+ delete this.#requests[msg.value.requestId];
41
+ if (!fut) {
42
+ log().child({ resp: msg.value }).warn('received unexpected inference response');
43
+ return;
44
+ }
45
+ fut.resolve(msg.value);
46
+ break;
47
+ }
48
+ });
49
+ }
50
+
51
+ async doInference(method: string, data: unknown): Promise<unknown> {
52
+ const requestId = 'inference_job_' + randomUUID;
53
+ process.send!({ case: 'inferenceRequest', value: { requestId, method, data } });
54
+ this.#requests[requestId] = new PendingInference();
55
+ const resp = await this.#requests[requestId]!.promise;
56
+ if (resp.error) {
57
+ throw new Error(`inference of ${method} failed: ${resp.error.message}`);
58
+ }
59
+ return resp.data;
60
+ }
61
+ }
33
62
 
34
63
  const startJob = (
35
64
  proc: JobProcess,
@@ -54,7 +83,8 @@ const startJob = (
54
83
  closeEvent.emit('close', true, reason);
55
84
  };
56
85
 
57
- const ctx = new JobContext(proc, info, room, onConnect, onShutdown);
86
+ const ctx = new JobContext(proc, info, room, onConnect, onShutdown, new InfClient());
87
+ new CurrentJobContext(ctx);
58
88
 
59
89
  const task = new Promise<void>(async () => {
60
90
  const unconnectedTimeout = setTimeout(() => {
@@ -110,6 +140,10 @@ const startJob = (
110
140
  // this is handled in cli, triggering a termination of all child processes at once.
111
141
  process.on('SIGINT', () => {});
112
142
 
143
+ // don't do anything on SIGTERM
144
+ // Render uses SIGTERM in autoscale, this ensures the processes are properly drained if needed
145
+ process.on('SIGTERM', () => {});
146
+
113
147
  await once(process, 'message').then(([msg]: IPCMessage[]) => {
114
148
  msg = msg!;
115
149
  if (msg.case !== 'initializeRequest') {
@@ -120,6 +154,10 @@ const startJob = (
120
154
  const proc = new JobProcess();
121
155
  let logger = log().child({ pid: proc.pid });
122
156
 
157
+ process.on('unhandledRejection', (reason) => {
158
+ logger.error(reason);
159
+ });
160
+
123
161
  logger.debug('initializing job runner');
124
162
  agent.prewarm(proc);
125
163
  logger.debug('job runner initialized');
@@ -7,7 +7,12 @@ import type { LoggerOptions } from '../log.js';
7
7
  export type IPCMessage =
8
8
  | {
9
9
  case: 'initializeRequest';
10
- value: { loggerOptions: LoggerOptions };
10
+ value: {
11
+ loggerOptions: LoggerOptions;
12
+ pingInterval?: number;
13
+ pingTimeout?: number;
14
+ highPingThreshold?: number;
15
+ };
11
16
  }
12
17
  | {
13
18
  case: 'initializeResponse';
@@ -29,6 +34,14 @@ export type IPCMessage =
29
34
  case: 'shutdownRequest';
30
35
  value: { reason?: string };
31
36
  }
37
+ | {
38
+ case: 'inferenceRequest';
39
+ value: { method: string; requestId: string; data: unknown };
40
+ }
41
+ | {
42
+ case: 'inferenceResponse';
43
+ value: { requestId: string; data: unknown; error?: Error };
44
+ }
32
45
  | {
33
46
  case: 'exiting';
34
47
  value: { reason?: string };
@@ -4,8 +4,9 @@
4
4
  import { MultiMutex, Mutex } from '@livekit/mutex';
5
5
  import type { RunningJobInfo } from '../job.js';
6
6
  import { Queue } from '../utils.js';
7
+ import type { InferenceExecutor } from './inference_executor.js';
7
8
  import type { JobExecutor } from './job_executor.js';
8
- import { ProcJobExecutor } from './proc_job_executor.js';
9
+ import { JobProcExecutor } from './job_proc_executor.js';
9
10
 
10
11
  export class ProcPool {
11
12
  agent: string;
@@ -20,12 +21,18 @@ export class ProcPool {
20
21
  procMutex?: MultiMutex;
21
22
  procUnlock?: () => void;
22
23
  warmedProcQueue = new Queue<JobExecutor>();
24
+ inferenceExecutor?: InferenceExecutor;
25
+ memoryWarnMB: number;
26
+ memoryLimitMB: number;
23
27
 
24
28
  constructor(
25
29
  agent: string,
26
30
  numIdleProcesses: number,
27
31
  initializeTimeout: number,
28
32
  closeTimeout: number,
33
+ inferenceExecutor: InferenceExecutor | undefined,
34
+ memoryWarnMB: number,
35
+ memoryLimitMB: number,
29
36
  ) {
30
37
  this.agent = agent;
31
38
  if (numIdleProcesses > 0) {
@@ -33,6 +40,9 @@ export class ProcPool {
33
40
  }
34
41
  this.initializeTimeout = initializeTimeout;
35
42
  this.closeTimeout = closeTimeout;
43
+ this.inferenceExecutor = inferenceExecutor;
44
+ this.memoryWarnMB = memoryWarnMB;
45
+ this.memoryLimitMB = memoryLimitMB;
36
46
  }
37
47
 
38
48
  get processes(): JobExecutor[] {
@@ -52,7 +62,17 @@ export class ProcPool {
52
62
  this.procUnlock = undefined;
53
63
  }
54
64
  } else {
55
- proc = new ProcJobExecutor(this.agent, this.initializeTimeout, this.closeTimeout);
65
+ proc = new JobProcExecutor(
66
+ this.agent,
67
+ this.inferenceExecutor,
68
+ this.initializeTimeout,
69
+ this.closeTimeout,
70
+ this.memoryWarnMB,
71
+ this.memoryLimitMB,
72
+ 2500,
73
+ 60000,
74
+ 500,
75
+ );
56
76
  this.executors.push(proc);
57
77
  await proc.start();
58
78
  await proc.initialize();
@@ -61,7 +81,17 @@ export class ProcPool {
61
81
  }
62
82
 
63
83
  async procWatchTask() {
64
- const proc = new ProcJobExecutor(this.agent, this.initializeTimeout, this.closeTimeout);
84
+ const proc = new JobProcExecutor(
85
+ this.agent,
86
+ this.inferenceExecutor,
87
+ this.initializeTimeout,
88
+ this.closeTimeout,
89
+ this.memoryWarnMB,
90
+ this.memoryLimitMB,
91
+ 2500,
92
+ 60000,
93
+ 500,
94
+ );
65
95
 
66
96
  try {
67
97
  this.executors.push(proc);
@@ -6,31 +6,54 @@ import { once } from 'node:events';
6
6
  import type { RunningJobInfo } from '../job.js';
7
7
  import { log, loggerOptions } from '../log.js';
8
8
  import { Future } from '../utils.js';
9
- import type { ProcOpts } from './job_executor.js';
10
- import { JobExecutor } from './job_executor.js';
11
9
  import type { IPCMessage } from './message.js';
12
10
 
13
- export class ProcJobExecutor extends JobExecutor {
11
+ export interface ProcOpts {
12
+ initializeTimeout: number;
13
+ closeTimeout: number;
14
+ memoryWarnMB: number;
15
+ memoryLimitMB: number;
16
+ pingInterval: number;
17
+ pingTimeout: number;
18
+ highPingThreshold: number;
19
+ }
20
+
21
+ export abstract class SupervisedProc {
14
22
  #opts: ProcOpts;
15
23
  #started = false;
16
24
  #closing = false;
17
25
  #runningJob?: RunningJobInfo = undefined;
18
- #proc?: ChildProcess;
26
+ proc?: ChildProcess;
19
27
  #pingInterval?: ReturnType<typeof setInterval>;
28
+ #memoryWatch?: ReturnType<typeof setInterval>;
20
29
  #pongTimeout?: ReturnType<typeof setTimeout>;
21
- #init = new Future();
30
+ protected init = new Future();
22
31
  #join = new Future();
23
32
  #logger = log().child({ runningJob: this.#runningJob });
24
33
 
25
- constructor(agent: string, initializeTimeout: number, closeTimeout: number) {
26
- super();
34
+ constructor(
35
+ initializeTimeout: number,
36
+ closeTimeout: number,
37
+ memoryWarnMB: number,
38
+ memoryLimitMB: number,
39
+ pingInterval: number,
40
+ pingTimeout: number,
41
+ highPingThreshold: number,
42
+ ) {
27
43
  this.#opts = {
28
- agent,
29
44
  initializeTimeout,
30
45
  closeTimeout,
46
+ memoryWarnMB,
47
+ memoryLimitMB,
48
+ pingInterval,
49
+ pingTimeout,
50
+ highPingThreshold,
31
51
  };
32
52
  }
33
53
 
54
+ abstract createProcess(): ChildProcess;
55
+ abstract mainTask(child: ChildProcess): Promise<void>;
56
+
34
57
  get started(): boolean {
35
58
  return this.#started;
36
59
  }
@@ -46,36 +69,50 @@ export class ProcJobExecutor extends JobExecutor {
46
69
  throw new Error('runner is closed');
47
70
  }
48
71
 
49
- this.#proc = await import('./job_main.js').then((m) =>
50
- m.runProcess({
51
- agentFile: this.#opts.agent,
52
- }),
53
- );
72
+ this.proc = this.createProcess();
54
73
 
55
74
  this.#started = true;
56
75
  this.run();
57
76
  }
58
77
 
59
78
  async run() {
60
- await this.#init.await;
79
+ await this.init.await;
61
80
 
62
81
  this.#pingInterval = setInterval(() => {
63
- this.#proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });
64
- }, this.PING_INTERVAL);
82
+ this.proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });
83
+ }, this.#opts.pingInterval);
65
84
 
66
85
  this.#pongTimeout = setTimeout(() => {
67
86
  this.#logger.warn('job is unresponsive');
68
87
  clearTimeout(this.#pongTimeout);
69
88
  clearInterval(this.#pingInterval);
70
- this.#proc!.kill();
89
+ this.proc!.kill();
71
90
  this.#join.resolve();
72
- }, this.PING_TIMEOUT);
91
+ }, this.#opts.pingTimeout);
92
+
93
+ this.#memoryWatch = setInterval(() => {
94
+ const memoryMB = process.memoryUsage().heapUsed / (1024 * 1024);
95
+ if (this.#opts.memoryLimitMB > 0 && memoryMB > this.#opts.memoryLimitMB) {
96
+ this.#logger
97
+ .child({ memoryUsageMB: memoryMB, memoryLimitMB: this.#opts.memoryLimitMB })
98
+ .error('process exceeded memory limit, killing process');
99
+ this.close();
100
+ } else if (this.#opts.memoryWarnMB > 0 && memoryMB > this.#opts.memoryWarnMB) {
101
+ this.#logger
102
+ .child({
103
+ memoryUsageMB: memoryMB,
104
+ memoryWarnMB: this.#opts.memoryWarnMB,
105
+ memoryLimitMB: this.#opts.memoryLimitMB,
106
+ })
107
+ .error('process memory usage is high');
108
+ }
109
+ });
73
110
 
74
111
  const listener = (msg: IPCMessage) => {
75
112
  switch (msg.case) {
76
113
  case 'pongResponse': {
77
114
  const delay = Date.now() - msg.value.timestamp;
78
- if (delay > this.HIGH_PING_THRESHOLD) {
115
+ if (delay > this.#opts.highPingThreshold) {
79
116
  this.#logger.child({ delay }).warn('job executor is unresponsive');
80
117
  }
81
118
  this.#pongTimeout?.refresh();
@@ -87,23 +124,26 @@ export class ProcJobExecutor extends JobExecutor {
87
124
  }
88
125
  case 'done': {
89
126
  this.#closing = true;
90
- this.#proc!.off('message', listener);
127
+ this.proc!.off('message', listener);
91
128
  this.#join.resolve();
92
129
  break;
93
130
  }
94
131
  }
95
132
  };
96
- this.#proc!.on('message', listener);
97
- this.#proc!.on('error', (err) => {
133
+ this.proc!.on('message', listener);
134
+ this.proc!.on('error', (err) => {
98
135
  if (this.#closing) return;
99
136
  this.#logger
100
137
  .child({ err })
101
138
  .warn('job process exited unexpectedly; this likely means the error above caused a crash');
102
139
  clearTimeout(this.#pongTimeout);
103
140
  clearInterval(this.#pingInterval);
141
+ clearInterval(this.#memoryWatch);
104
142
  this.#join.resolve();
105
143
  });
106
144
 
145
+ this.mainTask(this.proc!);
146
+
107
147
  await this.#join.await;
108
148
  }
109
149
 
@@ -118,17 +158,25 @@ export class ProcJobExecutor extends JobExecutor {
118
158
  async initialize() {
119
159
  const timer = setTimeout(() => {
120
160
  const err = new Error('runner initialization timed out');
121
- this.#init.reject(err);
161
+ this.init.reject(err);
122
162
  throw err;
123
163
  }, this.#opts.initializeTimeout);
124
- this.#proc!.send({ case: 'initializeRequest', value: { loggerOptions } });
125
- await once(this.#proc!, 'message').then(([msg]: IPCMessage[]) => {
164
+ this.proc!.send({
165
+ case: 'initializeRequest',
166
+ value: {
167
+ loggerOptions,
168
+ pingInterval: this.#opts.pingInterval,
169
+ pingTimeout: this.#opts.pingTimeout,
170
+ highPingThreshold: this.#opts.highPingThreshold,
171
+ },
172
+ });
173
+ await once(this.proc!, 'message').then(([msg]: IPCMessage[]) => {
126
174
  clearTimeout(timer);
127
175
  if (msg!.case !== 'initializeResponse') {
128
176
  throw new Error('first message must be InitializeResponse');
129
177
  }
130
178
  });
131
- this.#init.resolve();
179
+ this.init.resolve();
132
180
  }
133
181
 
134
182
  async close() {
@@ -138,11 +186,11 @@ export class ProcJobExecutor extends JobExecutor {
138
186
  this.#closing = true;
139
187
 
140
188
  if (!this.#runningJob) {
141
- this.#proc!.kill();
189
+ this.proc!.kill();
142
190
  this.#join.resolve();
143
191
  }
144
192
 
145
- this.#proc!.send({ case: 'shutdownRequest' });
193
+ this.proc!.send({ case: 'shutdownRequest' });
146
194
 
147
195
  const timer = setTimeout(() => {
148
196
  this.#logger.error('job shutdown is taking too much time');
@@ -159,6 +207,6 @@ export class ProcJobExecutor extends JobExecutor {
159
207
  throw new Error('executor already has a running job');
160
208
  }
161
209
  this.#runningJob = info;
162
- this.#proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
210
+ this.proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
163
211
  }
164
212
  }
package/src/job.ts CHANGED
@@ -11,8 +11,21 @@ import type {
11
11
  } from '@livekit/rtc-node';
12
12
  import { ParticipantKind, RoomEvent, TrackKind } from '@livekit/rtc-node';
13
13
  import type { Logger } from 'pino';
14
+ import type { InferenceExecutor } from './ipc/inference_executor.js';
14
15
  import { log } from './log.js';
15
16
 
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
+ }
28
+
16
29
  /** Which tracks, if any, should the agent automatically subscribe to? */
17
30
  export enum AutoSubscribe {
18
31
  SUBSCRIBE_ALL,
@@ -60,6 +73,7 @@ export class JobContext {
60
73
  };
61
74
  } = {};
62
75
  #logger: Logger;
76
+ #inferenceExecutor: InferenceExecutor;
63
77
 
64
78
  constructor(
65
79
  proc: JobProcess,
@@ -67,6 +81,7 @@ export class JobContext {
67
81
  room: Room,
68
82
  onConnect: () => void,
69
83
  onShutdown: (s: string) => void,
84
+ inferenceExecutor: InferenceExecutor,
70
85
  ) {
71
86
  this.#proc = proc;
72
87
  this.#info = info;
@@ -76,6 +91,7 @@ export class JobContext {
76
91
  this.onParticipantConnected = this.onParticipantConnected.bind(this);
77
92
  this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
78
93
  this.#logger = log().child({ info: this.#info });
94
+ this.#inferenceExecutor = inferenceExecutor;
79
95
  }
80
96
 
81
97
  get proc(): JobProcess {
@@ -96,6 +112,11 @@ export class JobContext {
96
112
  return this.#room.localParticipant;
97
113
  }
98
114
 
115
+ /** @returns The global inference executor */
116
+ get inferenceExecutor(): InferenceExecutor {
117
+ return this.#inferenceExecutor;
118
+ }
119
+
99
120
  /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
100
121
  addShutdownCallback(callback: () => Promise<void>) {
101
122
  this.shutdownCallbacks.push(callback);