@livekit/agents 0.1.0 → 0.3.0

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 (155) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +201 -0
  4. package/dist/audio.d.ts +9 -0
  5. package/dist/audio.d.ts.map +1 -0
  6. package/dist/audio.js +54 -0
  7. package/dist/audio.js.map +1 -0
  8. package/dist/cli.d.ts +12 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +102 -19
  11. package/dist/cli.js.map +1 -1
  12. package/dist/generator.d.ts +17 -6
  13. package/dist/generator.d.ts.map +1 -1
  14. package/dist/generator.js +20 -3
  15. package/dist/generator.js.map +1 -1
  16. package/dist/http_server.d.ts +1 -1
  17. package/dist/http_server.d.ts.map +1 -1
  18. package/dist/http_server.js +5 -3
  19. package/dist/http_server.js.map +1 -1
  20. package/dist/index.d.ts +14 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +14 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/ipc/job_executor.d.ts +19 -0
  25. package/dist/ipc/job_executor.d.ts.map +1 -0
  26. package/dist/ipc/job_executor.js +8 -0
  27. package/dist/ipc/job_executor.js.map +1 -0
  28. package/dist/ipc/job_main.d.ts +7 -4
  29. package/dist/ipc/job_main.d.ts.map +1 -1
  30. package/dist/ipc/job_main.js +102 -59
  31. package/dist/ipc/job_main.js.map +1 -1
  32. package/dist/ipc/message.d.ts +41 -0
  33. package/dist/ipc/message.d.ts.map +1 -0
  34. package/dist/ipc/message.js +2 -0
  35. package/dist/ipc/message.js.map +1 -0
  36. package/dist/ipc/proc_job_executor.d.ts +15 -0
  37. package/dist/ipc/proc_job_executor.d.ts.map +1 -0
  38. package/dist/ipc/proc_job_executor.js +150 -0
  39. package/dist/ipc/proc_job_executor.js.map +1 -0
  40. package/dist/ipc/proc_pool.d.ts +26 -0
  41. package/dist/ipc/proc_pool.d.ts.map +1 -0
  42. package/dist/ipc/proc_pool.js +83 -0
  43. package/dist/ipc/proc_pool.js.map +1 -0
  44. package/dist/job.d.ts +100 -0
  45. package/dist/job.d.ts.map +1 -0
  46. package/dist/job.js +213 -0
  47. package/dist/job.js.map +1 -0
  48. package/dist/llm/function_context.d.ts +20 -0
  49. package/dist/llm/function_context.d.ts.map +1 -0
  50. package/dist/llm/function_context.js +37 -0
  51. package/dist/llm/function_context.js.map +1 -0
  52. package/dist/llm/index.d.ts +3 -0
  53. package/dist/llm/index.d.ts.map +1 -0
  54. package/dist/llm/index.js +6 -0
  55. package/dist/llm/index.js.map +1 -0
  56. package/dist/log.d.ts +12 -1
  57. package/dist/log.d.ts.map +1 -1
  58. package/dist/log.js +28 -11
  59. package/dist/log.js.map +1 -1
  60. package/dist/multimodal/agent_playout.d.ts +34 -0
  61. package/dist/multimodal/agent_playout.d.ts.map +1 -0
  62. package/dist/multimodal/agent_playout.js +221 -0
  63. package/dist/multimodal/agent_playout.js.map +1 -0
  64. package/dist/multimodal/index.d.ts +3 -0
  65. package/dist/multimodal/index.d.ts.map +1 -0
  66. package/dist/multimodal/index.js +6 -0
  67. package/dist/multimodal/index.js.map +1 -0
  68. package/dist/multimodal/multimodal_agent.d.ts +47 -0
  69. package/dist/multimodal/multimodal_agent.d.ts.map +1 -0
  70. package/dist/multimodal/multimodal_agent.js +331 -0
  71. package/dist/multimodal/multimodal_agent.js.map +1 -0
  72. package/dist/plugin.js +20 -7
  73. package/dist/plugin.js.map +1 -1
  74. package/dist/stt/index.d.ts +1 -1
  75. package/dist/stt/index.d.ts.map +1 -1
  76. package/dist/stt/index.js.map +1 -1
  77. package/dist/stt/stream_adapter.d.ts +2 -11
  78. package/dist/stt/stream_adapter.d.ts.map +1 -1
  79. package/dist/stt/stream_adapter.js +47 -33
  80. package/dist/stt/stream_adapter.js.map +1 -1
  81. package/dist/stt/stt.d.ts +27 -0
  82. package/dist/stt/stt.d.ts.map +1 -1
  83. package/dist/stt/stt.js +32 -5
  84. package/dist/stt/stt.js.map +1 -1
  85. package/dist/transcription.d.ts +22 -0
  86. package/dist/transcription.d.ts.map +1 -0
  87. package/dist/transcription.js +111 -0
  88. package/dist/transcription.js.map +1 -0
  89. package/dist/tts/stream_adapter.d.ts +4 -11
  90. package/dist/tts/stream_adapter.d.ts.map +1 -1
  91. package/dist/tts/stream_adapter.js +66 -32
  92. package/dist/tts/stream_adapter.js.map +1 -1
  93. package/dist/tts/tts.d.ts +10 -0
  94. package/dist/tts/tts.d.ts.map +1 -1
  95. package/dist/tts/tts.js +48 -7
  96. package/dist/tts/tts.js.map +1 -1
  97. package/dist/utils.d.ts +59 -0
  98. package/dist/utils.d.ts.map +1 -1
  99. package/dist/utils.js +212 -6
  100. package/dist/utils.js.map +1 -1
  101. package/dist/vad.d.ts +29 -0
  102. package/dist/vad.d.ts.map +1 -1
  103. package/dist/vad.js.map +1 -1
  104. package/dist/worker.d.ts +69 -50
  105. package/dist/worker.d.ts.map +1 -1
  106. package/dist/worker.js +414 -213
  107. package/dist/worker.js.map +1 -1
  108. package/package.json +12 -10
  109. package/src/audio.ts +62 -0
  110. package/src/cli.ts +108 -20
  111. package/src/generator.ts +27 -7
  112. package/src/http_server.ts +5 -0
  113. package/src/index.ts +15 -3
  114. package/src/ipc/job_executor.ts +25 -0
  115. package/src/ipc/job_main.ts +141 -61
  116. package/src/ipc/message.ts +39 -0
  117. package/src/ipc/proc_job_executor.ts +162 -0
  118. package/src/ipc/proc_pool.ts +109 -0
  119. package/src/job.ts +278 -0
  120. package/src/llm/function_context.ts +61 -0
  121. package/src/llm/index.ts +11 -0
  122. package/src/log.ts +40 -8
  123. package/src/multimodal/agent_playout.ts +254 -0
  124. package/src/multimodal/index.ts +5 -0
  125. package/src/multimodal/multimodal_agent.ts +428 -0
  126. package/src/stt/index.ts +1 -1
  127. package/src/stt/stream_adapter.ts +32 -32
  128. package/src/stt/stt.ts +27 -0
  129. package/src/transcription.ts +128 -0
  130. package/src/tts/stream_adapter.ts +32 -31
  131. package/src/tts/tts.ts +10 -0
  132. package/src/utils.ts +257 -3
  133. package/src/vad.ts +29 -0
  134. package/src/worker.ts +465 -172
  135. package/tsconfig.json +7 -1
  136. package/dist/ipc/job_process.d.ts +0 -22
  137. package/dist/ipc/job_process.d.ts.map +0 -1
  138. package/dist/ipc/job_process.js +0 -73
  139. package/dist/ipc/job_process.js.map +0 -1
  140. package/dist/ipc/protocol.d.ts +0 -40
  141. package/dist/ipc/protocol.d.ts.map +0 -1
  142. package/dist/ipc/protocol.js +0 -14
  143. package/dist/ipc/protocol.js.map +0 -1
  144. package/dist/job_context.d.ts +0 -16
  145. package/dist/job_context.d.ts.map +0 -1
  146. package/dist/job_context.js +0 -31
  147. package/dist/job_context.js.map +0 -1
  148. package/dist/job_request.d.ts +0 -42
  149. package/dist/job_request.d.ts.map +0 -1
  150. package/dist/job_request.js +0 -79
  151. package/dist/job_request.js.map +0 -1
  152. package/src/ipc/job_process.ts +0 -96
  153. package/src/ipc/protocol.ts +0 -51
  154. package/src/job_context.ts +0 -49
  155. package/src/job_request.ts +0 -118
@@ -1,83 +1,163 @@
1
1
  // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
- import { type JobAssignment, ServerMessage } from '@livekit/protocol';
5
- import { Room } from '@livekit/rtc-node';
6
- import { type ChildProcess, fork } from 'child_process';
4
+ import { Room, RoomEvent } from '@livekit/rtc-node';
5
+ import type { ChildProcess } from 'child_process';
6
+ import { fork } from 'child_process';
7
7
  import { EventEmitter, once } from 'events';
8
- import { JobContext } from '../job_context.js';
9
- import { log } from '../log.js';
10
- import { IPC_MESSAGE, type JobMainArgs, type Message, type Ping } from './protocol.js';
8
+ import type { Logger } from 'pino';
9
+ import { fileURLToPath } from 'url';
10
+ 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';
14
+ import { initializeLogger, log } from '../log.js';
15
+ import { defaultInitializeProcessFunc } from '../worker.js';
16
+ import type { IPCMessage } from './message.js';
11
17
 
12
- export const runJob = (args: JobMainArgs): ChildProcess => {
13
- return fork(import.meta.filename, [args.raw, args.entry, args.fallbackURL]);
18
+ const ORPHANED_TIMEOUT = 15 * 1000;
19
+
20
+ type StartArgs = {
21
+ agentFile: string;
22
+ // userArguments: unknown;
23
+ };
24
+
25
+ type JobTask = {
26
+ ctx: JobContext;
27
+ task: Promise<void>;
28
+ };
29
+
30
+ export const runProcess = (args: StartArgs): ChildProcess => {
31
+ return fork(fileURLToPath(import.meta.url), [args.agentFile]);
32
+ };
33
+
34
+ const startJob = (
35
+ proc: JobProcess,
36
+ func: (ctx: JobContext) => Promise<void>,
37
+ info: RunningJobInfo,
38
+ closeEvent: EventEmitter,
39
+ logger: Logger,
40
+ ): JobTask => {
41
+ let connect = false;
42
+ let shutdown = false;
43
+
44
+ const room = new Room();
45
+ room.on(RoomEvent.Disconnected, () => {
46
+ closeEvent.emit('close', false);
47
+ });
48
+
49
+ const onConnect = () => {
50
+ connect = true;
51
+ };
52
+ const onShutdown = (reason: string) => {
53
+ shutdown = true;
54
+ closeEvent.emit('close', true, reason);
55
+ };
56
+
57
+ const ctx = new JobContext(proc, info, room, onConnect, onShutdown);
58
+
59
+ const task = new Promise<void>(async () => {
60
+ const unconnectedTimeout = setTimeout(() => {
61
+ if (!(connect || shutdown)) {
62
+ logger.warn(
63
+ 'room not connect after job_entry was called after 10 seconds, ',
64
+ 'did you forget to call ctx.connect()?',
65
+ );
66
+ }
67
+ }, 10000);
68
+ func(ctx).finally(() => clearTimeout(unconnectedTimeout));
69
+
70
+ await once(closeEvent, 'close').then((close) => {
71
+ logger.debug('shutting down');
72
+ process.send!({ case: 'exiting', value: { reason: close[1] } });
73
+ });
74
+
75
+ await room.disconnect();
76
+ logger.debug('disconnected from room');
77
+
78
+ const shutdownTasks = [];
79
+ for (const callback of ctx.shutdownCallbacks) {
80
+ shutdownTasks.push(callback());
81
+ }
82
+ await Promise.all(shutdownTasks).catch(() => logger.error('error while shutting down the job'));
83
+
84
+ process.send!({ case: 'done' });
85
+ process.exit();
86
+ });
87
+
88
+ return { ctx, task };
14
89
  };
15
90
 
16
91
  if (process.send) {
17
92
  // process.argv:
18
93
  // [0] `node'
19
94
  // [1] import.meta.filename
20
- // [2] proto.JobAssignment, serialized to JSON string
21
- // [3] import.meta.filename of function containing entry file
22
- // [4] fallback URL in case JobAssignment.url is empty
23
-
24
- const msg = new ServerMessage();
25
- msg.fromJsonString(process.argv[2]);
26
- const args = msg.message.value as JobAssignment;
27
-
28
- const room = new Room();
29
- const closeEvent = new EventEmitter();
30
- let shuttingDown = false;
31
- let closed = false;
32
-
33
- process.on('message', (msg: Message) => {
34
- if (msg.type === IPC_MESSAGE.ShutdownRequest) {
35
- shuttingDown = true;
36
- closed = true;
37
- closeEvent.emit('close');
38
- } else if (msg.type === IPC_MESSAGE.Ping) {
39
- process.send!({
40
- type: IPC_MESSAGE.Pong,
41
- lastTimestamp: (msg as Ping).timestamp,
42
- timestamp: Date.now(),
43
- });
95
+ // [2] import.meta.filename of function containing entry file
96
+ const moduleFile = process.argv[2];
97
+ const agent: Agent = await import(moduleFile).then((module) => {
98
+ const agent = module.default;
99
+ if (agent === undefined || !isAgent(agent)) {
100
+ throw new Error(`Unable to load agent: Missing or invalid default export in ${moduleFile}`);
44
101
  }
102
+ return agent;
45
103
  });
104
+ if (!agent.prewarm) {
105
+ agent.prewarm = defaultInitializeProcessFunc;
106
+ }
46
107
 
47
108
  // don't do anything on C-c
109
+ // this is handled in cli, triggering a termination of all child processes at once.
48
110
  process.on('SIGINT', () => {});
49
111
 
50
- const conn = room.connect(args.url || process.argv[4], args.token);
112
+ await once(process, 'message').then(([msg]: IPCMessage[]) => {
113
+ if (msg.case !== 'initializeRequest') {
114
+ throw new Error('first message must be InitializeRequest');
115
+ }
116
+ initializeLogger(msg.value.loggerOptions);
117
+ });
118
+ const proc = new JobProcess();
119
+ let logger = log().child({ pid: proc.pid });
51
120
 
52
- const start = () => {
53
- if (room.isConnected && !closed) {
54
- process.send!({ type: IPC_MESSAGE.StartJobResponse });
121
+ logger.debug('initializing job runner');
122
+ agent.prewarm(proc);
123
+ logger.debug('job runner initialized');
124
+ process.send({ case: 'initializeResponse' });
55
125
 
56
- // here we import the file containing the exported entry function, and call it.
57
- // the file must export default an Agent, usually using defineAgent().
58
- import(process.argv[3]).then((agent) => {
59
- agent.default.entry(new JobContext(closeEvent, args.job!, room));
60
- });
61
- }
62
- };
126
+ let job: JobTask | undefined = undefined;
127
+ const closeEvent = new EventEmitter();
63
128
 
64
- new Promise(() => {
65
- conn
66
- .then(() => {
67
- if (!closed) start();
68
- })
69
- .catch((err) => {
70
- if (!closed) process.send!({ type: IPC_MESSAGE.StartJobResponse, err });
71
- });
72
- });
129
+ const orphanedTimeout = setTimeout(() => {
130
+ logger.warn('process orphaned, shutting down');
131
+ process.exit();
132
+ }, ORPHANED_TIMEOUT);
73
133
 
74
- await once(closeEvent, 'close');
75
- log.debug('disconnecting from room');
76
- await room.disconnect();
77
- if (shuttingDown) {
78
- process.send({ type: IPC_MESSAGE.ShutdownResponse });
79
- } else {
80
- process.send({ type: IPC_MESSAGE.UserExit });
81
- }
82
- process.exit();
134
+ process.on('message', (msg: IPCMessage) => {
135
+ switch (msg.case) {
136
+ case 'pingRequest': {
137
+ orphanedTimeout.refresh();
138
+ process.send!({
139
+ case: 'pongResponse',
140
+ value: { lastTimestamp: msg.value.timestamp, timestamp: Date.now() },
141
+ });
142
+ break;
143
+ }
144
+ case 'startJobRequest': {
145
+ if (job) {
146
+ throw new Error('job task already running');
147
+ }
148
+
149
+ logger = logger.child({ jobID: msg.value.runningJob.job.id });
150
+
151
+ job = startJob(proc, agent.entry, msg.value.runningJob, closeEvent, logger);
152
+ logger.debug('job started');
153
+ break;
154
+ }
155
+ case 'shutdownRequest': {
156
+ if (!job) {
157
+ break;
158
+ }
159
+ closeEvent.emit('close', '');
160
+ }
161
+ }
162
+ });
83
163
  }
@@ -0,0 +1,39 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { RunningJobInfo } from '../job.js';
5
+ import type { LoggerOptions } from '../log.js';
6
+
7
+ export type IPCMessage =
8
+ | {
9
+ case: 'initializeRequest';
10
+ value: { loggerOptions: LoggerOptions };
11
+ }
12
+ | {
13
+ case: 'initializeResponse';
14
+ value: undefined;
15
+ }
16
+ | {
17
+ case: 'pingRequest';
18
+ value: { timestamp: number };
19
+ }
20
+ | {
21
+ case: 'pongResponse';
22
+ value: { lastTimestamp: number; timestamp: number };
23
+ }
24
+ | {
25
+ case: 'startJobRequest';
26
+ value: { runningJob: RunningJobInfo };
27
+ }
28
+ | {
29
+ case: 'shutdownRequest';
30
+ value: { reason?: string };
31
+ }
32
+ | {
33
+ case: 'exiting';
34
+ value: { reason?: string };
35
+ }
36
+ | {
37
+ case: 'done';
38
+ value: undefined;
39
+ };
@@ -0,0 +1,162 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { ChildProcess } from 'child_process';
5
+ import { once } from 'events';
6
+ import type { RunningJobInfo } from '../job.js';
7
+ import { log, loggerOptions } from '../log.js';
8
+ import { Future } from '../utils.js';
9
+ import type { ProcOpts } from './job_executor.js';
10
+ import { JobExecutor } from './job_executor.js';
11
+ import type { IPCMessage } from './message.js';
12
+
13
+ export class ProcJobExecutor extends JobExecutor {
14
+ #opts: ProcOpts;
15
+ #started = false;
16
+ #closing = false;
17
+ #runningJob?: RunningJobInfo = undefined;
18
+ #proc?: ChildProcess;
19
+ #pingInterval?: ReturnType<typeof setInterval>;
20
+ #pongTimeout?: ReturnType<typeof setTimeout>;
21
+ #init = new Future();
22
+ #join = new Future();
23
+ #logger = log().child({ runningJob: this.#runningJob });
24
+
25
+ constructor(agent: string, initializeTimeout: number, closeTimeout: number) {
26
+ super();
27
+ this.#opts = {
28
+ agent,
29
+ initializeTimeout,
30
+ closeTimeout,
31
+ };
32
+ }
33
+
34
+ get started(): boolean {
35
+ return this.#started;
36
+ }
37
+
38
+ get runningJob(): RunningJobInfo | undefined {
39
+ return this.#runningJob;
40
+ }
41
+
42
+ async start() {
43
+ if (this.#started) {
44
+ throw new Error('runner already started');
45
+ } else if (this.#closing) {
46
+ throw new Error('runner is closed');
47
+ }
48
+
49
+ this.#proc = await import('./job_main.js').then((m) =>
50
+ m.runProcess({
51
+ agentFile: this.#opts.agent,
52
+ }),
53
+ );
54
+
55
+ this.#started = true;
56
+ this.run();
57
+ }
58
+
59
+ async run() {
60
+ await this.#init.await;
61
+
62
+ this.#pingInterval = setInterval(() => {
63
+ this.#proc!.send({ case: 'pingRequest', value: { timestamp: Date.now() } });
64
+ }, this.PING_INTERVAL);
65
+
66
+ this.#pongTimeout = setTimeout(() => {
67
+ this.#logger.warn('job is unresponsive');
68
+ clearTimeout(this.#pongTimeout);
69
+ clearInterval(this.#pingInterval);
70
+ this.#proc!.kill();
71
+ this.#join.resolve();
72
+ }, this.PING_TIMEOUT);
73
+
74
+ const listener = (msg: IPCMessage) => {
75
+ switch (msg.case) {
76
+ case 'pongResponse': {
77
+ const delay = Date.now() - msg.value.timestamp;
78
+ if (delay > this.HIGH_PING_THRESHOLD) {
79
+ this.#logger.child({ delay }).warn('job executor is unresponsive');
80
+ }
81
+ this.#pongTimeout?.refresh();
82
+ break;
83
+ }
84
+ case 'exiting': {
85
+ this.#logger.child({ reason: msg.value.reason }).debug('job exiting');
86
+ break;
87
+ }
88
+ case 'done': {
89
+ this.#closing = true;
90
+ this.#proc!.off('message', listener);
91
+ this.#join.resolve();
92
+ break;
93
+ }
94
+ }
95
+ };
96
+ this.#proc!.on('message', listener);
97
+ this.#proc!.on('error', (err) => {
98
+ if (this.#closing) return;
99
+ this.#logger.child({ err }).warn('job process exited unexpectedly');
100
+ clearTimeout(this.#pongTimeout);
101
+ clearInterval(this.#pingInterval);
102
+ this.#join.resolve();
103
+ });
104
+
105
+ await this.#join.await;
106
+ }
107
+
108
+ async join() {
109
+ if (!this.#started) {
110
+ throw new Error('runner not started');
111
+ }
112
+
113
+ await this.#join.await;
114
+ }
115
+
116
+ async initialize() {
117
+ const timer = setTimeout(() => {
118
+ const err = new Error('runner initialization timed out');
119
+ this.#init.reject(err);
120
+ throw err;
121
+ }, this.#opts.initializeTimeout);
122
+ this.#proc!.send({ case: 'initializeRequest', value: { loggerOptions } });
123
+ await once(this.#proc!, 'message').then(([msg]: IPCMessage[]) => {
124
+ clearTimeout(timer);
125
+ if (msg.case !== 'initializeResponse') {
126
+ throw new Error('first message must be InitializeResponse');
127
+ }
128
+ });
129
+ this.#init.resolve();
130
+ }
131
+
132
+ async close() {
133
+ if (!this.#started) {
134
+ return;
135
+ }
136
+ this.#closing = true;
137
+
138
+ if (!this.#runningJob) {
139
+ this.#proc!.kill();
140
+ this.#join.resolve();
141
+ }
142
+
143
+ this.#proc!.send({ case: 'shutdownRequest' });
144
+
145
+ const timer = setTimeout(() => {
146
+ this.#logger.error('job shutdown is taking too much time');
147
+ }, this.#opts.closeTimeout);
148
+ await this.#join.await.then(() => {
149
+ clearTimeout(timer);
150
+ clearTimeout(this.#pongTimeout);
151
+ clearInterval(this.#pingInterval);
152
+ });
153
+ }
154
+
155
+ async launchJob(info: RunningJobInfo) {
156
+ if (this.#runningJob) {
157
+ throw new Error('executor already has a running job');
158
+ }
159
+ this.#runningJob = info;
160
+ this.#proc!.send({ case: 'startJobRequest', value: { runningJob: info } });
161
+ }
162
+ }
@@ -0,0 +1,109 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { RunningJobInfo } from '../job.js';
5
+ import { Mutex, Queue } from '../utils.js';
6
+ import type { JobExecutor } from './job_executor.js';
7
+ import { ProcJobExecutor } from './proc_job_executor.js';
8
+
9
+ export class ProcPool {
10
+ agent: string;
11
+ initializeTimeout: number;
12
+ closeTimeout: number;
13
+ executors: JobExecutor[] = [];
14
+ tasks: Promise<void>[] = [];
15
+ started = false;
16
+ closed = false;
17
+ controller = new AbortController();
18
+ initMutex = new Mutex();
19
+ procMutex: Mutex;
20
+ procUnlock?: () => void;
21
+ warmedProcQueue = new Queue<JobExecutor>();
22
+
23
+ constructor(
24
+ agent: string,
25
+ numIdleProcesses: number,
26
+ initializeTimeout: number,
27
+ closeTimeout: number,
28
+ ) {
29
+ this.agent = agent;
30
+ this.procMutex = new Mutex(numIdleProcesses);
31
+ this.initializeTimeout = initializeTimeout;
32
+ this.closeTimeout = closeTimeout;
33
+ }
34
+
35
+ get processes(): JobExecutor[] {
36
+ return this.executors;
37
+ }
38
+
39
+ getByJobId(id: string): JobExecutor | null {
40
+ return this.executors.find((x) => x.runningJob && x.runningJob.job.id === id) || null;
41
+ }
42
+
43
+ async launchJob(info: RunningJobInfo) {
44
+ const proc = await this.warmedProcQueue.get();
45
+ if (this.procUnlock) {
46
+ this.procUnlock();
47
+ this.procUnlock = undefined;
48
+ }
49
+ await proc.launchJob(info);
50
+ }
51
+
52
+ async procWatchTask() {
53
+ const proc = new ProcJobExecutor(this.agent, this.initializeTimeout, this.closeTimeout);
54
+
55
+ try {
56
+ this.executors.push(proc);
57
+
58
+ const unlock = await this.initMutex.lock();
59
+ if (this.closed) {
60
+ return;
61
+ }
62
+
63
+ await proc.start();
64
+ try {
65
+ await proc.initialize();
66
+ await this.warmedProcQueue.put(proc);
67
+ } catch {
68
+ if (this.procUnlock) {
69
+ this.procUnlock();
70
+ this.procUnlock = undefined;
71
+ }
72
+ }
73
+
74
+ unlock();
75
+ await proc.join();
76
+ } finally {
77
+ this.executors.splice(this.executors.indexOf(proc));
78
+ }
79
+ }
80
+
81
+ start() {
82
+ if (this.started) {
83
+ return;
84
+ }
85
+
86
+ this.started = true;
87
+ this.run(this.controller.signal);
88
+ }
89
+
90
+ async run(signal: AbortSignal) {
91
+ while (!signal.aborted) {
92
+ this.procUnlock = await this.procMutex.lock();
93
+ const task = this.procWatchTask();
94
+ this.tasks.push(task);
95
+ task.finally(() => this.tasks.splice(this.tasks.indexOf(task)));
96
+ }
97
+ }
98
+
99
+ async close() {
100
+ if (!this.started) {
101
+ return;
102
+ }
103
+ this.closed = true;
104
+ this.controller.abort();
105
+ this.warmedProcQueue.items.forEach((e) => e.close());
106
+ this.executors.forEach((e) => e.close());
107
+ await Promise.allSettled(this.tasks);
108
+ }
109
+ }