@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +47 -0
- package/LICENSE +201 -0
- package/dist/audio.d.ts +9 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +54 -0
- package/dist/audio.js.map +1 -0
- package/dist/cli.d.ts +12 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +102 -19
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +17 -6
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +20 -3
- package/dist/generator.js.map +1 -1
- package/dist/http_server.d.ts +1 -1
- package/dist/http_server.d.ts.map +1 -1
- package/dist/http_server.js +5 -3
- package/dist/http_server.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/ipc/job_executor.d.ts +19 -0
- package/dist/ipc/job_executor.d.ts.map +1 -0
- package/dist/ipc/job_executor.js +8 -0
- package/dist/ipc/job_executor.js.map +1 -0
- package/dist/ipc/job_main.d.ts +7 -4
- package/dist/ipc/job_main.d.ts.map +1 -1
- package/dist/ipc/job_main.js +102 -59
- package/dist/ipc/job_main.js.map +1 -1
- package/dist/ipc/message.d.ts +41 -0
- package/dist/ipc/message.d.ts.map +1 -0
- package/dist/ipc/message.js +2 -0
- package/dist/ipc/message.js.map +1 -0
- package/dist/ipc/proc_job_executor.d.ts +15 -0
- package/dist/ipc/proc_job_executor.d.ts.map +1 -0
- package/dist/ipc/proc_job_executor.js +150 -0
- package/dist/ipc/proc_job_executor.js.map +1 -0
- package/dist/ipc/proc_pool.d.ts +26 -0
- package/dist/ipc/proc_pool.d.ts.map +1 -0
- package/dist/ipc/proc_pool.js +83 -0
- package/dist/ipc/proc_pool.js.map +1 -0
- package/dist/job.d.ts +100 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +213 -0
- package/dist/job.js.map +1 -0
- package/dist/llm/function_context.d.ts +20 -0
- package/dist/llm/function_context.d.ts.map +1 -0
- package/dist/llm/function_context.js +37 -0
- package/dist/llm/function_context.js.map +1 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +6 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/log.d.ts +12 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +28 -11
- package/dist/log.js.map +1 -1
- package/dist/multimodal/agent_playout.d.ts +34 -0
- package/dist/multimodal/agent_playout.d.ts.map +1 -0
- package/dist/multimodal/agent_playout.js +221 -0
- package/dist/multimodal/agent_playout.js.map +1 -0
- package/dist/multimodal/index.d.ts +3 -0
- package/dist/multimodal/index.d.ts.map +1 -0
- package/dist/multimodal/index.js +6 -0
- package/dist/multimodal/index.js.map +1 -0
- package/dist/multimodal/multimodal_agent.d.ts +47 -0
- package/dist/multimodal/multimodal_agent.d.ts.map +1 -0
- package/dist/multimodal/multimodal_agent.js +331 -0
- package/dist/multimodal/multimodal_agent.js.map +1 -0
- package/dist/plugin.js +20 -7
- package/dist/plugin.js.map +1 -1
- package/dist/stt/index.d.ts +1 -1
- package/dist/stt/index.d.ts.map +1 -1
- package/dist/stt/index.js.map +1 -1
- package/dist/stt/stream_adapter.d.ts +2 -11
- package/dist/stt/stream_adapter.d.ts.map +1 -1
- package/dist/stt/stream_adapter.js +47 -33
- package/dist/stt/stream_adapter.js.map +1 -1
- package/dist/stt/stt.d.ts +27 -0
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +32 -5
- package/dist/stt/stt.js.map +1 -1
- package/dist/transcription.d.ts +22 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +111 -0
- package/dist/transcription.js.map +1 -0
- package/dist/tts/stream_adapter.d.ts +4 -11
- package/dist/tts/stream_adapter.d.ts.map +1 -1
- package/dist/tts/stream_adapter.js +66 -32
- package/dist/tts/stream_adapter.js.map +1 -1
- package/dist/tts/tts.d.ts +10 -0
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +48 -7
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.d.ts +59 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +212 -6
- package/dist/utils.js.map +1 -1
- package/dist/vad.d.ts +29 -0
- package/dist/vad.d.ts.map +1 -1
- package/dist/vad.js.map +1 -1
- package/dist/worker.d.ts +69 -50
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +414 -213
- package/dist/worker.js.map +1 -1
- package/package.json +12 -10
- package/src/audio.ts +62 -0
- package/src/cli.ts +108 -20
- package/src/generator.ts +27 -7
- package/src/http_server.ts +5 -0
- package/src/index.ts +15 -3
- package/src/ipc/job_executor.ts +25 -0
- package/src/ipc/job_main.ts +141 -61
- package/src/ipc/message.ts +39 -0
- package/src/ipc/proc_job_executor.ts +162 -0
- package/src/ipc/proc_pool.ts +109 -0
- package/src/job.ts +278 -0
- package/src/llm/function_context.ts +61 -0
- package/src/llm/index.ts +11 -0
- package/src/log.ts +40 -8
- package/src/multimodal/agent_playout.ts +254 -0
- package/src/multimodal/index.ts +5 -0
- package/src/multimodal/multimodal_agent.ts +428 -0
- package/src/stt/index.ts +1 -1
- package/src/stt/stream_adapter.ts +32 -32
- package/src/stt/stt.ts +27 -0
- package/src/transcription.ts +128 -0
- package/src/tts/stream_adapter.ts +32 -31
- package/src/tts/tts.ts +10 -0
- package/src/utils.ts +257 -3
- package/src/vad.ts +29 -0
- package/src/worker.ts +465 -172
- package/tsconfig.json +7 -1
- package/dist/ipc/job_process.d.ts +0 -22
- package/dist/ipc/job_process.d.ts.map +0 -1
- package/dist/ipc/job_process.js +0 -73
- package/dist/ipc/job_process.js.map +0 -1
- package/dist/ipc/protocol.d.ts +0 -40
- package/dist/ipc/protocol.d.ts.map +0 -1
- package/dist/ipc/protocol.js +0 -14
- package/dist/ipc/protocol.js.map +0 -1
- package/dist/job_context.d.ts +0 -16
- package/dist/job_context.d.ts.map +0 -1
- package/dist/job_context.js +0 -31
- package/dist/job_context.js.map +0 -1
- package/dist/job_request.d.ts +0 -42
- package/dist/job_request.d.ts.map +0 -1
- package/dist/job_request.js +0 -79
- package/dist/job_request.js.map +0 -1
- package/src/ipc/job_process.ts +0 -96
- package/src/ipc/protocol.ts +0 -51
- package/src/job_context.ts +0 -49
- package/src/job_request.ts +0 -118
package/src/worker.ts
CHANGED
|
@@ -1,45 +1,117 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import type {
|
|
5
|
+
JobAssignment,
|
|
6
|
+
JobTermination,
|
|
7
|
+
ParticipantInfo,
|
|
8
|
+
TrackSource,
|
|
9
|
+
} from '@livekit/protocol';
|
|
4
10
|
import {
|
|
5
11
|
type AvailabilityRequest,
|
|
6
|
-
type Job,
|
|
7
|
-
type JobAssignment,
|
|
8
12
|
JobType,
|
|
9
13
|
ParticipantPermission,
|
|
10
14
|
ServerMessage,
|
|
11
15
|
WorkerMessage,
|
|
16
|
+
WorkerStatus,
|
|
12
17
|
} from '@livekit/protocol';
|
|
13
18
|
import { EventEmitter } from 'events';
|
|
14
|
-
import { AccessToken } from 'livekit-server-sdk';
|
|
19
|
+
import { AccessToken, RoomServiceClient } from 'livekit-server-sdk';
|
|
15
20
|
import os from 'os';
|
|
16
21
|
import { WebSocket } from 'ws';
|
|
17
22
|
import { HTTPServer } from './http_server.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
23
|
+
import { ProcPool } from './ipc/proc_pool.js';
|
|
24
|
+
import type { JobAcceptArguments, JobProcess, RunningJobInfo } from './job.js';
|
|
25
|
+
import { JobRequest } from './job.js';
|
|
21
26
|
import { log } from './log.js';
|
|
27
|
+
import { Future } from './utils.js';
|
|
22
28
|
import { version } from './version.js';
|
|
23
29
|
|
|
24
30
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
25
|
-
const ASSIGNMENT_TIMEOUT =
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const ASSIGNMENT_TIMEOUT = 7.5 * 1000;
|
|
32
|
+
const UPDATE_LOAD_INTERVAL = 2.5 * 1000;
|
|
33
|
+
|
|
34
|
+
class Default {
|
|
35
|
+
static loadThreshold(production: boolean): number {
|
|
36
|
+
if (production) {
|
|
37
|
+
return 0.65;
|
|
38
|
+
} else {
|
|
39
|
+
return Infinity;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static numIdleProcesses(production: boolean): number {
|
|
44
|
+
if (production) {
|
|
45
|
+
return 3;
|
|
46
|
+
} else {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static port(production: boolean): number {
|
|
52
|
+
if (production) {
|
|
53
|
+
return 8081;
|
|
54
|
+
} else {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Necessary credentials not provided and not found in an appropriate environmental variable. */
|
|
61
|
+
export class MissingCredentialsError extends Error {
|
|
62
|
+
constructor(msg?: string) {
|
|
63
|
+
super(msg);
|
|
64
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Worker did not run as expected. */
|
|
69
|
+
export class WorkerError extends Error {
|
|
70
|
+
constructor(msg?: string) {
|
|
71
|
+
super(msg);
|
|
72
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @internal */
|
|
77
|
+
export const defaultInitializeProcessFunc = (_: JobProcess) => _;
|
|
78
|
+
const defaultRequestFunc = async (ctx: JobRequest) => {
|
|
79
|
+
await ctx.accept();
|
|
80
|
+
};
|
|
81
|
+
const defaultCpuLoad = async (): Promise<number> => {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const cpus1 = os.cpus();
|
|
84
|
+
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
const cpus2 = os.cpus();
|
|
87
|
+
|
|
88
|
+
let idle = 0;
|
|
89
|
+
let total = 0;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < cpus1.length; i++) {
|
|
92
|
+
const cpu1 = cpus1[i].times;
|
|
93
|
+
const cpu2 = cpus2[i].times;
|
|
94
|
+
|
|
95
|
+
idle += cpu2.idle - cpu1.idle;
|
|
96
|
+
|
|
97
|
+
const total1 = Object.values(cpu1).reduce((acc, i) => acc + i, 0);
|
|
98
|
+
const total2 = Object.values(cpu2).reduce((acc, i) => acc + i, 0);
|
|
99
|
+
|
|
100
|
+
total += total2 - total1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resolve(+(1 - idle / total).toFixed(2));
|
|
104
|
+
}, UPDATE_LOAD_INTERVAL);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Participant permissions to pass to every agent spun up by this worker. */
|
|
109
|
+
export class WorkerPermissions {
|
|
39
110
|
canPublish: boolean;
|
|
40
111
|
canSubscribe: boolean;
|
|
41
112
|
canPublishData: boolean;
|
|
42
113
|
canUpdateMetadata: boolean;
|
|
114
|
+
canPublishSources: TrackSource[];
|
|
43
115
|
hidden: boolean;
|
|
44
116
|
|
|
45
117
|
constructor(
|
|
@@ -47,21 +119,37 @@ class WorkerPermissions {
|
|
|
47
119
|
canSubscribe = true,
|
|
48
120
|
canPublishData = true,
|
|
49
121
|
canUpdateMetadata = true,
|
|
122
|
+
canPublishSources: TrackSource[] = [],
|
|
50
123
|
hidden = false,
|
|
51
124
|
) {
|
|
52
125
|
this.canPublish = canPublish;
|
|
53
126
|
this.canSubscribe = canSubscribe;
|
|
54
127
|
this.canPublishData = canPublishData;
|
|
55
128
|
this.canUpdateMetadata = canUpdateMetadata;
|
|
129
|
+
this.canPublishSources = canPublishSources;
|
|
56
130
|
this.hidden = hidden;
|
|
57
131
|
}
|
|
58
132
|
}
|
|
59
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Data class describing worker behaviour.
|
|
136
|
+
*
|
|
137
|
+
* @remarks
|
|
138
|
+
* The Agents framework provides sane worker defaults, and works out-of-the-box with no tweaking
|
|
139
|
+
* necessary. The only mandatory parameter is `agent`, which points to the entry function.
|
|
140
|
+
*
|
|
141
|
+
* This class is mostly useful in conjunction with {@link cli.runApp}.
|
|
142
|
+
*/
|
|
60
143
|
export class WorkerOptions {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
144
|
+
agent: string;
|
|
145
|
+
requestFunc: (job: JobRequest) => Promise<void>;
|
|
146
|
+
loadFunc: () => Promise<number>;
|
|
147
|
+
loadThreshold: number;
|
|
148
|
+
numIdleProcesses: number;
|
|
149
|
+
shutdownProcessTimeout: number;
|
|
150
|
+
initializeProcessTimeout: number;
|
|
64
151
|
permissions: WorkerPermissions;
|
|
152
|
+
agentName: string;
|
|
65
153
|
workerType: JobType;
|
|
66
154
|
maxRetry: number;
|
|
67
155
|
wsURL: string;
|
|
@@ -69,24 +157,45 @@ export class WorkerOptions {
|
|
|
69
157
|
apiSecret?: string;
|
|
70
158
|
host: string;
|
|
71
159
|
port: number;
|
|
160
|
+
logLevel: string;
|
|
161
|
+
production: boolean;
|
|
72
162
|
|
|
163
|
+
/** @param options */
|
|
73
164
|
constructor({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
165
|
+
agent,
|
|
166
|
+
requestFunc = defaultRequestFunc,
|
|
167
|
+
loadFunc = defaultCpuLoad,
|
|
168
|
+
loadThreshold = undefined,
|
|
169
|
+
numIdleProcesses = undefined,
|
|
170
|
+
shutdownProcessTimeout = 60 * 1000,
|
|
171
|
+
initializeProcessTimeout = 10 * 1000,
|
|
77
172
|
permissions = new WorkerPermissions(),
|
|
78
|
-
|
|
173
|
+
agentName = '',
|
|
174
|
+
workerType = JobType.JT_ROOM,
|
|
79
175
|
maxRetry = MAX_RECONNECT_ATTEMPTS,
|
|
80
176
|
wsURL = 'ws://localhost:7880',
|
|
81
177
|
apiKey = undefined,
|
|
82
178
|
apiSecret = undefined,
|
|
83
179
|
host = 'localhost',
|
|
84
|
-
port =
|
|
180
|
+
port = undefined,
|
|
181
|
+
logLevel = 'info',
|
|
182
|
+
production = false,
|
|
85
183
|
}: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Path to a file that has {@link Agent} as a default export, dynamically imported later for
|
|
186
|
+
* entrypoint and prewarm functions
|
|
187
|
+
*/
|
|
188
|
+
agent: string;
|
|
189
|
+
requestFunc?: (job: JobRequest) => Promise<void>;
|
|
190
|
+
/** Called to determine the current load of the worker. Should return a value between 0 and 1. */
|
|
191
|
+
loadFunc?: () => Promise<number>;
|
|
192
|
+
/** When the load exceeds this threshold, the worker will be marked as unavailable. */
|
|
193
|
+
loadThreshold?: number;
|
|
194
|
+
numIdleProcesses?: number;
|
|
195
|
+
shutdownProcessTimeout?: number;
|
|
196
|
+
initializeProcessTimeout?: number;
|
|
89
197
|
permissions?: WorkerPermissions;
|
|
198
|
+
agentName?: string;
|
|
90
199
|
workerType?: JobType;
|
|
91
200
|
maxRetry?: number;
|
|
92
201
|
wsURL?: string;
|
|
@@ -94,113 +203,146 @@ export class WorkerOptions {
|
|
|
94
203
|
apiSecret?: string;
|
|
95
204
|
host?: string;
|
|
96
205
|
port?: number;
|
|
206
|
+
logLevel?: string;
|
|
207
|
+
production?: boolean;
|
|
97
208
|
}) {
|
|
209
|
+
this.agent = agent;
|
|
210
|
+
if (!this.agent) {
|
|
211
|
+
throw new Error('No Agent file was passed to the worker');
|
|
212
|
+
}
|
|
98
213
|
this.requestFunc = requestFunc;
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
214
|
+
this.loadFunc = loadFunc;
|
|
215
|
+
this.loadThreshold = loadThreshold || Default.loadThreshold(production);
|
|
216
|
+
this.numIdleProcesses = numIdleProcesses || Default.numIdleProcesses(production);
|
|
217
|
+
this.shutdownProcessTimeout = shutdownProcessTimeout;
|
|
218
|
+
this.initializeProcessTimeout = initializeProcessTimeout;
|
|
101
219
|
this.permissions = permissions;
|
|
220
|
+
this.agentName = agentName;
|
|
102
221
|
this.workerType = workerType;
|
|
103
222
|
this.maxRetry = maxRetry;
|
|
104
223
|
this.wsURL = wsURL;
|
|
105
224
|
this.apiKey = apiKey;
|
|
106
225
|
this.apiSecret = apiSecret;
|
|
107
226
|
this.host = host;
|
|
108
|
-
this.port = port;
|
|
227
|
+
this.port = port || Default.port(production);
|
|
228
|
+
this.logLevel = logLevel;
|
|
229
|
+
this.production = production;
|
|
109
230
|
}
|
|
110
231
|
}
|
|
111
232
|
|
|
112
|
-
class ActiveJob {
|
|
113
|
-
job: Job;
|
|
114
|
-
acceptData: AcceptData;
|
|
115
|
-
|
|
116
|
-
constructor(job: Job, acceptData: AcceptData) {
|
|
117
|
-
this.job = job;
|
|
118
|
-
this.acceptData = acceptData;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
type AssignmentPair = {
|
|
123
|
-
// this string is the JSON string version of the JobAssignment.
|
|
124
|
-
// we keep it around to unpack it again in the child, because we can't pass Job directly.
|
|
125
|
-
raw: string;
|
|
126
|
-
asgn: JobAssignment;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
233
|
class PendingAssignment {
|
|
130
|
-
promise = new Promise<
|
|
131
|
-
this.resolve = resolve; //
|
|
234
|
+
promise = new Promise<JobAssignment>((resolve) => {
|
|
235
|
+
this.resolve = resolve; // this is how JavaScript lets you resolve promises externally
|
|
132
236
|
});
|
|
133
|
-
resolve(arg:
|
|
134
|
-
arg;
|
|
237
|
+
resolve(arg: JobAssignment) {
|
|
238
|
+
arg; // useless call to counteract TypeScript E6133
|
|
135
239
|
}
|
|
136
240
|
}
|
|
137
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Central orchestrator for all processes and job requests.
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* For most usecases, Worker should not be initialized or handled directly; you should instead call
|
|
247
|
+
* for its creation through {@link cli.runApp}. This could, however, be useful in situations where
|
|
248
|
+
* you don't have access to a command line, such as a headless program, or one that uses Agents
|
|
249
|
+
* behind a wrapper.
|
|
250
|
+
*/
|
|
138
251
|
export class Worker {
|
|
139
|
-
opts: WorkerOptions;
|
|
252
|
+
#opts: WorkerOptions;
|
|
253
|
+
#procPool: ProcPool;
|
|
254
|
+
|
|
140
255
|
#id = 'unregistered';
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
256
|
+
#closed = true;
|
|
257
|
+
#draining = false;
|
|
258
|
+
#connecting = false;
|
|
259
|
+
#tasks: Promise<void>[] = [];
|
|
260
|
+
#pending: { [id: string]: PendingAssignment } = {};
|
|
261
|
+
#close = new Future();
|
|
262
|
+
|
|
145
263
|
event = new EventEmitter();
|
|
146
|
-
|
|
147
|
-
|
|
264
|
+
#session: WebSocket | undefined = undefined;
|
|
265
|
+
#httpServer: HTTPServer;
|
|
266
|
+
#logger = log().child({ version });
|
|
148
267
|
|
|
268
|
+
/* @throws {@link MissingCredentialsError} if URL, API key or API secret are missing */
|
|
149
269
|
constructor(opts: WorkerOptions) {
|
|
150
270
|
opts.wsURL = opts.wsURL || process.env.LIVEKIT_URL || '';
|
|
151
271
|
opts.apiKey = opts.apiKey || process.env.LIVEKIT_API_KEY || '';
|
|
152
272
|
opts.apiSecret = opts.apiSecret || process.env.LIVEKIT_API_SECRET || '';
|
|
153
273
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
274
|
+
if (opts.wsURL === '')
|
|
275
|
+
throw new MissingCredentialsError(
|
|
276
|
+
'URL is required: Set LIVEKIT_URL, run with --url, or pass wsURL in WorkerOptions',
|
|
277
|
+
);
|
|
278
|
+
if (opts.apiKey === '')
|
|
279
|
+
throw new MissingCredentialsError(
|
|
280
|
+
'API Key is required: Set LIVEKIT_API_KEY, run with --api-key, or pass apiKey in WorkerOptions',
|
|
281
|
+
);
|
|
282
|
+
if (opts.apiSecret === '')
|
|
283
|
+
throw new MissingCredentialsError(
|
|
284
|
+
'API Secret is required: Set LIVEKIT_API_SECRET, run with --api-secret, or pass apiSecret in WorkerOptions',
|
|
285
|
+
);
|
|
157
286
|
|
|
158
|
-
|
|
159
|
-
|
|
287
|
+
this.#procPool = new ProcPool(
|
|
288
|
+
opts.agent,
|
|
289
|
+
opts.numIdleProcesses,
|
|
290
|
+
opts.initializeProcessTimeout,
|
|
291
|
+
opts.shutdownProcessTimeout,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
this.#opts = opts;
|
|
295
|
+
this.#httpServer = new HTTPServer(opts.host, opts.port);
|
|
160
296
|
}
|
|
161
297
|
|
|
298
|
+
/* @throws {@link WorkerError} if worker failed to connect or already running */
|
|
162
299
|
async run() {
|
|
163
|
-
this
|
|
300
|
+
if (!this.#closed) {
|
|
301
|
+
throw new WorkerError('worker is already running');
|
|
302
|
+
}
|
|
164
303
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (this.opts.apiSecret === '')
|
|
169
|
-
throw new Error('--api-secret is required, or set LIVEKIT_API_SECRET env var');
|
|
304
|
+
this.#logger.info('starting worker');
|
|
305
|
+
this.#closed = false;
|
|
306
|
+
this.#procPool.start();
|
|
170
307
|
|
|
171
308
|
const workerWS = async () => {
|
|
172
309
|
let retries = 0;
|
|
173
|
-
|
|
174
|
-
const token = new AccessToken(this.opts.apiKey, this.opts.apiSecret);
|
|
175
|
-
token.addGrant({ agent: true });
|
|
176
|
-
const jwt = await token.toJwt();
|
|
310
|
+
this.#connecting = true;
|
|
177
311
|
|
|
178
|
-
|
|
312
|
+
while (!this.#closed) {
|
|
313
|
+
const url = new URL(this.#opts.wsURL);
|
|
179
314
|
url.protocol = url.protocol.replace('http', 'ws');
|
|
180
|
-
|
|
315
|
+
const token = new AccessToken(this.#opts.apiKey, this.#opts.apiSecret);
|
|
316
|
+
token.addGrant({ agent: true });
|
|
317
|
+
const jwt = await token.toJwt();
|
|
318
|
+
this.#session = new WebSocket(url + 'agent', {
|
|
181
319
|
headers: { authorization: 'Bearer ' + jwt },
|
|
182
320
|
});
|
|
183
321
|
|
|
184
322
|
try {
|
|
185
323
|
await new Promise((resolve, reject) => {
|
|
186
|
-
this
|
|
187
|
-
this
|
|
188
|
-
this
|
|
324
|
+
this.#session!.on('open', resolve);
|
|
325
|
+
this.#session!.on('error', (error) => reject(error));
|
|
326
|
+
this.#session!.on('close', (code) => reject(`WebSocket returned ${code}`));
|
|
189
327
|
});
|
|
190
328
|
|
|
191
|
-
|
|
329
|
+
retries = 0;
|
|
330
|
+
this.#logger.debug('connected to LiveKit server');
|
|
331
|
+
this.#runWS(this.#session);
|
|
192
332
|
return;
|
|
193
333
|
} catch (e) {
|
|
194
|
-
if (this
|
|
195
|
-
if (retries >= this
|
|
196
|
-
throw new
|
|
334
|
+
if (this.#closed) return;
|
|
335
|
+
if (retries >= this.#opts.maxRetry) {
|
|
336
|
+
throw new WorkerError(
|
|
337
|
+
`failed to connect to LiveKit server after ${retries} attempts: ${e}`,
|
|
338
|
+
);
|
|
197
339
|
}
|
|
198
340
|
|
|
199
341
|
retries++;
|
|
200
342
|
const delay = Math.min(retries * 2, 10);
|
|
201
343
|
|
|
202
|
-
this
|
|
203
|
-
`failed to connect to LiveKit server, retrying in ${delay} seconds: ${e} (${retries}/${this
|
|
344
|
+
this.#logger.warn(
|
|
345
|
+
`failed to connect to LiveKit server, retrying in ${delay} seconds: ${e} (${retries}/${this.#opts.maxRetry})`,
|
|
204
346
|
);
|
|
205
347
|
|
|
206
348
|
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
|
|
@@ -208,24 +350,92 @@ export class Worker {
|
|
|
208
350
|
}
|
|
209
351
|
};
|
|
210
352
|
|
|
211
|
-
await Promise.all([workerWS(), this
|
|
353
|
+
await Promise.all([workerWS(), this.#httpServer.run()]);
|
|
354
|
+
this.#close.resolve();
|
|
212
355
|
}
|
|
213
356
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
357
|
+
get id(): string {
|
|
358
|
+
return this.#id;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
get activeJobs(): RunningJobInfo[] {
|
|
362
|
+
return this.#procPool.processes
|
|
363
|
+
.filter((proc) => proc.runningJob)
|
|
364
|
+
.map((proc) => proc.runningJob!);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* @throws {@link WorkerError} if worker did not drain in time */
|
|
368
|
+
async drain(timeout?: number) {
|
|
369
|
+
if (this.#draining) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.#logger.info('draining worker');
|
|
374
|
+
this.#draining = true;
|
|
375
|
+
|
|
376
|
+
this.event.emit(
|
|
377
|
+
'worker_msg',
|
|
378
|
+
new WorkerMessage({
|
|
379
|
+
message: {
|
|
380
|
+
case: 'updateWorker',
|
|
381
|
+
value: {
|
|
382
|
+
status: WorkerStatus.WS_FULL,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const joinJobs = async () => {
|
|
389
|
+
return Promise.all(
|
|
390
|
+
this.#procPool.processes.map((proc) => {
|
|
391
|
+
if (!proc.runningJob) {
|
|
392
|
+
proc.close();
|
|
393
|
+
}
|
|
394
|
+
return proc.join();
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
throw new WorkerError('timed out draining');
|
|
401
|
+
}, timeout);
|
|
402
|
+
if (timeout === undefined) clearTimeout(timer);
|
|
403
|
+
await joinJobs().then(() => {
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
});
|
|
226
406
|
}
|
|
227
407
|
|
|
228
|
-
|
|
408
|
+
async simulateJob(roomName: string, participantIdentity?: string) {
|
|
409
|
+
const client = new RoomServiceClient(this.#opts.wsURL, this.#opts.apiKey, this.#opts.apiSecret);
|
|
410
|
+
const room = await client.createRoom({ name: roomName });
|
|
411
|
+
let participant: ParticipantInfo | undefined = undefined;
|
|
412
|
+
if (participantIdentity) {
|
|
413
|
+
try {
|
|
414
|
+
participant = await client.getParticipant(roomName, participantIdentity);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
this.#logger.fatal(
|
|
417
|
+
`participant with identity ${participantIdentity} not found in room ${roomName}`,
|
|
418
|
+
);
|
|
419
|
+
throw e;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.event!.emit(
|
|
424
|
+
'worker_msg',
|
|
425
|
+
new WorkerMessage({
|
|
426
|
+
message: {
|
|
427
|
+
case: 'simulateJob',
|
|
428
|
+
value: {
|
|
429
|
+
type: JobType.JT_PUBLISHER,
|
|
430
|
+
room,
|
|
431
|
+
participant,
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#runWS(ws: WebSocket) {
|
|
229
439
|
let closingWS = false;
|
|
230
440
|
|
|
231
441
|
const send = (msg: WorkerMessage) => {
|
|
@@ -239,44 +449,62 @@ export class Worker {
|
|
|
239
449
|
|
|
240
450
|
ws.addEventListener('close', () => {
|
|
241
451
|
closingWS = true;
|
|
242
|
-
this
|
|
452
|
+
this.#logger.error('worker connection closed unexpectedly');
|
|
243
453
|
this.close();
|
|
244
454
|
});
|
|
245
455
|
|
|
246
456
|
ws.addEventListener('message', (event) => {
|
|
247
457
|
if (event.type !== 'message') {
|
|
248
|
-
this
|
|
458
|
+
this.#logger.warn('unexpected message type: ' + event.type);
|
|
249
459
|
return;
|
|
250
460
|
}
|
|
251
461
|
|
|
252
462
|
const msg = new ServerMessage();
|
|
253
463
|
msg.fromBinary(event.data as Uint8Array);
|
|
464
|
+
|
|
465
|
+
// register is the only valid first message, and it is only valid as the
|
|
466
|
+
// first message
|
|
467
|
+
if (this.#connecting && msg.message.case !== 'register') {
|
|
468
|
+
throw new WorkerError('expected register response as first message');
|
|
469
|
+
}
|
|
470
|
+
|
|
254
471
|
switch (msg.message.case) {
|
|
255
472
|
case 'register': {
|
|
256
473
|
this.#id = msg.message.value.workerId;
|
|
257
|
-
|
|
474
|
+
this.#logger
|
|
258
475
|
.child({ id: this.id, server_info: msg.message.value.serverInfo })
|
|
259
476
|
.info('registered worker');
|
|
477
|
+
this.event.emit(
|
|
478
|
+
'worker_registered',
|
|
479
|
+
msg.message.value.workerId,
|
|
480
|
+
msg.message.value.serverInfo!,
|
|
481
|
+
);
|
|
482
|
+
this.#connecting = false;
|
|
260
483
|
break;
|
|
261
484
|
}
|
|
262
485
|
case 'availability': {
|
|
263
|
-
this
|
|
486
|
+
const task = this.#availability(msg.message.value);
|
|
487
|
+
this.#tasks.push(task);
|
|
488
|
+
task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
|
|
264
489
|
break;
|
|
265
490
|
}
|
|
266
491
|
case 'assignment': {
|
|
267
492
|
const job = msg.message.value.job!;
|
|
268
|
-
if (job.id in this
|
|
269
|
-
const task = this
|
|
270
|
-
delete this
|
|
271
|
-
task.
|
|
272
|
-
asgn: msg.message.value,
|
|
273
|
-
raw: msg.toJsonString(),
|
|
274
|
-
});
|
|
493
|
+
if (job.id in this.#pending) {
|
|
494
|
+
const task = this.#pending[job.id];
|
|
495
|
+
delete this.#pending[job.id];
|
|
496
|
+
task.resolve(msg.message.value);
|
|
275
497
|
} else {
|
|
276
|
-
|
|
498
|
+
this.#logger.child({ job }).warn('received assignment for unknown job ' + job.id);
|
|
277
499
|
}
|
|
278
500
|
break;
|
|
279
501
|
}
|
|
502
|
+
case 'termination': {
|
|
503
|
+
const task = this.#termination(msg.message.value);
|
|
504
|
+
this.#tasks.push(task);
|
|
505
|
+
task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
280
508
|
}
|
|
281
509
|
});
|
|
282
510
|
|
|
@@ -286,13 +514,14 @@ export class Worker {
|
|
|
286
514
|
message: {
|
|
287
515
|
case: 'register',
|
|
288
516
|
value: {
|
|
289
|
-
type: this
|
|
290
|
-
|
|
517
|
+
type: this.#opts.workerType,
|
|
518
|
+
agentName: this.#opts.agentName,
|
|
291
519
|
allowedPermissions: new ParticipantPermission({
|
|
292
|
-
canPublish: this
|
|
293
|
-
canSubscribe: this
|
|
294
|
-
canPublishData: this
|
|
295
|
-
|
|
520
|
+
canPublish: this.#opts.permissions.canPublish,
|
|
521
|
+
canSubscribe: this.#opts.permissions.canSubscribe,
|
|
522
|
+
canPublishData: this.#opts.permissions.canPublishData,
|
|
523
|
+
canUpdateMetadata: this.#opts.permissions.canUpdateMetadata,
|
|
524
|
+
hidden: this.#opts.permissions.hidden,
|
|
296
525
|
agent: true,
|
|
297
526
|
}),
|
|
298
527
|
version,
|
|
@@ -301,84 +530,148 @@ export class Worker {
|
|
|
301
530
|
}),
|
|
302
531
|
);
|
|
303
532
|
|
|
533
|
+
let currentStatus = WorkerStatus.WS_AVAILABLE;
|
|
304
534
|
const loadMonitor = setInterval(() => {
|
|
305
535
|
if (closingWS) clearInterval(loadMonitor);
|
|
536
|
+
|
|
537
|
+
const oldStatus = currentStatus;
|
|
538
|
+
this.#opts.loadFunc().then((currentLoad: number) => {
|
|
539
|
+
const isFull = currentLoad >= this.#opts.loadThreshold;
|
|
540
|
+
const currentlyAvailable = !isFull;
|
|
541
|
+
currentStatus = currentlyAvailable ? WorkerStatus.WS_AVAILABLE : WorkerStatus.WS_FULL;
|
|
542
|
+
|
|
543
|
+
if (oldStatus != currentStatus) {
|
|
544
|
+
const extra = { load: currentLoad, loadThreshold: this.#opts.loadThreshold };
|
|
545
|
+
if (isFull) {
|
|
546
|
+
this.#logger.child(extra).info('worker is at full capacity, marking as unavailable');
|
|
547
|
+
} else {
|
|
548
|
+
this.#logger.child(extra).info('worker is below capacity, marking as available');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.event.emit(
|
|
553
|
+
'worker_msg',
|
|
554
|
+
new WorkerMessage({
|
|
555
|
+
message: {
|
|
556
|
+
case: 'updateWorker',
|
|
557
|
+
value: {
|
|
558
|
+
load: currentLoad,
|
|
559
|
+
status: currentStatus,
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
}, UPDATE_LOAD_INTERVAL);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async #availability(msg: AvailabilityRequest) {
|
|
569
|
+
let answered = false;
|
|
570
|
+
|
|
571
|
+
const onReject = async () => {
|
|
572
|
+
answered = true;
|
|
306
573
|
this.event.emit(
|
|
307
574
|
'worker_msg',
|
|
308
575
|
new WorkerMessage({
|
|
309
576
|
message: {
|
|
310
|
-
case: '
|
|
577
|
+
case: 'availability',
|
|
311
578
|
value: {
|
|
312
|
-
|
|
579
|
+
jobId: msg.job!.id,
|
|
580
|
+
available: false,
|
|
313
581
|
},
|
|
314
582
|
},
|
|
315
583
|
}),
|
|
316
584
|
);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
585
|
+
};
|
|
319
586
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const req = new JobRequest(msg.job!, tx);
|
|
587
|
+
const onAccept = async (args: JobAcceptArguments) => {
|
|
588
|
+
answered = true;
|
|
323
589
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
590
|
+
this.event.emit(
|
|
591
|
+
'worker_msg',
|
|
592
|
+
new WorkerMessage({
|
|
593
|
+
message: {
|
|
594
|
+
case: 'availability',
|
|
595
|
+
value: {
|
|
596
|
+
jobId: msg.job!.id,
|
|
597
|
+
available: true,
|
|
598
|
+
participantIdentity: args.identity,
|
|
599
|
+
participantName: args.name,
|
|
600
|
+
participantMetadata: args.metadata,
|
|
601
|
+
},
|
|
334
602
|
},
|
|
335
|
-
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
this.pending[req.id] = { value: new PendingAssignment() };
|
|
339
|
-
this.event.emit('worker_msg', msg);
|
|
340
|
-
if (!av.avail) return;
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
341
605
|
|
|
606
|
+
this.#pending[req.id] = new PendingAssignment();
|
|
342
607
|
const timer = setTimeout(() => {
|
|
343
|
-
|
|
608
|
+
this.#logger.child({ req }).warn(`assignment for job ${req.id} timed out`);
|
|
344
609
|
return;
|
|
345
610
|
}, ASSIGNMENT_TIMEOUT);
|
|
346
|
-
this
|
|
611
|
+
const asgn = await this.#pending[req.id].promise.then(async (asgn) => {
|
|
347
612
|
clearTimeout(timer);
|
|
348
|
-
|
|
613
|
+
return asgn;
|
|
349
614
|
});
|
|
350
|
-
});
|
|
351
615
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
616
|
+
await this.#procPool.launchJob({
|
|
617
|
+
acceptArguments: args,
|
|
618
|
+
job: msg.job!,
|
|
619
|
+
url: asgn.url || this.#opts.wsURL,
|
|
620
|
+
token: asgn.token,
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const req = new JobRequest(msg.job!, onReject, onAccept);
|
|
625
|
+
this.#logger
|
|
626
|
+
.child({ job: msg.job, resuming: msg.resuming, agentName: this.#opts.agentName })
|
|
627
|
+
.info('received job request');
|
|
628
|
+
|
|
629
|
+
const jobRequestTask = async () => {
|
|
630
|
+
try {
|
|
631
|
+
await this.#opts.requestFunc(req);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
this.#logger
|
|
634
|
+
.child({ job: msg.job, resuming: msg.resuming, agentName: this.#opts.agentName })
|
|
635
|
+
.info('jobRequestFunc failed');
|
|
636
|
+
await onReject();
|
|
370
637
|
}
|
|
638
|
+
|
|
639
|
+
if (!answered) {
|
|
640
|
+
this.#logger
|
|
641
|
+
.child({ job: msg.job, resuming: msg.resuming, agentName: this.#opts.agentName })
|
|
642
|
+
.info('no answer was given inside the jobRequestFunc, automatically rejecting the job');
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const task = jobRequestTask();
|
|
647
|
+
this.#tasks.push(task);
|
|
648
|
+
task.finally(() => this.#tasks.splice(this.#tasks.indexOf(task)));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async #termination(msg: JobTermination) {
|
|
652
|
+
const proc = this.#procPool.getByJobId(msg.jobId);
|
|
653
|
+
if (proc === null) {
|
|
654
|
+
// safe to ignore
|
|
655
|
+
return;
|
|
371
656
|
}
|
|
657
|
+
await proc.close();
|
|
372
658
|
}
|
|
373
659
|
|
|
374
660
|
async close() {
|
|
375
|
-
if (this
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await this.httpServer.close();
|
|
379
|
-
for await (const value of Object.values(this.processes)) {
|
|
380
|
-
await value.proc.close();
|
|
661
|
+
if (this.#closed) {
|
|
662
|
+
await this.#close.await;
|
|
663
|
+
return;
|
|
381
664
|
}
|
|
382
|
-
|
|
665
|
+
|
|
666
|
+
this.#logger.info('shutting down worker');
|
|
667
|
+
|
|
668
|
+
this.#closed = true;
|
|
669
|
+
|
|
670
|
+
await this.#procPool.close();
|
|
671
|
+
await this.#httpServer.close();
|
|
672
|
+
await Promise.allSettled(this.#tasks);
|
|
673
|
+
|
|
674
|
+
this.#session?.close();
|
|
675
|
+
await this.#close.await;
|
|
383
676
|
}
|
|
384
677
|
}
|