@livekit/agents 1.0.9 → 1.0.11
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/audio.cjs +3 -3
- package/dist/audio.cjs.map +1 -1
- package/dist/audio.d.cts +1 -1
- package/dist/audio.d.ts +1 -1
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +2 -2
- package/dist/audio.js.map +1 -1
- package/dist/llm/llm.cjs +7 -4
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.d.ts.map +1 -1
- package/dist/llm/llm.js +7 -4
- package/dist/llm/llm.js.map +1 -1
- package/dist/metrics/base.cjs.map +1 -1
- package/dist/metrics/base.d.cts +23 -18
- package/dist/metrics/base.d.ts +23 -18
- package/dist/metrics/base.d.ts.map +1 -1
- package/dist/metrics/usage_collector.cjs +2 -2
- package/dist/metrics/usage_collector.cjs.map +1 -1
- package/dist/metrics/usage_collector.d.cts +1 -1
- package/dist/metrics/usage_collector.d.ts +1 -1
- package/dist/metrics/usage_collector.d.ts.map +1 -1
- package/dist/metrics/usage_collector.js +2 -2
- package/dist/metrics/usage_collector.js.map +1 -1
- package/dist/metrics/utils.cjs +14 -7
- package/dist/metrics/utils.cjs.map +1 -1
- package/dist/metrics/utils.d.ts.map +1 -1
- package/dist/metrics/utils.js +14 -7
- package/dist/metrics/utils.js.map +1 -1
- package/dist/stt/stt.cjs +5 -5
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.js +6 -6
- package/dist/stt/stt.js.map +1 -1
- package/dist/tts/tts.cjs +11 -10
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +11 -10
- package/dist/tts/tts.js.map +1 -1
- package/dist/vad.cjs +5 -5
- package/dist/vad.cjs.map +1 -1
- package/dist/vad.js +5 -5
- package/dist/vad.js.map +1 -1
- package/dist/voice/agent_activity.cjs +7 -4
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +7 -4
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/generation_tools.test.cjs +236 -0
- package/dist/voice/generation_tools.test.cjs.map +1 -0
- package/dist/voice/generation_tools.test.js +235 -0
- package/dist/voice/generation_tools.test.js.map +1 -0
- package/dist/voice/index.cjs +3 -1
- package/dist/voice/index.cjs.map +1 -1
- package/dist/voice/index.d.cts +1 -0
- package/dist/voice/index.d.ts +1 -0
- package/dist/voice/index.d.ts.map +1 -1
- package/dist/voice/index.js +1 -0
- package/dist/voice/index.js.map +1 -1
- package/package.json +1 -1
- package/src/audio.ts +1 -1
- package/src/llm/llm.ts +7 -4
- package/src/metrics/base.ts +23 -18
- package/src/metrics/usage_collector.ts +3 -3
- package/src/metrics/utils.ts +16 -7
- package/src/stt/stt.ts +6 -6
- package/src/tts/tts.ts +11 -10
- package/src/vad.ts +5 -5
- package/src/voice/agent_activity.ts +8 -4
- package/src/voice/generation_tools.test.ts +268 -0
- package/src/voice/index.ts +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var import_web = require("stream/web");
|
|
3
|
+
var import_vitest = require("vitest");
|
|
4
|
+
var import_zod = require("zod");
|
|
5
|
+
var import_llm = require("../llm/index.cjs");
|
|
6
|
+
var import_log = require("../log.cjs");
|
|
7
|
+
var import_utils = require("../utils.cjs");
|
|
8
|
+
var import_generation = require("./generation.cjs");
|
|
9
|
+
function createStringStream(chunks, delayMs = 0) {
|
|
10
|
+
return new import_web.ReadableStream({
|
|
11
|
+
async start(controller) {
|
|
12
|
+
for (const c of chunks) {
|
|
13
|
+
if (delayMs > 0) {
|
|
14
|
+
await (0, import_utils.delay)(delayMs);
|
|
15
|
+
}
|
|
16
|
+
controller.enqueue(c);
|
|
17
|
+
}
|
|
18
|
+
controller.close();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function createFunctionCallStream(fc) {
|
|
23
|
+
return new import_web.ReadableStream({
|
|
24
|
+
start(controller) {
|
|
25
|
+
controller.enqueue(fc);
|
|
26
|
+
controller.close();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function createFunctionCallStreamFromArray(fcs) {
|
|
31
|
+
return new import_web.ReadableStream({
|
|
32
|
+
start(controller) {
|
|
33
|
+
for (const fc of fcs) {
|
|
34
|
+
controller.enqueue(fc);
|
|
35
|
+
}
|
|
36
|
+
controller.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
(0, import_vitest.describe)("Generation + Tool Execution", () => {
|
|
41
|
+
(0, import_log.initializeLogger)({ pretty: false, level: "silent" });
|
|
42
|
+
(0, import_vitest.it)("should not abort tool when preamble forwarders are cleaned up", async () => {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const replyAbortController = new AbortController();
|
|
45
|
+
const forwarderController = new AbortController();
|
|
46
|
+
const chunks = Array.from({ length: 50 }, () => `Hi.`);
|
|
47
|
+
const fullPreambleText = chunks.join("");
|
|
48
|
+
const preamble = createStringStream(chunks, 20);
|
|
49
|
+
const [textForwardTask, textOut] = (0, import_generation.performTextForwarding)(
|
|
50
|
+
preamble,
|
|
51
|
+
forwarderController,
|
|
52
|
+
null
|
|
53
|
+
);
|
|
54
|
+
let toolAborted = false;
|
|
55
|
+
const getWeather = (0, import_llm.tool)({
|
|
56
|
+
description: "weather",
|
|
57
|
+
parameters: import_zod.z.object({ location: import_zod.z.string() }),
|
|
58
|
+
execute: async ({ location }, { abortSignal }) => {
|
|
59
|
+
if (abortSignal) {
|
|
60
|
+
abortSignal.addEventListener("abort", () => {
|
|
61
|
+
toolAborted = true;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
await (0, import_utils.delay)(6e3);
|
|
65
|
+
return `Sunny in ${location}`;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const fc = import_llm.FunctionCall.create({
|
|
69
|
+
callId: "call_1",
|
|
70
|
+
name: "getWeather",
|
|
71
|
+
args: JSON.stringify({ location: "San Francisco" })
|
|
72
|
+
});
|
|
73
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
74
|
+
const [execTask, toolOutput] = (0, import_generation.performToolExecutions)({
|
|
75
|
+
session: {},
|
|
76
|
+
speechHandle: { id: "speech_test", _itemAdded: () => {
|
|
77
|
+
} },
|
|
78
|
+
toolCtx: { getWeather },
|
|
79
|
+
toolCallStream,
|
|
80
|
+
controller: replyAbortController,
|
|
81
|
+
onToolExecutionStarted: () => {
|
|
82
|
+
},
|
|
83
|
+
onToolExecutionCompleted: () => {
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
await toolOutput.firstToolStartedFuture.await;
|
|
87
|
+
await (0, import_utils.delay)(100);
|
|
88
|
+
await (0, import_utils.cancelAndWait)([textForwardTask], 5e3);
|
|
89
|
+
await execTask.result;
|
|
90
|
+
(0, import_vitest.expect)(toolOutput.output.length).toBe(1);
|
|
91
|
+
const out = toolOutput.output[0];
|
|
92
|
+
(0, import_vitest.expect)((_a = out.toolCallOutput) == null ? void 0 : _a.isError).toBe(false);
|
|
93
|
+
(0, import_vitest.expect)((_b = out.toolCallOutput) == null ? void 0 : _b.output).toContain("Sunny in San Francisco");
|
|
94
|
+
(0, import_vitest.expect)(textOut.text).not.toBe(fullPreambleText);
|
|
95
|
+
(0, import_vitest.expect)(toolAborted).toBe(false);
|
|
96
|
+
}, 3e4);
|
|
97
|
+
(0, import_vitest.it)("should return basic tool execution output", async () => {
|
|
98
|
+
var _a, _b;
|
|
99
|
+
const replyAbortController = new AbortController();
|
|
100
|
+
const echo = (0, import_llm.tool)({
|
|
101
|
+
description: "echo",
|
|
102
|
+
parameters: import_zod.z.object({ msg: import_zod.z.string() }),
|
|
103
|
+
execute: async ({ msg }) => `echo: ${msg}`
|
|
104
|
+
});
|
|
105
|
+
const fc = import_llm.FunctionCall.create({
|
|
106
|
+
callId: "call_2",
|
|
107
|
+
name: "echo",
|
|
108
|
+
args: JSON.stringify({ msg: "hello" })
|
|
109
|
+
});
|
|
110
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
111
|
+
const [execTask, toolOutput] = (0, import_generation.performToolExecutions)({
|
|
112
|
+
session: {},
|
|
113
|
+
speechHandle: { id: "speech_test2", _itemAdded: () => {
|
|
114
|
+
} },
|
|
115
|
+
toolCtx: { echo },
|
|
116
|
+
toolCallStream,
|
|
117
|
+
controller: replyAbortController
|
|
118
|
+
});
|
|
119
|
+
await execTask.result;
|
|
120
|
+
(0, import_vitest.expect)(toolOutput.output.length).toBe(1);
|
|
121
|
+
const out = toolOutput.output[0];
|
|
122
|
+
(0, import_vitest.expect)((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(false);
|
|
123
|
+
(0, import_vitest.expect)((_b = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _b.output).toContain("echo: hello");
|
|
124
|
+
});
|
|
125
|
+
(0, import_vitest.it)("should abort tool when reply is aborted mid-execution", async () => {
|
|
126
|
+
var _a;
|
|
127
|
+
const replyAbortController = new AbortController();
|
|
128
|
+
let aborted = false;
|
|
129
|
+
const longOp = (0, import_llm.tool)({
|
|
130
|
+
description: "longOp",
|
|
131
|
+
parameters: import_zod.z.object({ ms: import_zod.z.number() }),
|
|
132
|
+
execute: async ({ ms }, { abortSignal }) => {
|
|
133
|
+
if (abortSignal) {
|
|
134
|
+
abortSignal.addEventListener("abort", () => {
|
|
135
|
+
aborted = true;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
await (0, import_utils.delay)(ms);
|
|
139
|
+
return "done";
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
const fc = import_llm.FunctionCall.create({
|
|
143
|
+
callId: "call_abort_1",
|
|
144
|
+
name: "longOp",
|
|
145
|
+
args: JSON.stringify({ ms: 5e3 })
|
|
146
|
+
});
|
|
147
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
148
|
+
const [execTask, toolOutput] = (0, import_generation.performToolExecutions)({
|
|
149
|
+
session: {},
|
|
150
|
+
speechHandle: { id: "speech_abort", _itemAdded: () => {
|
|
151
|
+
} },
|
|
152
|
+
toolCtx: { longOp },
|
|
153
|
+
toolCallStream,
|
|
154
|
+
controller: replyAbortController
|
|
155
|
+
});
|
|
156
|
+
await toolOutput.firstToolStartedFuture.await;
|
|
157
|
+
replyAbortController.abort();
|
|
158
|
+
await execTask.result;
|
|
159
|
+
(0, import_vitest.expect)(aborted).toBe(true);
|
|
160
|
+
(0, import_vitest.expect)(toolOutput.output.length).toBe(1);
|
|
161
|
+
const out = toolOutput.output[0];
|
|
162
|
+
(0, import_vitest.expect)((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(true);
|
|
163
|
+
}, 2e4);
|
|
164
|
+
(0, import_vitest.it)("should return error output on invalid tool args (zod validation failure)", async () => {
|
|
165
|
+
var _a;
|
|
166
|
+
const replyAbortController = new AbortController();
|
|
167
|
+
const echo = (0, import_llm.tool)({
|
|
168
|
+
description: "echo",
|
|
169
|
+
parameters: import_zod.z.object({ msg: import_zod.z.string() }),
|
|
170
|
+
execute: async ({ msg }) => `echo: ${msg}`
|
|
171
|
+
});
|
|
172
|
+
const fc = import_llm.FunctionCall.create({
|
|
173
|
+
callId: "call_invalid_args",
|
|
174
|
+
name: "echo",
|
|
175
|
+
args: JSON.stringify({ msg: 123 })
|
|
176
|
+
});
|
|
177
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
178
|
+
const [execTask, toolOutput] = (0, import_generation.performToolExecutions)({
|
|
179
|
+
session: {},
|
|
180
|
+
speechHandle: { id: "speech_invalid", _itemAdded: () => {
|
|
181
|
+
} },
|
|
182
|
+
toolCtx: { echo },
|
|
183
|
+
toolCallStream,
|
|
184
|
+
controller: replyAbortController
|
|
185
|
+
});
|
|
186
|
+
await execTask.result;
|
|
187
|
+
(0, import_vitest.expect)(toolOutput.output.length).toBe(1);
|
|
188
|
+
const out = toolOutput.output[0];
|
|
189
|
+
(0, import_vitest.expect)((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
(0, import_vitest.it)("should handle multiple tool calls within a single stream", async () => {
|
|
192
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
193
|
+
const replyAbortController = new AbortController();
|
|
194
|
+
const sum = (0, import_llm.tool)({
|
|
195
|
+
description: "sum",
|
|
196
|
+
parameters: import_zod.z.object({ a: import_zod.z.number(), b: import_zod.z.number() }),
|
|
197
|
+
execute: async ({ a, b }) => a + b
|
|
198
|
+
});
|
|
199
|
+
const upper = (0, import_llm.tool)({
|
|
200
|
+
description: "upper",
|
|
201
|
+
parameters: import_zod.z.object({ s: import_zod.z.string() }),
|
|
202
|
+
execute: async ({ s }) => s.toUpperCase()
|
|
203
|
+
});
|
|
204
|
+
const fc1 = import_llm.FunctionCall.create({
|
|
205
|
+
callId: "call_multi_1",
|
|
206
|
+
name: "sum",
|
|
207
|
+
args: JSON.stringify({ a: 2, b: 3 })
|
|
208
|
+
});
|
|
209
|
+
const fc2 = import_llm.FunctionCall.create({
|
|
210
|
+
callId: "call_multi_2",
|
|
211
|
+
name: "upper",
|
|
212
|
+
args: JSON.stringify({ s: "hey" })
|
|
213
|
+
});
|
|
214
|
+
const toolCallStream = createFunctionCallStreamFromArray([fc1, fc2]);
|
|
215
|
+
const [execTask, toolOutput] = (0, import_generation.performToolExecutions)({
|
|
216
|
+
session: {},
|
|
217
|
+
speechHandle: { id: "speech_multi", _itemAdded: () => {
|
|
218
|
+
} },
|
|
219
|
+
toolCtx: { sum, upper },
|
|
220
|
+
toolCallStream,
|
|
221
|
+
controller: replyAbortController
|
|
222
|
+
});
|
|
223
|
+
await execTask.result;
|
|
224
|
+
(0, import_vitest.expect)(toolOutput.output.length).toBe(2);
|
|
225
|
+
const sorted = [...toolOutput.output].sort(
|
|
226
|
+
(a, b) => a.toolCall.callId.localeCompare(b.toolCall.callId)
|
|
227
|
+
);
|
|
228
|
+
(0, import_vitest.expect)((_a = sorted[0]) == null ? void 0 : _a.toolCall.name).toBe("sum");
|
|
229
|
+
(0, import_vitest.expect)((_c = (_b = sorted[0]) == null ? void 0 : _b.toolCallOutput) == null ? void 0 : _c.isError).toBe(false);
|
|
230
|
+
(0, import_vitest.expect)((_e = (_d = sorted[0]) == null ? void 0 : _d.toolCallOutput) == null ? void 0 : _e.output).toBe("5");
|
|
231
|
+
(0, import_vitest.expect)((_f = sorted[1]) == null ? void 0 : _f.toolCall.name).toBe("upper");
|
|
232
|
+
(0, import_vitest.expect)((_h = (_g = sorted[1]) == null ? void 0 : _g.toolCallOutput) == null ? void 0 : _h.isError).toBe(false);
|
|
233
|
+
(0, import_vitest.expect)((_j = (_i = sorted[1]) == null ? void 0 : _i.toolCallOutput) == null ? void 0 : _j.output).toBe('"HEY"');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
//# sourceMappingURL=generation_tools.test.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/voice/generation_tools.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { ReadableStream as NodeReadableStream } from 'stream/web';\nimport { describe, expect, it } from 'vitest';\nimport { z } from 'zod';\nimport { FunctionCall, tool } from '../llm/index.js';\nimport { initializeLogger } from '../log.js';\nimport type { Task } from '../utils.js';\nimport { cancelAndWait, delay } from '../utils.js';\nimport { type _TextOut, performTextForwarding, performToolExecutions } from './generation.js';\n\nfunction createStringStream(chunks: string[], delayMs: number = 0): NodeReadableStream<string> {\n return new NodeReadableStream<string>({\n async start(controller) {\n for (const c of chunks) {\n if (delayMs > 0) {\n await delay(delayMs);\n }\n controller.enqueue(c);\n }\n controller.close();\n },\n });\n}\n\nfunction createFunctionCallStream(fc: FunctionCall): NodeReadableStream<FunctionCall> {\n return new NodeReadableStream<FunctionCall>({\n start(controller) {\n controller.enqueue(fc);\n controller.close();\n },\n });\n}\n\nfunction createFunctionCallStreamFromArray(fcs: FunctionCall[]): NodeReadableStream<FunctionCall> {\n return new NodeReadableStream<FunctionCall>({\n start(controller) {\n for (const fc of fcs) {\n controller.enqueue(fc);\n }\n controller.close();\n },\n });\n}\n\ndescribe('Generation + Tool Execution', () => {\n initializeLogger({ pretty: false, level: 'silent' });\n\n it('should not abort tool when preamble forwarders are cleaned up', async () => {\n const replyAbortController = new AbortController();\n const forwarderController = new AbortController();\n\n const chunks = Array.from({ length: 50 }, () => `Hi.`);\n const fullPreambleText = chunks.join('');\n const preamble = createStringStream(chunks, 20);\n const [textForwardTask, textOut]: [Task<void>, _TextOut] = performTextForwarding(\n preamble,\n forwarderController,\n null,\n );\n\n // Tool that takes > 5 seconds\n let toolAborted = false;\n const getWeather = tool({\n description: 'weather',\n parameters: z.object({ location: z.string() }),\n execute: async ({ location }, { abortSignal }) => {\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => {\n toolAborted = true;\n });\n }\n // 6s delay\n await delay(6000);\n return `Sunny in ${location}`;\n },\n });\n\n const fc = FunctionCall.create({\n callId: 'call_1',\n name: 'getWeather',\n args: JSON.stringify({ location: 'San Francisco' }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_test', _itemAdded: () => {} } as any,\n toolCtx: { getWeather } as any,\n toolCallStream,\n controller: replyAbortController,\n onToolExecutionStarted: () => {},\n onToolExecutionCompleted: () => {},\n });\n\n // Ensure tool has started, then cancel forwarders mid-stream (without aborting parent AbortController)\n await toolOutput.firstToolStartedFuture.await;\n await delay(100);\n await cancelAndWait([textForwardTask], 5000);\n\n await execTask.result;\n\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0]!;\n expect(out.toolCallOutput?.isError).toBe(false);\n expect(out.toolCallOutput?.output).toContain('Sunny in San Francisco');\n // Forwarder should have been cancelled before finishing all preamble chunks\n expect(textOut.text).not.toBe(fullPreambleText);\n // Tool's abort signal must not have fired\n expect(toolAborted).toBe(false);\n }, 30_000);\n\n it('should return basic tool execution output', async () => {\n const replyAbortController = new AbortController();\n\n const echo = tool({\n description: 'echo',\n parameters: z.object({ msg: z.string() }),\n execute: async ({ msg }) => `echo: ${msg}`,\n });\n\n const fc = FunctionCall.create({\n callId: 'call_2',\n name: 'echo',\n args: JSON.stringify({ msg: 'hello' }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_test2', _itemAdded: () => {} } as any,\n toolCtx: { echo } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(false);\n expect(out?.toolCallOutput?.output).toContain('echo: hello');\n });\n\n it('should abort tool when reply is aborted mid-execution', async () => {\n const replyAbortController = new AbortController();\n\n let aborted = false;\n const longOp = tool({\n description: 'longOp',\n parameters: z.object({ ms: z.number() }),\n execute: async ({ ms }, { abortSignal }) => {\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => {\n aborted = true;\n });\n }\n await delay(ms);\n return 'done';\n },\n });\n\n const fc = FunctionCall.create({\n callId: 'call_abort_1',\n name: 'longOp',\n args: JSON.stringify({ ms: 5000 }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_abort', _itemAdded: () => {} } as any,\n toolCtx: { longOp } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await toolOutput.firstToolStartedFuture.await;\n replyAbortController.abort();\n await execTask.result;\n\n expect(aborted).toBe(true);\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(true);\n }, 20_000);\n\n it('should return error output on invalid tool args (zod validation failure)', async () => {\n const replyAbortController = new AbortController();\n\n const echo = tool({\n description: 'echo',\n parameters: z.object({ msg: z.string() }),\n execute: async ({ msg }) => `echo: ${msg}`,\n });\n\n // invalid: msg should be string\n const fc = FunctionCall.create({\n callId: 'call_invalid_args',\n name: 'echo',\n args: JSON.stringify({ msg: 123 }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_invalid', _itemAdded: () => {} } as any,\n toolCtx: { echo } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(true);\n });\n\n it('should handle multiple tool calls within a single stream', async () => {\n const replyAbortController = new AbortController();\n\n const sum = tool({\n description: 'sum',\n parameters: z.object({ a: z.number(), b: z.number() }),\n execute: async ({ a, b }) => a + b,\n });\n const upper = tool({\n description: 'upper',\n parameters: z.object({ s: z.string() }),\n execute: async ({ s }) => s.toUpperCase(),\n });\n\n const fc1 = FunctionCall.create({\n callId: 'call_multi_1',\n name: 'sum',\n args: JSON.stringify({ a: 2, b: 3 }),\n });\n const fc2 = FunctionCall.create({\n callId: 'call_multi_2',\n name: 'upper',\n args: JSON.stringify({ s: 'hey' }),\n });\n const toolCallStream = createFunctionCallStreamFromArray([fc1, fc2]);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_multi', _itemAdded: () => {} } as any,\n toolCtx: { sum, upper } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(2);\n\n // sort by callId to assert deterministically\n const sorted = [...toolOutput.output].sort((a, b) =>\n a.toolCall.callId.localeCompare(b.toolCall.callId),\n );\n\n expect(sorted[0]?.toolCall.name).toBe('sum');\n expect(sorted[0]?.toolCallOutput?.isError).toBe(false);\n expect(sorted[0]?.toolCallOutput?.output).toBe('5');\n expect(sorted[1]?.toolCall.name).toBe('upper');\n expect(sorted[1]?.toolCallOutput?.isError).toBe(false);\n expect(sorted[1]?.toolCallOutput?.output).toBe('\"HEY\"');\n });\n});\n"],"mappings":"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","names":["NodeReadableStream"]}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { ReadableStream as NodeReadableStream } from "stream/web";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { FunctionCall, tool } from "../llm/index.js";
|
|
5
|
+
import { initializeLogger } from "../log.js";
|
|
6
|
+
import { cancelAndWait, delay } from "../utils.js";
|
|
7
|
+
import { performTextForwarding, performToolExecutions } from "./generation.js";
|
|
8
|
+
function createStringStream(chunks, delayMs = 0) {
|
|
9
|
+
return new NodeReadableStream({
|
|
10
|
+
async start(controller) {
|
|
11
|
+
for (const c of chunks) {
|
|
12
|
+
if (delayMs > 0) {
|
|
13
|
+
await delay(delayMs);
|
|
14
|
+
}
|
|
15
|
+
controller.enqueue(c);
|
|
16
|
+
}
|
|
17
|
+
controller.close();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function createFunctionCallStream(fc) {
|
|
22
|
+
return new NodeReadableStream({
|
|
23
|
+
start(controller) {
|
|
24
|
+
controller.enqueue(fc);
|
|
25
|
+
controller.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createFunctionCallStreamFromArray(fcs) {
|
|
30
|
+
return new NodeReadableStream({
|
|
31
|
+
start(controller) {
|
|
32
|
+
for (const fc of fcs) {
|
|
33
|
+
controller.enqueue(fc);
|
|
34
|
+
}
|
|
35
|
+
controller.close();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
describe("Generation + Tool Execution", () => {
|
|
40
|
+
initializeLogger({ pretty: false, level: "silent" });
|
|
41
|
+
it("should not abort tool when preamble forwarders are cleaned up", async () => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const replyAbortController = new AbortController();
|
|
44
|
+
const forwarderController = new AbortController();
|
|
45
|
+
const chunks = Array.from({ length: 50 }, () => `Hi.`);
|
|
46
|
+
const fullPreambleText = chunks.join("");
|
|
47
|
+
const preamble = createStringStream(chunks, 20);
|
|
48
|
+
const [textForwardTask, textOut] = performTextForwarding(
|
|
49
|
+
preamble,
|
|
50
|
+
forwarderController,
|
|
51
|
+
null
|
|
52
|
+
);
|
|
53
|
+
let toolAborted = false;
|
|
54
|
+
const getWeather = tool({
|
|
55
|
+
description: "weather",
|
|
56
|
+
parameters: z.object({ location: z.string() }),
|
|
57
|
+
execute: async ({ location }, { abortSignal }) => {
|
|
58
|
+
if (abortSignal) {
|
|
59
|
+
abortSignal.addEventListener("abort", () => {
|
|
60
|
+
toolAborted = true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await delay(6e3);
|
|
64
|
+
return `Sunny in ${location}`;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const fc = FunctionCall.create({
|
|
68
|
+
callId: "call_1",
|
|
69
|
+
name: "getWeather",
|
|
70
|
+
args: JSON.stringify({ location: "San Francisco" })
|
|
71
|
+
});
|
|
72
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
73
|
+
const [execTask, toolOutput] = performToolExecutions({
|
|
74
|
+
session: {},
|
|
75
|
+
speechHandle: { id: "speech_test", _itemAdded: () => {
|
|
76
|
+
} },
|
|
77
|
+
toolCtx: { getWeather },
|
|
78
|
+
toolCallStream,
|
|
79
|
+
controller: replyAbortController,
|
|
80
|
+
onToolExecutionStarted: () => {
|
|
81
|
+
},
|
|
82
|
+
onToolExecutionCompleted: () => {
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await toolOutput.firstToolStartedFuture.await;
|
|
86
|
+
await delay(100);
|
|
87
|
+
await cancelAndWait([textForwardTask], 5e3);
|
|
88
|
+
await execTask.result;
|
|
89
|
+
expect(toolOutput.output.length).toBe(1);
|
|
90
|
+
const out = toolOutput.output[0];
|
|
91
|
+
expect((_a = out.toolCallOutput) == null ? void 0 : _a.isError).toBe(false);
|
|
92
|
+
expect((_b = out.toolCallOutput) == null ? void 0 : _b.output).toContain("Sunny in San Francisco");
|
|
93
|
+
expect(textOut.text).not.toBe(fullPreambleText);
|
|
94
|
+
expect(toolAborted).toBe(false);
|
|
95
|
+
}, 3e4);
|
|
96
|
+
it("should return basic tool execution output", async () => {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
const replyAbortController = new AbortController();
|
|
99
|
+
const echo = tool({
|
|
100
|
+
description: "echo",
|
|
101
|
+
parameters: z.object({ msg: z.string() }),
|
|
102
|
+
execute: async ({ msg }) => `echo: ${msg}`
|
|
103
|
+
});
|
|
104
|
+
const fc = FunctionCall.create({
|
|
105
|
+
callId: "call_2",
|
|
106
|
+
name: "echo",
|
|
107
|
+
args: JSON.stringify({ msg: "hello" })
|
|
108
|
+
});
|
|
109
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
110
|
+
const [execTask, toolOutput] = performToolExecutions({
|
|
111
|
+
session: {},
|
|
112
|
+
speechHandle: { id: "speech_test2", _itemAdded: () => {
|
|
113
|
+
} },
|
|
114
|
+
toolCtx: { echo },
|
|
115
|
+
toolCallStream,
|
|
116
|
+
controller: replyAbortController
|
|
117
|
+
});
|
|
118
|
+
await execTask.result;
|
|
119
|
+
expect(toolOutput.output.length).toBe(1);
|
|
120
|
+
const out = toolOutput.output[0];
|
|
121
|
+
expect((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(false);
|
|
122
|
+
expect((_b = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _b.output).toContain("echo: hello");
|
|
123
|
+
});
|
|
124
|
+
it("should abort tool when reply is aborted mid-execution", async () => {
|
|
125
|
+
var _a;
|
|
126
|
+
const replyAbortController = new AbortController();
|
|
127
|
+
let aborted = false;
|
|
128
|
+
const longOp = tool({
|
|
129
|
+
description: "longOp",
|
|
130
|
+
parameters: z.object({ ms: z.number() }),
|
|
131
|
+
execute: async ({ ms }, { abortSignal }) => {
|
|
132
|
+
if (abortSignal) {
|
|
133
|
+
abortSignal.addEventListener("abort", () => {
|
|
134
|
+
aborted = true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
await delay(ms);
|
|
138
|
+
return "done";
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const fc = FunctionCall.create({
|
|
142
|
+
callId: "call_abort_1",
|
|
143
|
+
name: "longOp",
|
|
144
|
+
args: JSON.stringify({ ms: 5e3 })
|
|
145
|
+
});
|
|
146
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
147
|
+
const [execTask, toolOutput] = performToolExecutions({
|
|
148
|
+
session: {},
|
|
149
|
+
speechHandle: { id: "speech_abort", _itemAdded: () => {
|
|
150
|
+
} },
|
|
151
|
+
toolCtx: { longOp },
|
|
152
|
+
toolCallStream,
|
|
153
|
+
controller: replyAbortController
|
|
154
|
+
});
|
|
155
|
+
await toolOutput.firstToolStartedFuture.await;
|
|
156
|
+
replyAbortController.abort();
|
|
157
|
+
await execTask.result;
|
|
158
|
+
expect(aborted).toBe(true);
|
|
159
|
+
expect(toolOutput.output.length).toBe(1);
|
|
160
|
+
const out = toolOutput.output[0];
|
|
161
|
+
expect((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(true);
|
|
162
|
+
}, 2e4);
|
|
163
|
+
it("should return error output on invalid tool args (zod validation failure)", async () => {
|
|
164
|
+
var _a;
|
|
165
|
+
const replyAbortController = new AbortController();
|
|
166
|
+
const echo = tool({
|
|
167
|
+
description: "echo",
|
|
168
|
+
parameters: z.object({ msg: z.string() }),
|
|
169
|
+
execute: async ({ msg }) => `echo: ${msg}`
|
|
170
|
+
});
|
|
171
|
+
const fc = FunctionCall.create({
|
|
172
|
+
callId: "call_invalid_args",
|
|
173
|
+
name: "echo",
|
|
174
|
+
args: JSON.stringify({ msg: 123 })
|
|
175
|
+
});
|
|
176
|
+
const toolCallStream = createFunctionCallStream(fc);
|
|
177
|
+
const [execTask, toolOutput] = performToolExecutions({
|
|
178
|
+
session: {},
|
|
179
|
+
speechHandle: { id: "speech_invalid", _itemAdded: () => {
|
|
180
|
+
} },
|
|
181
|
+
toolCtx: { echo },
|
|
182
|
+
toolCallStream,
|
|
183
|
+
controller: replyAbortController
|
|
184
|
+
});
|
|
185
|
+
await execTask.result;
|
|
186
|
+
expect(toolOutput.output.length).toBe(1);
|
|
187
|
+
const out = toolOutput.output[0];
|
|
188
|
+
expect((_a = out == null ? void 0 : out.toolCallOutput) == null ? void 0 : _a.isError).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it("should handle multiple tool calls within a single stream", async () => {
|
|
191
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
192
|
+
const replyAbortController = new AbortController();
|
|
193
|
+
const sum = tool({
|
|
194
|
+
description: "sum",
|
|
195
|
+
parameters: z.object({ a: z.number(), b: z.number() }),
|
|
196
|
+
execute: async ({ a, b }) => a + b
|
|
197
|
+
});
|
|
198
|
+
const upper = tool({
|
|
199
|
+
description: "upper",
|
|
200
|
+
parameters: z.object({ s: z.string() }),
|
|
201
|
+
execute: async ({ s }) => s.toUpperCase()
|
|
202
|
+
});
|
|
203
|
+
const fc1 = FunctionCall.create({
|
|
204
|
+
callId: "call_multi_1",
|
|
205
|
+
name: "sum",
|
|
206
|
+
args: JSON.stringify({ a: 2, b: 3 })
|
|
207
|
+
});
|
|
208
|
+
const fc2 = FunctionCall.create({
|
|
209
|
+
callId: "call_multi_2",
|
|
210
|
+
name: "upper",
|
|
211
|
+
args: JSON.stringify({ s: "hey" })
|
|
212
|
+
});
|
|
213
|
+
const toolCallStream = createFunctionCallStreamFromArray([fc1, fc2]);
|
|
214
|
+
const [execTask, toolOutput] = performToolExecutions({
|
|
215
|
+
session: {},
|
|
216
|
+
speechHandle: { id: "speech_multi", _itemAdded: () => {
|
|
217
|
+
} },
|
|
218
|
+
toolCtx: { sum, upper },
|
|
219
|
+
toolCallStream,
|
|
220
|
+
controller: replyAbortController
|
|
221
|
+
});
|
|
222
|
+
await execTask.result;
|
|
223
|
+
expect(toolOutput.output.length).toBe(2);
|
|
224
|
+
const sorted = [...toolOutput.output].sort(
|
|
225
|
+
(a, b) => a.toolCall.callId.localeCompare(b.toolCall.callId)
|
|
226
|
+
);
|
|
227
|
+
expect((_a = sorted[0]) == null ? void 0 : _a.toolCall.name).toBe("sum");
|
|
228
|
+
expect((_c = (_b = sorted[0]) == null ? void 0 : _b.toolCallOutput) == null ? void 0 : _c.isError).toBe(false);
|
|
229
|
+
expect((_e = (_d = sorted[0]) == null ? void 0 : _d.toolCallOutput) == null ? void 0 : _e.output).toBe("5");
|
|
230
|
+
expect((_f = sorted[1]) == null ? void 0 : _f.toolCall.name).toBe("upper");
|
|
231
|
+
expect((_h = (_g = sorted[1]) == null ? void 0 : _g.toolCallOutput) == null ? void 0 : _h.isError).toBe(false);
|
|
232
|
+
expect((_j = (_i = sorted[1]) == null ? void 0 : _i.toolCallOutput) == null ? void 0 : _j.output).toBe('"HEY"');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
//# sourceMappingURL=generation_tools.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/voice/generation_tools.test.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { ReadableStream as NodeReadableStream } from 'stream/web';\nimport { describe, expect, it } from 'vitest';\nimport { z } from 'zod';\nimport { FunctionCall, tool } from '../llm/index.js';\nimport { initializeLogger } from '../log.js';\nimport type { Task } from '../utils.js';\nimport { cancelAndWait, delay } from '../utils.js';\nimport { type _TextOut, performTextForwarding, performToolExecutions } from './generation.js';\n\nfunction createStringStream(chunks: string[], delayMs: number = 0): NodeReadableStream<string> {\n return new NodeReadableStream<string>({\n async start(controller) {\n for (const c of chunks) {\n if (delayMs > 0) {\n await delay(delayMs);\n }\n controller.enqueue(c);\n }\n controller.close();\n },\n });\n}\n\nfunction createFunctionCallStream(fc: FunctionCall): NodeReadableStream<FunctionCall> {\n return new NodeReadableStream<FunctionCall>({\n start(controller) {\n controller.enqueue(fc);\n controller.close();\n },\n });\n}\n\nfunction createFunctionCallStreamFromArray(fcs: FunctionCall[]): NodeReadableStream<FunctionCall> {\n return new NodeReadableStream<FunctionCall>({\n start(controller) {\n for (const fc of fcs) {\n controller.enqueue(fc);\n }\n controller.close();\n },\n });\n}\n\ndescribe('Generation + Tool Execution', () => {\n initializeLogger({ pretty: false, level: 'silent' });\n\n it('should not abort tool when preamble forwarders are cleaned up', async () => {\n const replyAbortController = new AbortController();\n const forwarderController = new AbortController();\n\n const chunks = Array.from({ length: 50 }, () => `Hi.`);\n const fullPreambleText = chunks.join('');\n const preamble = createStringStream(chunks, 20);\n const [textForwardTask, textOut]: [Task<void>, _TextOut] = performTextForwarding(\n preamble,\n forwarderController,\n null,\n );\n\n // Tool that takes > 5 seconds\n let toolAborted = false;\n const getWeather = tool({\n description: 'weather',\n parameters: z.object({ location: z.string() }),\n execute: async ({ location }, { abortSignal }) => {\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => {\n toolAborted = true;\n });\n }\n // 6s delay\n await delay(6000);\n return `Sunny in ${location}`;\n },\n });\n\n const fc = FunctionCall.create({\n callId: 'call_1',\n name: 'getWeather',\n args: JSON.stringify({ location: 'San Francisco' }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_test', _itemAdded: () => {} } as any,\n toolCtx: { getWeather } as any,\n toolCallStream,\n controller: replyAbortController,\n onToolExecutionStarted: () => {},\n onToolExecutionCompleted: () => {},\n });\n\n // Ensure tool has started, then cancel forwarders mid-stream (without aborting parent AbortController)\n await toolOutput.firstToolStartedFuture.await;\n await delay(100);\n await cancelAndWait([textForwardTask], 5000);\n\n await execTask.result;\n\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0]!;\n expect(out.toolCallOutput?.isError).toBe(false);\n expect(out.toolCallOutput?.output).toContain('Sunny in San Francisco');\n // Forwarder should have been cancelled before finishing all preamble chunks\n expect(textOut.text).not.toBe(fullPreambleText);\n // Tool's abort signal must not have fired\n expect(toolAborted).toBe(false);\n }, 30_000);\n\n it('should return basic tool execution output', async () => {\n const replyAbortController = new AbortController();\n\n const echo = tool({\n description: 'echo',\n parameters: z.object({ msg: z.string() }),\n execute: async ({ msg }) => `echo: ${msg}`,\n });\n\n const fc = FunctionCall.create({\n callId: 'call_2',\n name: 'echo',\n args: JSON.stringify({ msg: 'hello' }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_test2', _itemAdded: () => {} } as any,\n toolCtx: { echo } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(false);\n expect(out?.toolCallOutput?.output).toContain('echo: hello');\n });\n\n it('should abort tool when reply is aborted mid-execution', async () => {\n const replyAbortController = new AbortController();\n\n let aborted = false;\n const longOp = tool({\n description: 'longOp',\n parameters: z.object({ ms: z.number() }),\n execute: async ({ ms }, { abortSignal }) => {\n if (abortSignal) {\n abortSignal.addEventListener('abort', () => {\n aborted = true;\n });\n }\n await delay(ms);\n return 'done';\n },\n });\n\n const fc = FunctionCall.create({\n callId: 'call_abort_1',\n name: 'longOp',\n args: JSON.stringify({ ms: 5000 }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_abort', _itemAdded: () => {} } as any,\n toolCtx: { longOp } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await toolOutput.firstToolStartedFuture.await;\n replyAbortController.abort();\n await execTask.result;\n\n expect(aborted).toBe(true);\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(true);\n }, 20_000);\n\n it('should return error output on invalid tool args (zod validation failure)', async () => {\n const replyAbortController = new AbortController();\n\n const echo = tool({\n description: 'echo',\n parameters: z.object({ msg: z.string() }),\n execute: async ({ msg }) => `echo: ${msg}`,\n });\n\n // invalid: msg should be string\n const fc = FunctionCall.create({\n callId: 'call_invalid_args',\n name: 'echo',\n args: JSON.stringify({ msg: 123 }),\n });\n const toolCallStream = createFunctionCallStream(fc);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_invalid', _itemAdded: () => {} } as any,\n toolCtx: { echo } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(1);\n const out = toolOutput.output[0];\n expect(out?.toolCallOutput?.isError).toBe(true);\n });\n\n it('should handle multiple tool calls within a single stream', async () => {\n const replyAbortController = new AbortController();\n\n const sum = tool({\n description: 'sum',\n parameters: z.object({ a: z.number(), b: z.number() }),\n execute: async ({ a, b }) => a + b,\n });\n const upper = tool({\n description: 'upper',\n parameters: z.object({ s: z.string() }),\n execute: async ({ s }) => s.toUpperCase(),\n });\n\n const fc1 = FunctionCall.create({\n callId: 'call_multi_1',\n name: 'sum',\n args: JSON.stringify({ a: 2, b: 3 }),\n });\n const fc2 = FunctionCall.create({\n callId: 'call_multi_2',\n name: 'upper',\n args: JSON.stringify({ s: 'hey' }),\n });\n const toolCallStream = createFunctionCallStreamFromArray([fc1, fc2]);\n\n const [execTask, toolOutput] = performToolExecutions({\n session: {} as any,\n speechHandle: { id: 'speech_multi', _itemAdded: () => {} } as any,\n toolCtx: { sum, upper } as any,\n toolCallStream,\n controller: replyAbortController,\n });\n\n await execTask.result;\n expect(toolOutput.output.length).toBe(2);\n\n // sort by callId to assert deterministically\n const sorted = [...toolOutput.output].sort((a, b) =>\n a.toolCall.callId.localeCompare(b.toolCall.callId),\n );\n\n expect(sorted[0]?.toolCall.name).toBe('sum');\n expect(sorted[0]?.toolCallOutput?.isError).toBe(false);\n expect(sorted[0]?.toolCallOutput?.output).toBe('5');\n expect(sorted[1]?.toolCall.name).toBe('upper');\n expect(sorted[1]?.toolCallOutput?.isError).toBe(false);\n expect(sorted[1]?.toolCallOutput?.output).toBe('\"HEY\"');\n });\n});\n"],"mappings":"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","names":[]}
|
package/dist/voice/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ var import_agent = require("./agent.cjs");
|
|
|
29
29
|
var import_agent_session = require("./agent_session.cjs");
|
|
30
30
|
__reExport(voice_exports, require("./avatar/index.cjs"), module.exports);
|
|
31
31
|
__reExport(voice_exports, require("./events.cjs"), module.exports);
|
|
32
|
+
__reExport(voice_exports, require("./room_io/index.cjs"), module.exports);
|
|
32
33
|
var import_run_context = require("./run_context.cjs");
|
|
33
34
|
// Annotate the CommonJS export names for ESM import in node:
|
|
34
35
|
0 && (module.exports = {
|
|
@@ -37,6 +38,7 @@ var import_run_context = require("./run_context.cjs");
|
|
|
37
38
|
RunContext,
|
|
38
39
|
StopResponse,
|
|
39
40
|
...require("./avatar/index.cjs"),
|
|
40
|
-
...require("./events.cjs")
|
|
41
|
+
...require("./events.cjs"),
|
|
42
|
+
...require("./room_io/index.cjs")
|
|
41
43
|
});
|
|
42
44
|
//# sourceMappingURL=index.cjs.map
|
package/dist/voice/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\n\nexport * from './avatar/index.js';\nexport * from './events.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAA2E;AAC3E,2BAAuD;AAEvD,0BAAc,8BANd;AAOA,0BAAc,wBAPd;AAQA,yBAA2B;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\n\nexport * from './avatar/index.js';\nexport * from './events.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAA2E;AAC3E,2BAAuD;AAEvD,0BAAc,8BANd;AAOA,0BAAc,wBAPd;AAQA,0BAAc,+BARd;AASA,yBAA2B;","names":[]}
|
package/dist/voice/index.d.cts
CHANGED
|
@@ -2,5 +2,6 @@ export { Agent, StopResponse, type AgentOptions, type ModelSettings } from './ag
|
|
|
2
2
|
export { AgentSession, type AgentSessionOptions } from './agent_session.js';
|
|
3
3
|
export * from './avatar/index.js';
|
|
4
4
|
export * from './events.js';
|
|
5
|
+
export * from './room_io/index.js';
|
|
5
6
|
export { RunContext } from './run_context.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/voice/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { Agent, StopResponse, type AgentOptions, type ModelSettings } from './ag
|
|
|
2
2
|
export { AgentSession, type AgentSessionOptions } from './agent_session.js';
|
|
3
3
|
export * from './avatar/index.js';
|
|
4
4
|
export * from './events.js';
|
|
5
|
+
export * from './room_io/index.js';
|
|
5
6
|
export { RunContext } from './run_context.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/voice/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Agent, StopResponse } from "./agent.js";
|
|
|
2
2
|
import { AgentSession } from "./agent_session.js";
|
|
3
3
|
export * from "./avatar/index.js";
|
|
4
4
|
export * from "./events.js";
|
|
5
|
+
export * from "./room_io/index.js";
|
|
5
6
|
import { RunContext } from "./run_context.js";
|
|
6
7
|
export {
|
|
7
8
|
Agent,
|
package/dist/voice/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\n\nexport * from './avatar/index.js';\nexport * from './events.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":"AAGA,SAAS,OAAO,oBAA2D;AAC3E,SAAS,oBAA8C;AAEvD,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/voice/index.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nexport { Agent, StopResponse, type AgentOptions, type ModelSettings } from './agent.js';\nexport { AgentSession, type AgentSessionOptions } from './agent_session.js';\n\nexport * from './avatar/index.js';\nexport * from './events.js';\nexport * from './room_io/index.js';\nexport { RunContext } from './run_context.js';\n"],"mappings":"AAGA,SAAS,OAAO,oBAA2D;AAC3E,SAAS,oBAA8C;AAEvD,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;","names":[]}
|
package/package.json
CHANGED
package/src/audio.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { AudioFrame } from '@livekit/rtc-node';
|
|
|
5
5
|
import { log } from './log.js';
|
|
6
6
|
import type { AudioBuffer } from './utils.js';
|
|
7
7
|
|
|
8
|
-
export function
|
|
8
|
+
export function calculateAudioDurationSeconds(frame: AudioBuffer) {
|
|
9
9
|
// TODO(AJS-102): use frame.durationMs once available in rtc-node
|
|
10
10
|
return Array.isArray(frame)
|
|
11
11
|
? frame.reduce((sum, a) => sum + a.samplesPerChannel / a.sampleRate, 0)
|
package/src/llm/llm.ts
CHANGED
|
@@ -203,12 +203,13 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
203
203
|
this.output.close();
|
|
204
204
|
|
|
205
205
|
const duration = process.hrtime.bigint() - startTime;
|
|
206
|
+
const durationMs = Math.trunc(Number(duration / BigInt(1000000)));
|
|
206
207
|
const metrics: LLMMetrics = {
|
|
207
208
|
type: 'llm_metrics',
|
|
208
209
|
timestamp: Date.now(),
|
|
209
210
|
requestId,
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
ttftMs: ttft === BigInt(-1) ? -1 : Math.trunc(Number(ttft / BigInt(1000000))),
|
|
212
|
+
durationMs,
|
|
212
213
|
cancelled: this.abortController.signal.aborted,
|
|
213
214
|
label: this.#llm.label(),
|
|
214
215
|
completionTokens: usage?.completionTokens || 0,
|
|
@@ -216,8 +217,10 @@ export abstract class LLMStream implements AsyncIterableIterator<ChatChunk> {
|
|
|
216
217
|
promptCachedTokens: usage?.promptCachedTokens || 0,
|
|
217
218
|
totalTokens: usage?.totalTokens || 0,
|
|
218
219
|
tokensPerSecond: (() => {
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
if (durationMs <= 0) {
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
return (usage?.completionTokens || 0) / (durationMs / 1000);
|
|
221
224
|
})(),
|
|
222
225
|
};
|
|
223
226
|
this.#llm.emit('metrics_collected', metrics);
|