@livekit/agents 1.0.30 → 1.0.32
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/ipc/inference_proc_executor.cjs +6 -3
- package/dist/ipc/inference_proc_executor.cjs.map +1 -1
- package/dist/ipc/inference_proc_executor.d.ts.map +1 -1
- package/dist/ipc/inference_proc_executor.js +6 -3
- package/dist/ipc/inference_proc_executor.js.map +1 -1
- package/dist/ipc/job_proc_executor.cjs +6 -1
- package/dist/ipc/job_proc_executor.cjs.map +1 -1
- package/dist/ipc/job_proc_executor.d.ts.map +1 -1
- package/dist/ipc/job_proc_executor.js +6 -1
- package/dist/ipc/job_proc_executor.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +1 -1
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/ipc/supervised_proc.cjs +29 -7
- package/dist/ipc/supervised_proc.cjs.map +1 -1
- package/dist/ipc/supervised_proc.d.ts.map +1 -1
- package/dist/ipc/supervised_proc.js +29 -7
- package/dist/ipc/supervised_proc.js.map +1 -1
- package/dist/ipc/supervised_proc.test.cjs +145 -0
- package/dist/ipc/supervised_proc.test.cjs.map +1 -0
- package/dist/ipc/supervised_proc.test.js +122 -0
- package/dist/ipc/supervised_proc.test.js.map +1 -0
- package/dist/job.cjs +5 -1
- package/dist/job.cjs.map +1 -1
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +5 -1
- package/dist/job.js.map +1 -1
- package/dist/llm/chat_context.cjs +19 -2
- package/dist/llm/chat_context.cjs.map +1 -1
- package/dist/llm/chat_context.d.cts +8 -0
- package/dist/llm/chat_context.d.ts +8 -0
- package/dist/llm/chat_context.d.ts.map +1 -1
- package/dist/llm/chat_context.js +19 -2
- package/dist/llm/chat_context.js.map +1 -1
- package/dist/llm/provider_format/google.cjs +6 -2
- package/dist/llm/provider_format/google.cjs.map +1 -1
- package/dist/llm/provider_format/google.d.ts.map +1 -1
- package/dist/llm/provider_format/google.js +6 -2
- package/dist/llm/provider_format/google.js.map +1 -1
- package/dist/llm/realtime.cjs.map +1 -1
- package/dist/llm/realtime.d.cts +4 -0
- package/dist/llm/realtime.d.ts +4 -0
- package/dist/llm/realtime.d.ts.map +1 -1
- package/dist/llm/realtime.js.map +1 -1
- package/dist/log.cjs +3 -3
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +5 -0
- package/dist/log.d.ts +5 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +3 -3
- package/dist/log.js.map +1 -1
- package/dist/stream/stream_channel.cjs +8 -1
- package/dist/stream/stream_channel.cjs.map +1 -1
- package/dist/stream/stream_channel.d.cts +1 -0
- package/dist/stream/stream_channel.d.ts +1 -0
- package/dist/stream/stream_channel.d.ts.map +1 -1
- package/dist/stream/stream_channel.js +8 -1
- package/dist/stream/stream_channel.js.map +1 -1
- package/dist/telemetry/otel_http_exporter.cjs +13 -10
- package/dist/telemetry/otel_http_exporter.cjs.map +1 -1
- package/dist/telemetry/otel_http_exporter.d.ts.map +1 -1
- package/dist/telemetry/otel_http_exporter.js +13 -10
- package/dist/telemetry/otel_http_exporter.js.map +1 -1
- package/dist/telemetry/traces.cjs +22 -4
- package/dist/telemetry/traces.cjs.map +1 -1
- package/dist/telemetry/traces.d.ts.map +1 -1
- package/dist/telemetry/traces.js +22 -4
- package/dist/telemetry/traces.js.map +1 -1
- package/dist/voice/agent_activity.cjs +25 -5
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +1 -0
- package/dist/voice/agent_activity.d.ts +1 -0
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +26 -6
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/generation.cjs +3 -1
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +3 -1
- package/dist/voice/generation.js.map +1 -1
- package/package.json +1 -1
- package/src/ipc/inference_proc_executor.ts +11 -3
- package/src/ipc/job_proc_executor.ts +11 -1
- package/src/ipc/job_proc_lazy_main.ts +1 -1
- package/src/ipc/supervised_proc.test.ts +153 -0
- package/src/ipc/supervised_proc.ts +27 -9
- package/src/job.ts +4 -1
- package/src/llm/chat_context.ts +28 -2
- package/src/llm/provider_format/google.ts +6 -2
- package/src/llm/realtime.ts +5 -0
- package/src/log.ts +9 -3
- package/src/stream/stream_channel.ts +9 -1
- package/src/telemetry/otel_http_exporter.ts +14 -10
- package/src/telemetry/traces.ts +28 -4
- package/src/voice/agent_activity.ts +27 -2
- package/src/voice/generation.ts +2 -0
- package/src/llm/__snapshots__/utils.test.ts.snap +0 -65
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { fork, spawn } from 'node:child_process';
|
|
5
|
+
import { unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import pidusage from 'pidusage';
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
const childScript = join(tmpdir(), 'test_child.mjs');
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
writeFileSync(
|
|
15
|
+
childScript,
|
|
16
|
+
`process.on('message', (msg) => process.send?.({ echo: msg }));
|
|
17
|
+
setInterval(() => {}, 1000);`,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(childScript);
|
|
24
|
+
} catch {}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function getChildMemoryUsageMB(pid: number | undefined): Promise<number> {
|
|
28
|
+
if (!pid) return 0;
|
|
29
|
+
try {
|
|
30
|
+
const stats = await pidusage(pid);
|
|
31
|
+
return stats.memory / (1024 * 1024);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
34
|
+
if (code === 'ENOENT' || code === 'ESRCH') {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('pidusage on dead process', () => {
|
|
42
|
+
it('raw pidusage throws on dead pid', async () => {
|
|
43
|
+
const child = spawn('sleep', ['10']);
|
|
44
|
+
const pid = child.pid!;
|
|
45
|
+
|
|
46
|
+
child.kill('SIGKILL');
|
|
47
|
+
await new Promise<void>((r) => child.on('exit', r));
|
|
48
|
+
|
|
49
|
+
await expect(pidusage(pid)).rejects.toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('fixed version returns 0 instead of crashing', async () => {
|
|
53
|
+
const child = spawn('sleep', ['10']);
|
|
54
|
+
const pid = child.pid!;
|
|
55
|
+
|
|
56
|
+
child.kill('SIGKILL');
|
|
57
|
+
await new Promise<void>((r) => child.on('exit', r));
|
|
58
|
+
|
|
59
|
+
const mem = await getChildMemoryUsageMB(pid);
|
|
60
|
+
expect(mem).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles concurrent calls on dying process', async () => {
|
|
64
|
+
const child = spawn('sleep', ['10']);
|
|
65
|
+
const pid = child.pid!;
|
|
66
|
+
const exitPromise = new Promise<void>((r) => child.on('exit', r));
|
|
67
|
+
|
|
68
|
+
child.kill('SIGKILL');
|
|
69
|
+
|
|
70
|
+
const results = await Promise.all([
|
|
71
|
+
getChildMemoryUsageMB(pid),
|
|
72
|
+
getChildMemoryUsageMB(pid),
|
|
73
|
+
getChildMemoryUsageMB(pid),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
await exitPromise;
|
|
77
|
+
expect(results.every((r) => r === 0)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('IPC send on dead process', () => {
|
|
82
|
+
it('child.connected becomes false when child dies', async () => {
|
|
83
|
+
const child = fork(childScript, [], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] });
|
|
84
|
+
const exitPromise = new Promise<void>((r) => child.on('exit', r));
|
|
85
|
+
|
|
86
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
87
|
+
expect(child.connected).toBe(true);
|
|
88
|
+
|
|
89
|
+
child.kill('SIGKILL');
|
|
90
|
+
await exitPromise;
|
|
91
|
+
|
|
92
|
+
expect(child.connected).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('checking connected before send prevents crash', async () => {
|
|
96
|
+
const child = fork(childScript, [], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] });
|
|
97
|
+
const exitPromise = new Promise<void>((r) => child.on('exit', r));
|
|
98
|
+
|
|
99
|
+
// Suppress EPIPE errors that can occur due to race conditions between
|
|
100
|
+
// child.connected check and the actual pipe state
|
|
101
|
+
child.on('error', (err: NodeJS.ErrnoException) => {
|
|
102
|
+
if (err.code !== 'EPIPE') throw err;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let sent = 0;
|
|
106
|
+
let skipped = 0;
|
|
107
|
+
|
|
108
|
+
const interval = setInterval(() => {
|
|
109
|
+
if (child.connected) {
|
|
110
|
+
child.send({ ping: Date.now() });
|
|
111
|
+
sent++;
|
|
112
|
+
} else {
|
|
113
|
+
skipped++;
|
|
114
|
+
}
|
|
115
|
+
}, 20);
|
|
116
|
+
|
|
117
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
118
|
+
child.kill('SIGKILL');
|
|
119
|
+
await exitPromise;
|
|
120
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
121
|
+
clearInterval(interval);
|
|
122
|
+
|
|
123
|
+
expect(sent).toBeGreaterThan(0);
|
|
124
|
+
expect(skipped).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('timer cleanup', () => {
|
|
129
|
+
it('clearInterval stops the interval', async () => {
|
|
130
|
+
let count = 0;
|
|
131
|
+
const interval = setInterval(() => count++, 30);
|
|
132
|
+
|
|
133
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
134
|
+
const countAtClear = count;
|
|
135
|
+
clearInterval(interval);
|
|
136
|
+
|
|
137
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
138
|
+
expect(count).toBe(countAtClear);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('double clear is safe', () => {
|
|
142
|
+
const interval = setInterval(() => {}, 100);
|
|
143
|
+
const timeout = setTimeout(() => {}, 1000);
|
|
144
|
+
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
|
|
148
|
+
expect(() => {
|
|
149
|
+
clearInterval(interval);
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}).not.toThrow();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -80,7 +80,9 @@ export abstract class SupervisedProc {
|
|
|
80
80
|
await this.init.await;
|
|
81
81
|
|
|
82
82
|
this.#pingInterval = setInterval(() => {
|
|
83
|
-
this.proc
|
|
83
|
+
if (this.proc?.connected) {
|
|
84
|
+
this.proc.send({ case: 'pingRequest', value: { timestamp: Date.now() } });
|
|
85
|
+
}
|
|
84
86
|
}, this.#opts.pingInterval);
|
|
85
87
|
|
|
86
88
|
this.#pongTimeout = setTimeout(() => {
|
|
@@ -141,6 +143,7 @@ export abstract class SupervisedProc {
|
|
|
141
143
|
});
|
|
142
144
|
|
|
143
145
|
this.proc!.on('exit', () => {
|
|
146
|
+
this.clearTimers();
|
|
144
147
|
this.#join.resolve();
|
|
145
148
|
});
|
|
146
149
|
|
|
@@ -159,11 +162,13 @@ export abstract class SupervisedProc {
|
|
|
159
162
|
|
|
160
163
|
async initialize() {
|
|
161
164
|
const timer = setTimeout(() => {
|
|
162
|
-
|
|
163
|
-
this.init.reject(err);
|
|
164
|
-
throw err;
|
|
165
|
+
this.init.reject(new Error('runner initialization timed out'));
|
|
165
166
|
}, this.#opts.initializeTimeout);
|
|
166
|
-
this.proc
|
|
167
|
+
if (!this.proc?.connected) {
|
|
168
|
+
this.init.reject(new Error('process not connected'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.proc.send({
|
|
167
172
|
case: 'initializeRequest',
|
|
168
173
|
value: {
|
|
169
174
|
loggerOptions,
|
|
@@ -187,7 +192,9 @@ export abstract class SupervisedProc {
|
|
|
187
192
|
}
|
|
188
193
|
this.#closing = true;
|
|
189
194
|
|
|
190
|
-
this.proc
|
|
195
|
+
if (this.proc?.connected) {
|
|
196
|
+
this.proc.send({ case: 'shutdownRequest' });
|
|
197
|
+
}
|
|
191
198
|
|
|
192
199
|
const timer = setTimeout(() => {
|
|
193
200
|
this.#logger.error('job shutdown is taking too much time');
|
|
@@ -203,8 +210,11 @@ export abstract class SupervisedProc {
|
|
|
203
210
|
if (this.#runningJob) {
|
|
204
211
|
throw new Error('executor already has a running job');
|
|
205
212
|
}
|
|
213
|
+
if (!this.proc?.connected) {
|
|
214
|
+
throw new Error('process not connected');
|
|
215
|
+
}
|
|
206
216
|
this.#runningJob = info;
|
|
207
|
-
this.proc
|
|
217
|
+
this.proc.send({ case: 'startJobRequest', value: { runningJob: info } });
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
private async getChildMemoryUsageMB(): Promise<number> {
|
|
@@ -212,8 +222,16 @@ export abstract class SupervisedProc {
|
|
|
212
222
|
if (!pid) {
|
|
213
223
|
return 0;
|
|
214
224
|
}
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
try {
|
|
226
|
+
const stats = await pidusage(pid);
|
|
227
|
+
return stats.memory / (1024 * 1024);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
230
|
+
if (code === 'ENOENT' || code === 'ESRCH') {
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
private clearTimers() {
|
package/src/job.ts
CHANGED
|
@@ -126,7 +126,10 @@ export class JobContext {
|
|
|
126
126
|
this.#onShutdown = onShutdown;
|
|
127
127
|
this.onParticipantConnected = this.onParticipantConnected.bind(this);
|
|
128
128
|
this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
|
|
129
|
-
this.#logger = log().child({
|
|
129
|
+
this.#logger = log().child({
|
|
130
|
+
jobId: this.#info.job.id,
|
|
131
|
+
roomName: this.#info.job.room?.name,
|
|
132
|
+
});
|
|
130
133
|
this.#inferenceExecutor = inferenceExecutor;
|
|
131
134
|
this._sessionDirectory = path.join(os.tmpdir(), 'livekit-agents', `job-${this.#info.job.id}`);
|
|
132
135
|
}
|
package/src/llm/chat_context.ts
CHANGED
|
@@ -189,19 +189,35 @@ export class FunctionCall {
|
|
|
189
189
|
|
|
190
190
|
createdAt: number;
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Opaque signature for Gemini thinking mode.
|
|
194
|
+
* When using Gemini 3+ models with thinking enabled, this signature must be
|
|
195
|
+
* preserved and returned with function responses to maintain thought context.
|
|
196
|
+
*/
|
|
197
|
+
thoughtSignature?: string;
|
|
198
|
+
|
|
192
199
|
constructor(params: {
|
|
193
200
|
callId: string;
|
|
194
201
|
name: string;
|
|
195
202
|
args: string;
|
|
196
203
|
id?: string;
|
|
197
204
|
createdAt?: number;
|
|
205
|
+
thoughtSignature?: string;
|
|
198
206
|
}) {
|
|
199
|
-
const {
|
|
207
|
+
const {
|
|
208
|
+
callId,
|
|
209
|
+
name,
|
|
210
|
+
args,
|
|
211
|
+
id = shortuuid('item_'),
|
|
212
|
+
createdAt = Date.now(),
|
|
213
|
+
thoughtSignature,
|
|
214
|
+
} = params;
|
|
200
215
|
this.id = id;
|
|
201
216
|
this.callId = callId;
|
|
202
217
|
this.args = args;
|
|
203
218
|
this.name = name;
|
|
204
219
|
this.createdAt = createdAt;
|
|
220
|
+
this.thoughtSignature = thoughtSignature;
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
static create(params: {
|
|
@@ -210,6 +226,7 @@ export class FunctionCall {
|
|
|
210
226
|
args: string;
|
|
211
227
|
id?: string;
|
|
212
228
|
createdAt?: number;
|
|
229
|
+
thoughtSignature?: string;
|
|
213
230
|
}) {
|
|
214
231
|
return new FunctionCall(params);
|
|
215
232
|
}
|
|
@@ -224,6 +241,10 @@ export class FunctionCall {
|
|
|
224
241
|
args: this.args,
|
|
225
242
|
};
|
|
226
243
|
|
|
244
|
+
if (this.thoughtSignature) {
|
|
245
|
+
result.thoughtSignature = this.thoughtSignature;
|
|
246
|
+
}
|
|
247
|
+
|
|
227
248
|
if (!excludeTimestamp) {
|
|
228
249
|
result.createdAt = this.createdAt;
|
|
229
250
|
}
|
|
@@ -602,7 +623,12 @@ export class ChatContext {
|
|
|
602
623
|
return false;
|
|
603
624
|
}
|
|
604
625
|
} else if (a.type === 'function_call' && b.type === 'function_call') {
|
|
605
|
-
if (
|
|
626
|
+
if (
|
|
627
|
+
a.name !== b.name ||
|
|
628
|
+
a.callId !== b.callId ||
|
|
629
|
+
a.args !== b.args ||
|
|
630
|
+
a.thoughtSignature !== b.thoughtSignature
|
|
631
|
+
) {
|
|
606
632
|
return false;
|
|
607
633
|
}
|
|
608
634
|
} else if (a.type === 'function_call_output' && b.type === 'function_call_output') {
|
|
@@ -67,13 +67,17 @@ export async function toChatCtx(
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
} else if (msg.type === 'function_call') {
|
|
70
|
-
|
|
70
|
+
const functionCallPart: Record<string, unknown> = {
|
|
71
71
|
functionCall: {
|
|
72
72
|
id: msg.callId,
|
|
73
73
|
name: msg.name,
|
|
74
74
|
args: JSON.parse(msg.args || '{}'),
|
|
75
75
|
},
|
|
76
|
-
}
|
|
76
|
+
};
|
|
77
|
+
if (msg.thoughtSignature) {
|
|
78
|
+
functionCallPart.thoughtSignature = msg.thoughtSignature;
|
|
79
|
+
}
|
|
80
|
+
parts.push(functionCallPart);
|
|
77
81
|
} else if (msg.type === 'function_call_output') {
|
|
78
82
|
const response = msg.isError ? { error: msg.output } : { output: msg.output };
|
|
79
83
|
parts.push({
|
package/src/llm/realtime.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface GenerationCreatedEvent {
|
|
|
26
26
|
messageStream: ReadableStream<MessageGeneration>;
|
|
27
27
|
functionStream: ReadableStream<FunctionCall>;
|
|
28
28
|
userInitiated: boolean;
|
|
29
|
+
/** Response ID for correlating metrics with spans */
|
|
30
|
+
responseId?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface RealtimeModelError {
|
|
@@ -63,6 +65,9 @@ export abstract class RealtimeModel {
|
|
|
63
65
|
return this._capabilities;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/** The model name/identifier used by this realtime model */
|
|
69
|
+
abstract get model(): string;
|
|
70
|
+
|
|
66
71
|
abstract session(): RealtimeSession;
|
|
67
72
|
|
|
68
73
|
abstract close(): Promise<void>;
|
package/src/log.ts
CHANGED
|
@@ -62,6 +62,11 @@ class OtelDestination extends Writable {
|
|
|
62
62
|
* Enable OTEL logging by reconfiguring the logger with multistream.
|
|
63
63
|
* Uses a custom destination that receives full JSON logs (with msg, level, time).
|
|
64
64
|
*
|
|
65
|
+
* The base logger level is set to 'debug' so all logs are generated,
|
|
66
|
+
* while each stream filters to its own level:
|
|
67
|
+
* - Terminal: user-specified level (default: 'info')
|
|
68
|
+
* - OTEL/Cloud: always 'debug' to capture all logs for observability
|
|
69
|
+
*
|
|
65
70
|
* @internal
|
|
66
71
|
*/
|
|
67
72
|
export const enableOtelLogging = () => {
|
|
@@ -73,11 +78,12 @@ export const enableOtelLogging = () => {
|
|
|
73
78
|
|
|
74
79
|
const { pretty, level } = loggerOptions;
|
|
75
80
|
|
|
76
|
-
const
|
|
81
|
+
const terminalLevel = level || 'info';
|
|
77
82
|
const streams: { stream: DestinationStream; level: string }[] = [
|
|
78
|
-
{ stream: pretty ? pinoPretty({ colorize: true }) : process.stdout, level:
|
|
83
|
+
{ stream: pretty ? pinoPretty({ colorize: true }) : process.stdout, level: terminalLevel },
|
|
79
84
|
{ stream: new OtelDestination(), level: 'debug' },
|
|
80
85
|
];
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
// Base level must be 'debug' to generate all logs; each stream filters independently
|
|
88
|
+
logger = pino({ level: 'debug' }, multistream(streams));
|
|
83
89
|
};
|
|
@@ -8,25 +8,33 @@ export interface StreamChannel<T> {
|
|
|
8
8
|
write(chunk: T): Promise<void>;
|
|
9
9
|
close(): Promise<void>;
|
|
10
10
|
stream(): ReadableStream<T>;
|
|
11
|
+
readonly closed: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function createStreamChannel<T>(): StreamChannel<T> {
|
|
14
15
|
const transform = new IdentityTransform<T>();
|
|
15
16
|
const writer = transform.writable.getWriter();
|
|
17
|
+
let isClosed = false;
|
|
16
18
|
|
|
17
19
|
return {
|
|
18
20
|
write: (chunk: T) => writer.write(chunk),
|
|
19
21
|
stream: () => transform.readable,
|
|
20
22
|
close: async () => {
|
|
21
23
|
try {
|
|
22
|
-
|
|
24
|
+
const result = await writer.close();
|
|
25
|
+
isClosed = true;
|
|
26
|
+
return result;
|
|
23
27
|
} catch (e) {
|
|
24
28
|
if (e instanceof Error && e.name === 'TypeError') {
|
|
25
29
|
// Ignore error if the stream is already closed
|
|
30
|
+
isClosed = true;
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
28
33
|
throw e;
|
|
29
34
|
}
|
|
30
35
|
},
|
|
36
|
+
get closed() {
|
|
37
|
+
return isClosed;
|
|
38
|
+
},
|
|
31
39
|
};
|
|
32
40
|
}
|
|
@@ -122,16 +122,20 @@ export class SimpleOTLPHttpLogExporter {
|
|
|
122
122
|
}))
|
|
123
123
|
: [];
|
|
124
124
|
|
|
125
|
-
const logRecords = records.map((record) =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const logRecords = records.map((record) => {
|
|
126
|
+
// Ensure timestampMs is a valid number, fallback to current time if NaN/undefined
|
|
127
|
+
const timestampMs = Number.isFinite(record.timestampMs) ? record.timestampMs : Date.now();
|
|
128
|
+
return {
|
|
129
|
+
timeUnixNano: String(BigInt(Math.floor(timestampMs * 1_000_000))),
|
|
130
|
+
observedTimeUnixNano: String(BigInt(Date.now()) * BigInt(1_000_000)),
|
|
131
|
+
severityNumber: record.severityNumber ?? SeverityNumber.UNSPECIFIED,
|
|
132
|
+
severityText: record.severityText ?? 'unspecified',
|
|
133
|
+
body: { stringValue: record.body },
|
|
134
|
+
attributes: this.convertAttributes(record.attributes),
|
|
135
|
+
traceId: '',
|
|
136
|
+
spanId: '',
|
|
137
|
+
};
|
|
138
|
+
});
|
|
135
139
|
|
|
136
140
|
return {
|
|
137
141
|
resourceLogs: [
|
package/src/telemetry/traces.ts
CHANGED
|
@@ -457,9 +457,15 @@ export async function uploadSessionReport(options: {
|
|
|
457
457
|
// get reordered by the dashboard
|
|
458
458
|
let lastTimestamp = 0;
|
|
459
459
|
for (const item of report.chatHistory.items) {
|
|
460
|
+
// Skip null/undefined items
|
|
461
|
+
if (!item) continue;
|
|
462
|
+
|
|
460
463
|
// Ensure monotonically increasing timestamps for proper ordering
|
|
461
464
|
// Add 0.001ms (1 microsecond) offset when timestamps collide
|
|
462
|
-
|
|
465
|
+
// Also handle undefined/NaN timestamps from realtime mode (defensive)
|
|
466
|
+
const hasValidTimestamp = Number.isFinite(item.createdAt);
|
|
467
|
+
let itemTimestamp = hasValidTimestamp ? item.createdAt : Date.now();
|
|
468
|
+
|
|
463
469
|
if (itemTimestamp <= lastTimestamp) {
|
|
464
470
|
itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond
|
|
465
471
|
}
|
|
@@ -482,6 +488,7 @@ export async function uploadSessionReport(options: {
|
|
|
482
488
|
severityText,
|
|
483
489
|
});
|
|
484
490
|
}
|
|
491
|
+
|
|
485
492
|
await logExporter.export(logRecords);
|
|
486
493
|
|
|
487
494
|
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
@@ -574,13 +581,30 @@ export async function uploadSessionReport(options: {
|
|
|
574
581
|
}
|
|
575
582
|
|
|
576
583
|
if (res.statusCode && res.statusCode >= 400) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
)
|
|
584
|
+
// Read response body for error details
|
|
585
|
+
let body = '';
|
|
586
|
+
res.on('data', (chunk) => {
|
|
587
|
+
body += chunk.toString();
|
|
588
|
+
});
|
|
589
|
+
res.on('error', (readErr) => {
|
|
590
|
+
reject(
|
|
591
|
+
new Error(
|
|
592
|
+
`Failed to upload session report: ${res.statusCode} ${res.statusMessage} (body read error: ${readErr.message})`,
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
res.on('end', () => {
|
|
597
|
+
reject(
|
|
598
|
+
new Error(
|
|
599
|
+
`Failed to upload session report: ${res.statusCode} ${res.statusMessage} - ${body}`,
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
});
|
|
580
603
|
return;
|
|
581
604
|
}
|
|
582
605
|
|
|
583
606
|
res.resume(); // Drain the response
|
|
607
|
+
res.on('error', (readErr) => reject(new Error(`Response read error: ${readErr.message}`)));
|
|
584
608
|
res.on('end', () => resolve());
|
|
585
609
|
},
|
|
586
610
|
);
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
} from '../metrics/base.js';
|
|
38
38
|
import { DeferredReadableStream } from '../stream/deferred_stream.js';
|
|
39
39
|
import { STT, type STTError, type SpeechEvent } from '../stt/stt.js';
|
|
40
|
-
import { traceTypes, tracer } from '../telemetry/index.js';
|
|
40
|
+
import { recordRealtimeMetrics, traceTypes, tracer } from '../telemetry/index.js';
|
|
41
41
|
import { splitWords } from '../tokenize/basic/word.js';
|
|
42
42
|
import { TTS, type TTSError } from '../tts/tts.js';
|
|
43
43
|
import { Future, Task, cancelAndWait, waitFor } from '../utils.js';
|
|
@@ -91,6 +91,7 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
91
91
|
private started = false;
|
|
92
92
|
private audioRecognition?: AudioRecognition;
|
|
93
93
|
private realtimeSession?: RealtimeSession;
|
|
94
|
+
private realtimeSpans?: Map<string, Span>; // Maps response_id to OTEL span for metrics recording
|
|
94
95
|
private turnDetectionMode?: Exclude<TurnDetectionMode, _TurnDetector>;
|
|
95
96
|
private logger = log();
|
|
96
97
|
private _draining = false;
|
|
@@ -218,6 +219,7 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
218
219
|
|
|
219
220
|
if (this.llm instanceof RealtimeModel) {
|
|
220
221
|
this.realtimeSession = this.llm.session();
|
|
222
|
+
this.realtimeSpans = new Map<string, Span>();
|
|
221
223
|
this.realtimeSession.on('generation_created', (ev) => this.onGenerationCreated(ev));
|
|
222
224
|
this.realtimeSession.on('input_speech_started', (ev) => this.onInputSpeechStarted(ev));
|
|
223
225
|
this.realtimeSession.on('input_speech_stopped', (ev) => this.onInputSpeechStopped(ev));
|
|
@@ -505,6 +507,16 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
505
507
|
if (speechHandle && (ev.type === 'llm_metrics' || ev.type === 'tts_metrics')) {
|
|
506
508
|
ev.speechId = speechHandle.id;
|
|
507
509
|
}
|
|
510
|
+
|
|
511
|
+
// Record realtime metrics on the associated span (if available)
|
|
512
|
+
if (ev.type === 'realtime_model_metrics' && this.realtimeSpans) {
|
|
513
|
+
const span = this.realtimeSpans.get(ev.requestId);
|
|
514
|
+
if (span) {
|
|
515
|
+
recordRealtimeMetrics(span, ev);
|
|
516
|
+
this.realtimeSpans.delete(ev.requestId);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
508
520
|
this.agentSession.emit(
|
|
509
521
|
AgentSessionEventTypes.MetricsCollected,
|
|
510
522
|
createMetricsCollectedEvent({ metrics: ev }),
|
|
@@ -1722,6 +1734,12 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
1722
1734
|
throw new Error('llm is not a realtime model');
|
|
1723
1735
|
}
|
|
1724
1736
|
|
|
1737
|
+
// Store span for metrics recording when they arrive later
|
|
1738
|
+
span.setAttribute(traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.llm.model);
|
|
1739
|
+
if (this.realtimeSpans && ev.responseId) {
|
|
1740
|
+
this.realtimeSpans.set(ev.responseId, span);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1725
1743
|
this.logger.debug(
|
|
1726
1744
|
{ speech_id: speechHandle.id, stepIndex: speechHandle.numSteps },
|
|
1727
1745
|
'realtime generation started',
|
|
@@ -2009,7 +2027,13 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
2009
2027
|
|
|
2010
2028
|
await executeToolsTask.result;
|
|
2011
2029
|
|
|
2012
|
-
if (toolOutput.output.length === 0)
|
|
2030
|
+
if (toolOutput.output.length === 0) {
|
|
2031
|
+
// return to listening state for thinking-only turns (no audio output, no tools)
|
|
2032
|
+
if (!speechHandle.interrupted) {
|
|
2033
|
+
this.agentSession._updateAgentState('listening');
|
|
2034
|
+
}
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2013
2037
|
|
|
2014
2038
|
// important: no agent ouput should be used after this point
|
|
2015
2039
|
const { maxToolSteps } = this.agentSession.options;
|
|
@@ -2274,6 +2298,7 @@ export class AgentActivity implements RecognitionHooks {
|
|
|
2274
2298
|
}
|
|
2275
2299
|
|
|
2276
2300
|
this.detachAudioInput();
|
|
2301
|
+
this.realtimeSpans?.clear();
|
|
2277
2302
|
await this.realtimeSession?.close();
|
|
2278
2303
|
await this.audioRecognition?.close();
|
|
2279
2304
|
await this._mainTask?.cancelAndWait();
|
package/src/voice/generation.ts
CHANGED
|
@@ -442,6 +442,8 @@ export function performLLMInference(
|
|
|
442
442
|
callId: `${data.id}/fnc_${data.generatedToolCalls.length}`,
|
|
443
443
|
name: tool.name,
|
|
444
444
|
args: tool.args,
|
|
445
|
+
// Preserve thought signature for Gemini 3+ thinking mode
|
|
446
|
+
thoughtSignature: tool.thoughtSignature,
|
|
445
447
|
});
|
|
446
448
|
|
|
447
449
|
data.generatedToolCalls.push(toolCall);
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`oaiParams > common types 1`] = `
|
|
4
|
-
{
|
|
5
|
-
"additionalProperties": false,
|
|
6
|
-
"properties": {
|
|
7
|
-
"address": {
|
|
8
|
-
"additionalProperties": false,
|
|
9
|
-
"properties": {
|
|
10
|
-
"city": {
|
|
11
|
-
"type": "string",
|
|
12
|
-
},
|
|
13
|
-
"state": {
|
|
14
|
-
"type": "string",
|
|
15
|
-
},
|
|
16
|
-
"street": {
|
|
17
|
-
"type": "string",
|
|
18
|
-
},
|
|
19
|
-
"zip": {
|
|
20
|
-
"type": "string",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
"required": [
|
|
24
|
-
"street",
|
|
25
|
-
"city",
|
|
26
|
-
"state",
|
|
27
|
-
"zip",
|
|
28
|
-
],
|
|
29
|
-
"type": "object",
|
|
30
|
-
},
|
|
31
|
-
"age": {
|
|
32
|
-
"type": "number",
|
|
33
|
-
},
|
|
34
|
-
"favoriteColor": {
|
|
35
|
-
"enum": [
|
|
36
|
-
"red",
|
|
37
|
-
"green",
|
|
38
|
-
"blue",
|
|
39
|
-
],
|
|
40
|
-
"type": "string",
|
|
41
|
-
},
|
|
42
|
-
"hobbies": {
|
|
43
|
-
"items": {
|
|
44
|
-
"type": "string",
|
|
45
|
-
},
|
|
46
|
-
"type": "array",
|
|
47
|
-
},
|
|
48
|
-
"isStudent": {
|
|
49
|
-
"type": "boolean",
|
|
50
|
-
},
|
|
51
|
-
"name": {
|
|
52
|
-
"type": "string",
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
"required": [
|
|
56
|
-
"name",
|
|
57
|
-
"age",
|
|
58
|
-
"isStudent",
|
|
59
|
-
"hobbies",
|
|
60
|
-
"favoriteColor",
|
|
61
|
-
"address",
|
|
62
|
-
],
|
|
63
|
-
"type": "object",
|
|
64
|
-
}
|
|
65
|
-
`;
|