@livekit/agents 1.0.47 → 1.0.49
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/dist/beta/index.cjs +29 -0
- package/dist/beta/index.cjs.map +1 -0
- package/dist/beta/index.d.cts +2 -0
- package/dist/beta/index.d.ts +2 -0
- package/dist/beta/index.d.ts.map +1 -0
- package/dist/beta/index.js +7 -0
- package/dist/beta/index.js.map +1 -0
- package/dist/beta/workflows/index.cjs +29 -0
- package/dist/beta/workflows/index.cjs.map +1 -0
- package/dist/beta/workflows/index.d.cts +2 -0
- package/dist/beta/workflows/index.d.ts +2 -0
- package/dist/beta/workflows/index.d.ts.map +1 -0
- package/dist/beta/workflows/index.js +7 -0
- package/dist/beta/workflows/index.js.map +1 -0
- package/dist/beta/workflows/task_group.cjs +162 -0
- package/dist/beta/workflows/task_group.cjs.map +1 -0
- package/dist/beta/workflows/task_group.d.cts +32 -0
- package/dist/beta/workflows/task_group.d.ts +32 -0
- package/dist/beta/workflows/task_group.d.ts.map +1 -0
- package/dist/beta/workflows/task_group.js +138 -0
- package/dist/beta/workflows/task_group.js.map +1 -0
- package/dist/cpu.cjs +189 -0
- package/dist/cpu.cjs.map +1 -0
- package/dist/cpu.d.cts +24 -0
- package/dist/cpu.d.ts +24 -0
- package/dist/cpu.d.ts.map +1 -0
- package/dist/cpu.js +152 -0
- package/dist/cpu.js.map +1 -0
- package/dist/cpu.test.cjs +227 -0
- package/dist/cpu.test.cjs.map +1 -0
- package/dist/cpu.test.js +204 -0
- package/dist/cpu.test.js.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/inference/api_protos.d.cts +59 -59
- package/dist/inference/api_protos.d.ts +59 -59
- package/dist/inference/llm.cjs.map +1 -1
- package/dist/inference/llm.d.cts +1 -1
- package/dist/inference/llm.d.ts +1 -1
- package/dist/inference/llm.d.ts.map +1 -1
- package/dist/inference/llm.js.map +1 -1
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.cts +6 -0
- package/dist/inference/tts.d.ts +6 -0
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js.map +1 -1
- package/dist/llm/chat_context.cjs +89 -1
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.cts +10 -1
- package/dist/llm/chat_context.d.ts +10 -1
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +89 -1
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/chat_context.test.cjs +43 -0
- package/dist/llm/chat_context.test.cjs.map +1 -1
- package/dist/llm/chat_context.test.js +43 -0
- package/dist/llm/chat_context.test.js.map +1 -1
- package/dist/llm/index.cjs +2 -0
- package/dist/llm/index.cjs.map +1 -1
- package/dist/llm/index.d.cts +1 -1
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +3 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/provider_format/index.d.cts +1 -1
- package/dist/llm/provider_format/index.d.ts +1 -1
- package/dist/llm/tool_context.cjs +7 -0
- package/dist/llm/tool_context.cjs.map +1 -1
- package/dist/llm/tool_context.d.cts +10 -2
- package/dist/llm/tool_context.d.ts +10 -2
- package/dist/llm/tool_context.d.ts.map +1 -1
- package/dist/llm/tool_context.js +6 -0
- package/dist/llm/tool_context.js.map +1 -1
- package/dist/utils.cjs +1 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist/voice/agent.cjs +9 -0
- package/dist/voice/agent.cjs.map +1 -1
- package/dist/voice/agent.d.cts +1 -0
- package/dist/voice/agent.d.ts +1 -0
- package/dist/voice/agent.d.ts.map +1 -1
- package/dist/voice/agent.js +9 -0
- package/dist/voice/agent.js.map +1 -1
- package/dist/voice/agent_activity.cjs +67 -16
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +7 -0
- package/dist/voice/agent_activity.d.ts +7 -0
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +68 -17
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +27 -1
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +6 -0
- package/dist/voice/agent_session.d.ts +6 -0
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +27 -1
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/room_io/room_io.cjs +11 -2
- package/dist/voice/room_io/room_io.cjs.map +1 -1
- package/dist/voice/room_io/room_io.d.ts.map +1 -1
- package/dist/voice/room_io/room_io.js +12 -3
- package/dist/voice/room_io/room_io.js.map +1 -1
- package/dist/voice/testing/fake_llm.cjs +127 -0
- package/dist/voice/testing/fake_llm.cjs.map +1 -0
- package/dist/voice/testing/fake_llm.d.cts +30 -0
- package/dist/voice/testing/fake_llm.d.ts +30 -0
- package/dist/voice/testing/fake_llm.d.ts.map +1 -0
- package/dist/voice/testing/fake_llm.js +103 -0
- package/dist/voice/testing/fake_llm.js.map +1 -0
- package/dist/voice/testing/index.cjs +3 -0
- package/dist/voice/testing/index.cjs.map +1 -1
- package/dist/voice/testing/index.d.cts +1 -0
- package/dist/voice/testing/index.d.ts +1 -0
- package/dist/voice/testing/index.d.ts.map +1 -1
- package/dist/voice/testing/index.js +2 -0
- package/dist/voice/testing/index.js.map +1 -1
- package/dist/worker.cjs +6 -29
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +6 -19
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/beta/index.ts +9 -0
- package/src/beta/workflows/index.ts +9 -0
- package/src/beta/workflows/task_group.ts +194 -0
- package/src/cpu.test.ts +239 -0
- package/src/cpu.ts +173 -0
- package/src/index.ts +2 -1
- package/src/inference/llm.ts +2 -0
- package/src/inference/tts.ts +8 -1
- package/src/llm/chat_context.test.ts +48 -0
- package/src/llm/chat_context.ts +123 -0
- package/src/llm/index.ts +1 -0
- package/src/llm/tool_context.ts +14 -0
- package/src/utils.ts +5 -0
- package/src/voice/agent.ts +11 -0
- package/src/voice/agent_activity.ts +102 -16
- package/src/voice/agent_session.ts +33 -2
- package/src/voice/room_io/room_io.ts +14 -3
- package/src/voice/testing/fake_llm.ts +138 -0
- package/src/voice/testing/index.ts +2 -0
- package/src/worker.ts +34 -50
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import type { ChatContext } from '../../llm/chat_context.js';
|
|
6
|
+
import { LLM, ToolError, ToolFlag, tool } from '../../llm/index.js';
|
|
7
|
+
import { AgentTask } from '../../voice/agent.js';
|
|
8
|
+
|
|
9
|
+
interface FactoryInfo {
|
|
10
|
+
taskFactory: () => AgentTask;
|
|
11
|
+
id: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskGroupResult {
|
|
16
|
+
taskResults: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TaskCompletedEvent {
|
|
20
|
+
agentTask: AgentTask;
|
|
21
|
+
taskId: string;
|
|
22
|
+
result: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class OutOfScopeError extends ToolError {
|
|
26
|
+
readonly targetTaskIds: string[];
|
|
27
|
+
|
|
28
|
+
constructor(targetTaskIds: string[]) {
|
|
29
|
+
super('out_of_scope');
|
|
30
|
+
this.targetTaskIds = targetTaskIds;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TaskGroupOptions {
|
|
35
|
+
summarizeChatCtx?: boolean;
|
|
36
|
+
returnExceptions?: boolean;
|
|
37
|
+
chatCtx?: ChatContext;
|
|
38
|
+
onTaskCompleted?: (event: TaskCompletedEvent) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class TaskGroup extends AgentTask<TaskGroupResult> {
|
|
42
|
+
private _summarizeChatCtx: boolean;
|
|
43
|
+
private _returnExceptions: boolean;
|
|
44
|
+
private _visitedTasks = new Set<string>();
|
|
45
|
+
private _registeredFactories = new Map<string, FactoryInfo>();
|
|
46
|
+
private _taskCompletedCallback?: (event: TaskCompletedEvent) => Promise<void>;
|
|
47
|
+
private _currentTask?: AgentTask;
|
|
48
|
+
|
|
49
|
+
constructor(options: TaskGroupOptions = {}) {
|
|
50
|
+
const { summarizeChatCtx = true, returnExceptions = false, chatCtx, onTaskCompleted } = options;
|
|
51
|
+
|
|
52
|
+
super({ instructions: '*empty*', chatCtx });
|
|
53
|
+
|
|
54
|
+
this._summarizeChatCtx = summarizeChatCtx;
|
|
55
|
+
this._returnExceptions = returnExceptions;
|
|
56
|
+
this._taskCompletedCallback = onTaskCompleted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
add(task: () => AgentTask, { id, description }: { id: string; description: string }): this {
|
|
60
|
+
this._registeredFactories.set(id, { taskFactory: task, id, description });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async onEnter(): Promise<void> {
|
|
65
|
+
const taskStack = [...this._registeredFactories.keys()];
|
|
66
|
+
const taskResults: Record<string, unknown> = {};
|
|
67
|
+
|
|
68
|
+
while (taskStack.length > 0) {
|
|
69
|
+
const taskId = taskStack.shift()!;
|
|
70
|
+
const factoryInfo = this._registeredFactories.get(taskId)!;
|
|
71
|
+
|
|
72
|
+
this._currentTask = factoryInfo.taskFactory();
|
|
73
|
+
|
|
74
|
+
const sharedChatCtx = this._chatCtx.copy();
|
|
75
|
+
await this._currentTask.updateChatCtx(sharedChatCtx);
|
|
76
|
+
|
|
77
|
+
const outOfScopeTool = this.buildOutOfScopeTool(taskId);
|
|
78
|
+
if (outOfScopeTool) {
|
|
79
|
+
await this._currentTask.updateTools({
|
|
80
|
+
...this._currentTask.toolCtx,
|
|
81
|
+
out_of_scope: outOfScopeTool,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
this._visitedTasks.add(taskId);
|
|
87
|
+
const res = await this._currentTask.run();
|
|
88
|
+
taskResults[taskId] = res;
|
|
89
|
+
|
|
90
|
+
if (this._taskCompletedCallback) {
|
|
91
|
+
await this._taskCompletedCallback({
|
|
92
|
+
agentTask: this._currentTask,
|
|
93
|
+
taskId,
|
|
94
|
+
result: res,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e instanceof OutOfScopeError) {
|
|
99
|
+
taskStack.unshift(taskId);
|
|
100
|
+
for (let i = e.targetTaskIds.length - 1; i >= 0; i--) {
|
|
101
|
+
taskStack.unshift(e.targetTaskIds[i]!);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this._returnExceptions) {
|
|
107
|
+
taskResults[taskId] = e;
|
|
108
|
+
continue;
|
|
109
|
+
} else {
|
|
110
|
+
this.complete(e instanceof Error ? e : new Error(String(e)));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (this._summarizeChatCtx) {
|
|
118
|
+
const sessionLlm = this.session.llm;
|
|
119
|
+
if (!(sessionLlm instanceof LLM)) {
|
|
120
|
+
throw new Error('summarizeChatCtx requires a standard LLM on the session');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// TODO(parity): Add excludeConfigUpdate when AgentConfigUpdate is ported
|
|
124
|
+
const ctxToSummarize = this._chatCtx.copy({
|
|
125
|
+
excludeInstructions: true,
|
|
126
|
+
excludeHandoff: true,
|
|
127
|
+
excludeEmptyMessage: true,
|
|
128
|
+
excludeFunctionCall: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const summarizedChatCtx = await ctxToSummarize._summarize(sessionLlm, {
|
|
132
|
+
keepLastTurns: 0,
|
|
133
|
+
});
|
|
134
|
+
await this.updateChatCtx(summarizedChatCtx);
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
this.complete(new Error(`failed to summarize the chat_ctx: ${e}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.complete({ taskResults });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private buildOutOfScopeTool(activeTaskId: string) {
|
|
145
|
+
if (this._visitedTasks.size === 0) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const regressionTaskIds = new Set(this._visitedTasks);
|
|
150
|
+
regressionTaskIds.delete(activeTaskId);
|
|
151
|
+
|
|
152
|
+
if (regressionTaskIds.size === 0) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const taskRepr: Record<string, string> = {};
|
|
157
|
+
for (const [id, info] of this._registeredFactories) {
|
|
158
|
+
if (regressionTaskIds.has(id)) {
|
|
159
|
+
taskRepr[id] = info.description;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const taskIdValues = [...regressionTaskIds] as [string, ...string[]];
|
|
164
|
+
|
|
165
|
+
const description =
|
|
166
|
+
'Call to regress to other tasks according to what the user requested to modify, return the corresponding task ids. ' +
|
|
167
|
+
'For example, if the user wants to change their email and there is a task with id "email_task" with a description of "Collect the user\'s email", return the id ("get_email_task"). ' +
|
|
168
|
+
'If the user requests to regress to multiple tasks, such as changing their phone number and email, return both task ids in the order they were requested. ' +
|
|
169
|
+
`The following are the IDs and their corresponding task description. ${JSON.stringify(taskRepr)}`;
|
|
170
|
+
|
|
171
|
+
const currentTask = this._currentTask;
|
|
172
|
+
const registeredFactories = this._registeredFactories;
|
|
173
|
+
const visitedTasks = this._visitedTasks;
|
|
174
|
+
|
|
175
|
+
return tool({
|
|
176
|
+
description,
|
|
177
|
+
flags: ToolFlag.IGNORE_ON_ENTER,
|
|
178
|
+
parameters: z.object({
|
|
179
|
+
task_ids: z.array(z.enum(taskIdValues)).describe('The IDs of the tasks requested'),
|
|
180
|
+
}),
|
|
181
|
+
execute: async ({ task_ids }: { task_ids: string[] }) => {
|
|
182
|
+
for (const tid of task_ids) {
|
|
183
|
+
if (!registeredFactories.has(tid) || !visitedTasks.has(tid)) {
|
|
184
|
+
throw new ToolError(`Unable to regress, invalid task id ${tid}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (currentTask && !currentTask.done) {
|
|
189
|
+
currentTask.complete(new OutOfScopeError(task_ids));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/cpu.test.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { CGroupV1CpuMonitor, CGroupV2CpuMonitor, DefaultCpuMonitor, getCpuMonitor } from './cpu.js';
|
|
8
|
+
|
|
9
|
+
vi.mock('node:fs', () => ({
|
|
10
|
+
existsSync: vi.fn(() => false),
|
|
11
|
+
readFileSync: vi.fn(() => ''),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockExistsSync = vi.mocked(existsSync);
|
|
15
|
+
const mockReadFileSync = vi.mocked(readFileSync);
|
|
16
|
+
|
|
17
|
+
describe('cpu', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
delete process.env.NUM_CPUS;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
delete process.env.NUM_CPUS;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('getCpuMonitor', () => {
|
|
28
|
+
it('returns CGroupV2CpuMonitor when /sys/fs/cgroup/cpu.stat exists', () => {
|
|
29
|
+
mockExistsSync.mockImplementation((p) => p === '/sys/fs/cgroup/cpu.stat');
|
|
30
|
+
const monitor = getCpuMonitor();
|
|
31
|
+
expect(monitor).toBeInstanceOf(CGroupV2CpuMonitor);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns CGroupV1CpuMonitor when cgroup v1 paths exist', () => {
|
|
35
|
+
mockExistsSync.mockImplementation((p) => p === '/sys/fs/cgroup/cpuacct/cpuacct.usage');
|
|
36
|
+
const monitor = getCpuMonitor();
|
|
37
|
+
expect(monitor).toBeInstanceOf(CGroupV1CpuMonitor);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns DefaultCpuMonitor when no cgroup paths exist', () => {
|
|
41
|
+
mockExistsSync.mockReturnValue(false);
|
|
42
|
+
const monitor = getCpuMonitor();
|
|
43
|
+
expect(monitor).toBeInstanceOf(DefaultCpuMonitor);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('DefaultCpuMonitor', () => {
|
|
48
|
+
it('returns os.cpus().length for cpuCount', () => {
|
|
49
|
+
const monitor = new DefaultCpuMonitor();
|
|
50
|
+
expect(monitor.cpuCount()).toBe(os.cpus().length);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('respects NUM_CPUS env var', () => {
|
|
54
|
+
process.env.NUM_CPUS = '4.5';
|
|
55
|
+
const monitor = new DefaultCpuMonitor();
|
|
56
|
+
expect(monitor.cpuCount()).toBe(4.5);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('ignores invalid NUM_CPUS', () => {
|
|
60
|
+
process.env.NUM_CPUS = 'notanumber';
|
|
61
|
+
const monitor = new DefaultCpuMonitor();
|
|
62
|
+
expect(monitor.cpuCount()).toBe(os.cpus().length);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('cpuPercent returns value in [0, 1]', async () => {
|
|
66
|
+
const monitor = new DefaultCpuMonitor();
|
|
67
|
+
const result = await monitor.cpuPercent(50);
|
|
68
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
69
|
+
expect(result).toBeLessThanOrEqual(1);
|
|
70
|
+
}, 10_000);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('CGroupV2CpuMonitor', () => {
|
|
74
|
+
it('returns quota/period for cpuCount', () => {
|
|
75
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
76
|
+
if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';
|
|
77
|
+
return '';
|
|
78
|
+
});
|
|
79
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
80
|
+
expect(monitor.cpuCount()).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('falls back to os.cpus().length when quota is max', () => {
|
|
84
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
85
|
+
if (String(p) === '/sys/fs/cgroup/cpu.max') return 'max 100000';
|
|
86
|
+
return '';
|
|
87
|
+
});
|
|
88
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
89
|
+
expect(monitor.cpuCount()).toBe(os.cpus().length);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles missing cpu.max gracefully', () => {
|
|
93
|
+
mockReadFileSync.mockImplementation(() => {
|
|
94
|
+
throw new Error('ENOENT');
|
|
95
|
+
});
|
|
96
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
97
|
+
expect(monitor.cpuCount()).toBe(os.cpus().length);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('respects NUM_CPUS env var', () => {
|
|
101
|
+
process.env.NUM_CPUS = '3';
|
|
102
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
103
|
+
expect(monitor.cpuCount()).toBe(3);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('cpuPercent computes correct value from usage_usec deltas', async () => {
|
|
107
|
+
let callCount = 0;
|
|
108
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
109
|
+
if (String(p) === '/sys/fs/cgroup/cpu.stat') {
|
|
110
|
+
callCount++;
|
|
111
|
+
// Two reads: 1,000,000 usec apart => 1s of CPU usage over the interval
|
|
112
|
+
return callCount <= 1
|
|
113
|
+
? 'usage_usec 1000000\nuser_usec 800000\nsystem_usec 200000'
|
|
114
|
+
: 'usage_usec 2000000\nuser_usec 1600000\nsystem_usec 400000';
|
|
115
|
+
}
|
|
116
|
+
if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';
|
|
117
|
+
return '';
|
|
118
|
+
});
|
|
119
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
120
|
+
// interval=100ms, 2 cpus, 1s of usage => 1/(0.1*2) = 5, clamped to 1
|
|
121
|
+
const result = await monitor.cpuPercent(100);
|
|
122
|
+
expect(result).toBe(1);
|
|
123
|
+
}, 10_000);
|
|
124
|
+
|
|
125
|
+
it('cpuPercent returns fractional load', async () => {
|
|
126
|
+
let callCount = 0;
|
|
127
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
128
|
+
if (String(p) === '/sys/fs/cgroup/cpu.stat') {
|
|
129
|
+
callCount++;
|
|
130
|
+
// 50,000 usec delta => 0.05s of CPU over 0.1s on 2 cpus => 0.05/(0.1*2) = 0.25
|
|
131
|
+
return callCount <= 1 ? 'usage_usec 1000000\n' : 'usage_usec 1050000\n';
|
|
132
|
+
}
|
|
133
|
+
if (String(p) === '/sys/fs/cgroup/cpu.max') return '200000 100000';
|
|
134
|
+
return '';
|
|
135
|
+
});
|
|
136
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
137
|
+
const result = await monitor.cpuPercent(100);
|
|
138
|
+
expect(result).toBeCloseTo(0.25, 1);
|
|
139
|
+
}, 10_000);
|
|
140
|
+
|
|
141
|
+
it('throws when usage_usec is missing from cpu.stat', async () => {
|
|
142
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
143
|
+
if (String(p) === '/sys/fs/cgroup/cpu.stat') return 'user_usec 800000\nsystem_usec 200000';
|
|
144
|
+
return '';
|
|
145
|
+
});
|
|
146
|
+
const monitor = new CGroupV2CpuMonitor();
|
|
147
|
+
await expect(() => monitor.cpuPercent(50)).rejects.toThrow('Failed to read CPU usage');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('CGroupV1CpuMonitor', () => {
|
|
152
|
+
it('returns quota/period for cpuCount', () => {
|
|
153
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
154
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '200000';
|
|
155
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';
|
|
156
|
+
return '';
|
|
157
|
+
});
|
|
158
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
159
|
+
expect(monitor.cpuCount()).toBe(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('defaults to 2.0 when quota is -1', () => {
|
|
163
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
164
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '-1';
|
|
165
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';
|
|
166
|
+
return '';
|
|
167
|
+
});
|
|
168
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
169
|
+
expect(monitor.cpuCount()).toBe(2.0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('defaults to 2.0 when quota file is unreadable', () => {
|
|
173
|
+
mockReadFileSync.mockImplementation(() => {
|
|
174
|
+
throw new Error('ENOENT');
|
|
175
|
+
});
|
|
176
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
177
|
+
expect(monitor.cpuCount()).toBe(2.0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('clamps cpuCount to minimum 1.0', () => {
|
|
181
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
182
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '50000';
|
|
183
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';
|
|
184
|
+
return '';
|
|
185
|
+
});
|
|
186
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
187
|
+
expect(monitor.cpuCount()).toBe(1.0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('respects NUM_CPUS env var', () => {
|
|
191
|
+
process.env.NUM_CPUS = '8';
|
|
192
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
193
|
+
expect(monitor.cpuCount()).toBe(8);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('cpuPercent computes correct value from nanosecond deltas', async () => {
|
|
197
|
+
let callCount = 0;
|
|
198
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
199
|
+
if (String(p) === '/sys/fs/cgroup/cpuacct/cpuacct.usage') {
|
|
200
|
+
callCount++;
|
|
201
|
+
// 100_000_000 ns delta = 0.1s CPU over 0.1s interval on 2 cpus => 0.1/(0.1*2) = 0.5
|
|
202
|
+
return callCount <= 1 ? '1000000000' : '1100000000';
|
|
203
|
+
}
|
|
204
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '200000';
|
|
205
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';
|
|
206
|
+
return '';
|
|
207
|
+
});
|
|
208
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
209
|
+
const result = await monitor.cpuPercent(100);
|
|
210
|
+
expect(result).toBeCloseTo(0.5, 1);
|
|
211
|
+
}, 10_000);
|
|
212
|
+
|
|
213
|
+
it('clamps cpuPercent output to [0, 1]', async () => {
|
|
214
|
+
let callCount = 0;
|
|
215
|
+
mockReadFileSync.mockImplementation((p) => {
|
|
216
|
+
if (String(p) === '/sys/fs/cgroup/cpuacct/cpuacct.usage') {
|
|
217
|
+
callCount++;
|
|
218
|
+
// Huge delta => would exceed 1.0 without clamping
|
|
219
|
+
return callCount <= 1 ? '0' : '10000000000';
|
|
220
|
+
}
|
|
221
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_quota_us') return '100000';
|
|
222
|
+
if (String(p) === '/sys/fs/cgroup/cpu/cpu.cfs_period_us') return '100000';
|
|
223
|
+
return '';
|
|
224
|
+
});
|
|
225
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
226
|
+
const result = await monitor.cpuPercent(100);
|
|
227
|
+
expect(result).toBeLessThanOrEqual(1);
|
|
228
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
229
|
+
}, 10_000);
|
|
230
|
+
|
|
231
|
+
it('throws when cpuacct.usage is unreadable', async () => {
|
|
232
|
+
mockReadFileSync.mockImplementation(() => {
|
|
233
|
+
throw new Error('ENOENT');
|
|
234
|
+
});
|
|
235
|
+
const monitor = new CGroupV1CpuMonitor();
|
|
236
|
+
await expect(() => monitor.cpuPercent(50)).rejects.toThrow('Failed to read cpuacct.usage');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
package/src/cpu.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
|
|
7
|
+
/** @internal */
|
|
8
|
+
export interface CpuMonitor {
|
|
9
|
+
cpuCount(): number;
|
|
10
|
+
cpuPercent(intervalMs: number): Promise<number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cpuCountFromEnv(): number | undefined {
|
|
14
|
+
const raw = process.env.NUM_CPUS;
|
|
15
|
+
if (raw === undefined) return undefined;
|
|
16
|
+
const parsed = parseFloat(raw);
|
|
17
|
+
if (Number.isNaN(parsed)) {
|
|
18
|
+
console.warn('Failed to parse NUM_CPUS from environment');
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @internal */
|
|
25
|
+
export class DefaultCpuMonitor implements CpuMonitor {
|
|
26
|
+
cpuCount(): number {
|
|
27
|
+
return cpuCountFromEnv() ?? (os.cpus().length || 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
cpuPercent(intervalMs: number): Promise<number> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const cpus1 = os.cpus();
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
const cpus2 = os.cpus();
|
|
35
|
+
let idle = 0;
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (let i = 0; i < cpus1.length; i++) {
|
|
38
|
+
const cpu1 = cpus1[i]!.times;
|
|
39
|
+
const cpu2 = cpus2[i]!.times;
|
|
40
|
+
idle += cpu2.idle - cpu1.idle;
|
|
41
|
+
const total1 = Object.values(cpu1).reduce((acc, v) => acc + v, 0);
|
|
42
|
+
const total2 = Object.values(cpu2).reduce((acc, v) => acc + v, 0);
|
|
43
|
+
total += total2 - total1;
|
|
44
|
+
}
|
|
45
|
+
resolve(total === 0 ? 0 : Math.max(Math.min(+(1 - idle / total).toFixed(2), 1), 0));
|
|
46
|
+
}, intervalMs);
|
|
47
|
+
timer.unref();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @internal */
|
|
53
|
+
export class CGroupV2CpuMonitor implements CpuMonitor {
|
|
54
|
+
cpuCount(): number {
|
|
55
|
+
const envCpus = cpuCountFromEnv();
|
|
56
|
+
if (envCpus !== undefined) return envCpus;
|
|
57
|
+
const [quota, period] = this.#readCpuMax();
|
|
58
|
+
if (quota === 'max') return os.cpus().length || 1;
|
|
59
|
+
return parseInt(quota) / period;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
cpuPercent(intervalMs: number): Promise<number> {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const usageStart = this.#readCpuUsage();
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
try {
|
|
67
|
+
const usageEnd = this.#readCpuUsage();
|
|
68
|
+
const usageDiffUsec = usageEnd - usageStart;
|
|
69
|
+
const usageSeconds = usageDiffUsec / 1_000_000;
|
|
70
|
+
const numCpus = this.cpuCount();
|
|
71
|
+
const intervalSeconds = intervalMs / 1000;
|
|
72
|
+
const percent = usageSeconds / (intervalSeconds * numCpus);
|
|
73
|
+
resolve(Math.max(Math.min(percent, 1), 0));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
reject(e);
|
|
76
|
+
}
|
|
77
|
+
}, intervalMs);
|
|
78
|
+
timer.unref();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#readCpuMax(): [string, number] {
|
|
83
|
+
try {
|
|
84
|
+
const data = readFileSync('/sys/fs/cgroup/cpu.max', 'utf-8').trim().split(/\s+/);
|
|
85
|
+
const quota = data[0] ?? 'max';
|
|
86
|
+
const period = data[1] ? parseInt(data[1]) : 100_000;
|
|
87
|
+
return [quota, Number.isNaN(period) ? 100_000 : period];
|
|
88
|
+
} catch {
|
|
89
|
+
return ['max', 100_000];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#readCpuUsage(): number {
|
|
94
|
+
const content = readFileSync('/sys/fs/cgroup/cpu.stat', 'utf-8');
|
|
95
|
+
for (const line of content.split('\n')) {
|
|
96
|
+
if (line.startsWith('usage_usec')) {
|
|
97
|
+
return parseInt(line.split(/\s+/)[1]!);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error('Failed to read CPU usage from /sys/fs/cgroup/cpu.stat');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @internal */
|
|
105
|
+
export class CGroupV1CpuMonitor implements CpuMonitor {
|
|
106
|
+
cpuCount(): number {
|
|
107
|
+
const envCpus = cpuCountFromEnv();
|
|
108
|
+
if (envCpus !== undefined) return envCpus;
|
|
109
|
+
const [quota, period] = this.#readCfsQuotaAndPeriod();
|
|
110
|
+
if (quota === null || quota < 0 || period === null || period <= 0) {
|
|
111
|
+
// do not use the host CPU count as it could overstate the number available to the container
|
|
112
|
+
return 2.0;
|
|
113
|
+
}
|
|
114
|
+
return Math.max(quota / period, 1.0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cpuPercent(intervalMs: number): Promise<number> {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const usageStart = this.#readCpuacctUsage();
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
try {
|
|
122
|
+
const usageEnd = this.#readCpuacctUsage();
|
|
123
|
+
const usageDiffNs = usageEnd - usageStart;
|
|
124
|
+
const usageSeconds = usageDiffNs / 1_000_000_000;
|
|
125
|
+
const numCpus = this.cpuCount();
|
|
126
|
+
const intervalSeconds = intervalMs / 1000;
|
|
127
|
+
const percent = usageSeconds / (intervalSeconds * numCpus);
|
|
128
|
+
resolve(Math.max(Math.min(percent, 1.0), 0.0));
|
|
129
|
+
} catch (e) {
|
|
130
|
+
reject(e);
|
|
131
|
+
}
|
|
132
|
+
}, intervalMs);
|
|
133
|
+
timer.unref();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#readCfsQuotaAndPeriod(): [number | null, number | null] {
|
|
138
|
+
const quota = readFirstInt('/sys/fs/cgroup/cpu/cpu.cfs_quota_us');
|
|
139
|
+
const period = readFirstInt('/sys/fs/cgroup/cpu/cpu.cfs_period_us');
|
|
140
|
+
return [quota, period];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#readCpuacctUsage(): number {
|
|
144
|
+
const value = readFirstInt('/sys/fs/cgroup/cpuacct/cpuacct.usage');
|
|
145
|
+
if (value === null) {
|
|
146
|
+
throw new Error('Failed to read cpuacct.usage for cgroup v1');
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readFirstInt(path: string): number | null {
|
|
153
|
+
try {
|
|
154
|
+
const val = parseInt(readFileSync(path, 'utf-8').trim());
|
|
155
|
+
return Number.isNaN(val) ? null : val;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isCGroupV2(): boolean {
|
|
162
|
+
return existsSync('/sys/fs/cgroup/cpu.stat');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isCGroupV1(): boolean {
|
|
166
|
+
return existsSync('/sys/fs/cgroup/cpuacct/cpuacct.usage');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getCpuMonitor(): CpuMonitor {
|
|
170
|
+
if (isCGroupV2()) return new CGroupV2CpuMonitor();
|
|
171
|
+
if (isCGroupV1()) return new CGroupV1CpuMonitor();
|
|
172
|
+
return new DefaultCpuMonitor();
|
|
173
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @see {@link https://docs.livekit.io/agents/overview | LiveKit Agents documentation}
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
|
+
import * as beta from './beta/index.js';
|
|
12
13
|
import * as cli from './cli.js';
|
|
13
14
|
import * as inference from './inference/index.js';
|
|
14
15
|
import * as ipc from './ipc/index.js';
|
|
@@ -37,4 +38,4 @@ export * from './version.js';
|
|
|
37
38
|
export { createTimedString, isTimedString, type TimedString } from './voice/io.js';
|
|
38
39
|
export * from './worker.js';
|
|
39
40
|
|
|
40
|
-
export { cli, inference, ipc, llm, metrics, stream, stt, telemetry, tokenize, tts, voice };
|
|
41
|
+
export { beta, cli, inference, ipc, llm, metrics, stream, stt, telemetry, tokenize, tts, voice };
|
package/src/inference/llm.ts
CHANGED
|
@@ -17,6 +17,8 @@ import { type AnyString, createAccessToken } from './utils.js';
|
|
|
17
17
|
const DEFAULT_BASE_URL = 'https://agent-gateway.livekit.cloud/v1';
|
|
18
18
|
|
|
19
19
|
export type OpenAIModels =
|
|
20
|
+
| 'openai/gpt-5.4'
|
|
21
|
+
| 'openai/gpt-5.3-chat-latest'
|
|
20
22
|
| 'openai/gpt-5.2'
|
|
21
23
|
| 'openai/gpt-5.2-chat-latest'
|
|
22
24
|
| 'openai/gpt-5.1'
|
package/src/inference/tts.ts
CHANGED
|
@@ -62,7 +62,14 @@ export interface DeepgramTTSOptions {}
|
|
|
62
62
|
|
|
63
63
|
export interface RimeOptions {}
|
|
64
64
|
|
|
65
|
-
export interface InworldOptions {
|
|
65
|
+
export interface InworldOptions {
|
|
66
|
+
/** Controls how fast the voice speaks. 1.0 is normal speed, 0.5 is half, 1.5 is 1.5x. Default: 1.0. */
|
|
67
|
+
speaking_rate?: number;
|
|
68
|
+
/** Controls randomness in the output. Recommended between 0.6 and 1.1. Default: 1.1. */
|
|
69
|
+
temperature?: number;
|
|
70
|
+
/** Controls text normalization. "ON" expands numbers, dates, abbreviations. "OFF" reads text as written. Default: "ON". */
|
|
71
|
+
text_normalization?: 'ON' | 'OFF';
|
|
72
|
+
}
|
|
66
73
|
|
|
67
74
|
type _TTSModels =
|
|
68
75
|
| CartesiaModels
|