@livekit/agents 1.0.21 → 1.0.23
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/inference/api_protos.cjs +2 -2
- package/dist/inference/api_protos.cjs.map +1 -1
- package/dist/inference/api_protos.d.cts +16 -16
- package/dist/inference/api_protos.d.ts +16 -16
- package/dist/inference/api_protos.js +2 -2
- package/dist/inference/api_protos.js.map +1 -1
- package/dist/inference/stt.cjs +42 -30
- package/dist/inference/stt.cjs.map +1 -1
- package/dist/inference/stt.d.ts.map +1 -1
- package/dist/inference/stt.js +42 -30
- package/dist/inference/stt.js.map +1 -1
- package/dist/inference/tts.cjs +2 -3
- package/dist/inference/tts.cjs.map +1 -1
- package/dist/inference/tts.d.ts.map +1 -1
- package/dist/inference/tts.js +2 -3
- package/dist/inference/tts.js.map +1 -1
- package/dist/ipc/job_proc_lazy_main.cjs +35 -1
- package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
- package/dist/ipc/job_proc_lazy_main.js +13 -1
- package/dist/ipc/job_proc_lazy_main.js.map +1 -1
- package/dist/job.cjs +52 -6
- package/dist/job.cjs.map +1 -1
- package/dist/job.d.cts +2 -0
- package/dist/job.d.ts +2 -0
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +52 -6
- package/dist/job.js.map +1 -1
- package/dist/llm/llm.cjs +38 -3
- package/dist/llm/llm.cjs.map +1 -1
- package/dist/llm/llm.d.cts +1 -0
- package/dist/llm/llm.d.ts +1 -0
- package/dist/llm/llm.d.ts.map +1 -1
- package/dist/llm/llm.js +38 -3
- package/dist/llm/llm.js.map +1 -1
- package/dist/log.cjs +34 -10
- package/dist/log.cjs.map +1 -1
- package/dist/log.d.cts +7 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +34 -11
- package/dist/log.js.map +1 -1
- package/dist/stt/stt.cjs +18 -5
- package/dist/stt/stt.cjs.map +1 -1
- package/dist/stt/stt.d.ts.map +1 -1
- package/dist/stt/stt.js +18 -5
- package/dist/stt/stt.js.map +1 -1
- package/dist/telemetry/index.cjs +23 -2
- package/dist/telemetry/index.cjs.map +1 -1
- package/dist/telemetry/index.d.cts +4 -1
- package/dist/telemetry/index.d.ts +4 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +27 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/logging.cjs +65 -0
- package/dist/telemetry/logging.cjs.map +1 -0
- package/dist/telemetry/logging.d.cts +21 -0
- package/dist/telemetry/logging.d.ts +21 -0
- package/dist/telemetry/logging.d.ts.map +1 -0
- package/dist/telemetry/logging.js +40 -0
- package/dist/telemetry/logging.js.map +1 -0
- package/dist/telemetry/otel_http_exporter.cjs +144 -0
- package/dist/telemetry/otel_http_exporter.cjs.map +1 -0
- package/dist/telemetry/otel_http_exporter.d.cts +62 -0
- package/dist/telemetry/otel_http_exporter.d.ts +62 -0
- package/dist/telemetry/otel_http_exporter.d.ts.map +1 -0
- package/dist/telemetry/otel_http_exporter.js +120 -0
- package/dist/telemetry/otel_http_exporter.js.map +1 -0
- package/dist/telemetry/pino_otel_transport.cjs +217 -0
- package/dist/telemetry/pino_otel_transport.cjs.map +1 -0
- package/dist/telemetry/pino_otel_transport.d.cts +58 -0
- package/dist/telemetry/pino_otel_transport.d.ts +58 -0
- package/dist/telemetry/pino_otel_transport.d.ts.map +1 -0
- package/dist/telemetry/pino_otel_transport.js +189 -0
- package/dist/telemetry/pino_otel_transport.js.map +1 -0
- package/dist/telemetry/traces.cjs +225 -16
- package/dist/telemetry/traces.cjs.map +1 -1
- package/dist/telemetry/traces.d.cts +17 -0
- package/dist/telemetry/traces.d.ts +17 -0
- package/dist/telemetry/traces.d.ts.map +1 -1
- package/dist/telemetry/traces.js +211 -14
- package/dist/telemetry/traces.js.map +1 -1
- package/dist/tts/tts.cjs +68 -20
- package/dist/tts/tts.cjs.map +1 -1
- package/dist/tts/tts.d.cts +2 -0
- package/dist/tts/tts.d.ts +2 -0
- package/dist/tts/tts.d.ts.map +1 -1
- package/dist/tts/tts.js +68 -20
- package/dist/tts/tts.js.map +1 -1
- package/dist/utils.cjs +6 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +5 -0
- package/dist/utils.js.map +1 -1
- package/dist/voice/agent_activity.cjs +93 -7
- package/dist/voice/agent_activity.cjs.map +1 -1
- package/dist/voice/agent_activity.d.cts +3 -0
- package/dist/voice/agent_activity.d.ts +3 -0
- package/dist/voice/agent_activity.d.ts.map +1 -1
- package/dist/voice/agent_activity.js +93 -7
- package/dist/voice/agent_activity.js.map +1 -1
- package/dist/voice/agent_session.cjs +122 -27
- package/dist/voice/agent_session.cjs.map +1 -1
- package/dist/voice/agent_session.d.cts +15 -0
- package/dist/voice/agent_session.d.ts +15 -0
- package/dist/voice/agent_session.d.ts.map +1 -1
- package/dist/voice/agent_session.js +122 -27
- package/dist/voice/agent_session.js.map +1 -1
- package/dist/voice/audio_recognition.cjs +69 -22
- package/dist/voice/audio_recognition.cjs.map +1 -1
- package/dist/voice/audio_recognition.d.cts +5 -0
- package/dist/voice/audio_recognition.d.ts +5 -0
- package/dist/voice/audio_recognition.d.ts.map +1 -1
- package/dist/voice/audio_recognition.js +69 -22
- package/dist/voice/audio_recognition.js.map +1 -1
- package/dist/voice/generation.cjs +43 -3
- package/dist/voice/generation.cjs.map +1 -1
- package/dist/voice/generation.d.ts.map +1 -1
- package/dist/voice/generation.js +43 -3
- package/dist/voice/generation.js.map +1 -1
- package/dist/voice/report.cjs +3 -2
- package/dist/voice/report.cjs.map +1 -1
- package/dist/voice/report.d.cts +7 -1
- package/dist/voice/report.d.ts +7 -1
- package/dist/voice/report.d.ts.map +1 -1
- package/dist/voice/report.js +3 -2
- package/dist/voice/report.js.map +1 -1
- package/package.json +8 -2
- package/src/inference/api_protos.ts +2 -2
- package/src/inference/stt.ts +48 -33
- package/src/inference/tts.ts +4 -3
- package/src/ipc/job_proc_lazy_main.ts +12 -1
- package/src/job.ts +59 -10
- package/src/llm/llm.ts +48 -5
- package/src/log.ts +52 -15
- package/src/stt/stt.ts +18 -5
- package/src/telemetry/index.ts +22 -4
- package/src/telemetry/logging.ts +55 -0
- package/src/telemetry/otel_http_exporter.ts +191 -0
- package/src/telemetry/pino_otel_transport.ts +265 -0
- package/src/telemetry/traces.ts +320 -20
- package/src/tts/tts.ts +85 -24
- package/src/utils.ts +5 -0
- package/src/voice/agent_activity.ts +140 -22
- package/src/voice/agent_session.ts +174 -34
- package/src/voice/audio_recognition.ts +85 -26
- package/src/voice/generation.ts +59 -7
- package/src/voice/report.ts +10 -4
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom Pino OTEL Transport
|
|
7
|
+
*
|
|
8
|
+
* Standalone exporter for Pino logs to LiveKit Cloud.
|
|
9
|
+
* Uses raw HTTP JSON format, bypassing the OTEL SDK.
|
|
10
|
+
*/
|
|
11
|
+
import { SeverityNumber } from '@opentelemetry/api-logs';
|
|
12
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
13
|
+
|
|
14
|
+
export interface PinoLogObject {
|
|
15
|
+
level: number;
|
|
16
|
+
time: number;
|
|
17
|
+
msg: string;
|
|
18
|
+
pid?: number;
|
|
19
|
+
hostname?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PinoCloudExporterConfig {
|
|
24
|
+
cloudHostname: string;
|
|
25
|
+
roomId: string;
|
|
26
|
+
jobId: string;
|
|
27
|
+
loggerName?: string;
|
|
28
|
+
batchSize?: number;
|
|
29
|
+
flushIntervalMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mapPinoLevelToSeverity(pinoLevel: number): {
|
|
33
|
+
severityNumber: SeverityNumber;
|
|
34
|
+
severityText: string;
|
|
35
|
+
} {
|
|
36
|
+
if (pinoLevel <= 10) {
|
|
37
|
+
return { severityNumber: SeverityNumber.TRACE, severityText: 'TRACE' };
|
|
38
|
+
} else if (pinoLevel <= 20) {
|
|
39
|
+
return { severityNumber: SeverityNumber.DEBUG, severityText: 'DEBUG' };
|
|
40
|
+
} else if (pinoLevel <= 30) {
|
|
41
|
+
return { severityNumber: SeverityNumber.INFO, severityText: 'INFO' };
|
|
42
|
+
} else if (pinoLevel <= 40) {
|
|
43
|
+
return { severityNumber: SeverityNumber.WARN, severityText: 'WARN' };
|
|
44
|
+
} else if (pinoLevel <= 50) {
|
|
45
|
+
return { severityNumber: SeverityNumber.ERROR, severityText: 'ERROR' };
|
|
46
|
+
} else {
|
|
47
|
+
return { severityNumber: SeverityNumber.FATAL, severityText: 'FATAL' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EXCLUDE_FIELDS = new Set(['level', 'time', 'msg', 'pid', 'hostname', 'v']);
|
|
52
|
+
|
|
53
|
+
function convertValue(value: unknown): unknown {
|
|
54
|
+
if (value === null || value === undefined) {
|
|
55
|
+
return { stringValue: '' };
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
return { stringValue: value };
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'number') {
|
|
61
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'boolean') {
|
|
64
|
+
return { boolValue: value };
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'object') {
|
|
67
|
+
return { stringValue: JSON.stringify(value) };
|
|
68
|
+
}
|
|
69
|
+
return { stringValue: String(value) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Standalone Pino log exporter for LiveKit Cloud.
|
|
74
|
+
*
|
|
75
|
+
* Collects Pino logs, batches them, and sends via raw HTTP JSON.
|
|
76
|
+
* No OTEL SDK dependency
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const exporter = new PinoCloudExporter({
|
|
81
|
+
* cloudHostname: 'cloud.livekit.io',
|
|
82
|
+
* roomId: 'RM_xxx',
|
|
83
|
+
* jobId: 'AJ_xxx',
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* // In Pino formatter hook:
|
|
87
|
+
* exporter.emit(logObj);
|
|
88
|
+
*
|
|
89
|
+
* // On session end:
|
|
90
|
+
* await exporter.flush();
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export class PinoCloudExporter {
|
|
94
|
+
private readonly config: PinoCloudExporterConfig;
|
|
95
|
+
private readonly loggerName: string;
|
|
96
|
+
private readonly batchSize: number;
|
|
97
|
+
private readonly flushIntervalMs: number;
|
|
98
|
+
private jwt: string | null = null;
|
|
99
|
+
private pendingLogs: any[] = [];
|
|
100
|
+
private flushTimer: NodeJS.Timeout | null = null;
|
|
101
|
+
|
|
102
|
+
constructor(config: PinoCloudExporterConfig) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.loggerName = config.loggerName || 'livekit.agents';
|
|
105
|
+
this.batchSize = config.batchSize || 100;
|
|
106
|
+
this.flushIntervalMs = config.flushIntervalMs || 5000;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
emit(logObj: PinoLogObject): void {
|
|
110
|
+
const record = this.convertToOtlpRecord(logObj);
|
|
111
|
+
this.pendingLogs.push(record);
|
|
112
|
+
|
|
113
|
+
if (!this.flushTimer) {
|
|
114
|
+
this.flushTimer = setTimeout(() => {
|
|
115
|
+
this.flush().catch(console.error);
|
|
116
|
+
}, this.flushIntervalMs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.pendingLogs.length >= this.batchSize) {
|
|
120
|
+
this.flush().catch(console.error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private convertToOtlpRecord(logObj: PinoLogObject): any {
|
|
125
|
+
const { severityNumber, severityText } = mapPinoLevelToSeverity(logObj.level);
|
|
126
|
+
|
|
127
|
+
const attributes: any[] = [
|
|
128
|
+
{ key: 'room_id', value: { stringValue: this.config.roomId } },
|
|
129
|
+
{ key: 'job_id', value: { stringValue: this.config.jobId } },
|
|
130
|
+
{ key: 'logger.name', value: { stringValue: this.loggerName } },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
if (logObj.pid !== undefined) {
|
|
134
|
+
attributes.push({ key: 'process.pid', value: { intValue: String(logObj.pid) } });
|
|
135
|
+
}
|
|
136
|
+
if (logObj.hostname !== undefined) {
|
|
137
|
+
attributes.push({ key: 'host.name', value: { stringValue: logObj.hostname } });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [key, value] of Object.entries(logObj)) {
|
|
141
|
+
if (!EXCLUDE_FIELDS.has(key)) {
|
|
142
|
+
attributes.push({ key, value: convertValue(value) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
timeUnixNano: String(BigInt(logObj.time) * BigInt(1_000_000)),
|
|
148
|
+
observedTimeUnixNano: String(BigInt(Date.now()) * BigInt(1_000_000)),
|
|
149
|
+
severityNumber,
|
|
150
|
+
severityText,
|
|
151
|
+
body: { stringValue: logObj.msg || '' },
|
|
152
|
+
attributes,
|
|
153
|
+
traceId: '',
|
|
154
|
+
spanId: '',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async flush(): Promise<void> {
|
|
159
|
+
if (this.flushTimer) {
|
|
160
|
+
clearTimeout(this.flushTimer);
|
|
161
|
+
this.flushTimer = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.pendingLogs.length === 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const logs = this.pendingLogs;
|
|
169
|
+
this.pendingLogs = [];
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await this.sendLogs(logs);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.pendingLogs = [...logs, ...this.pendingLogs];
|
|
175
|
+
console.error('[PinoCloudExporter] Failed to flush logs:', error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async sendLogs(logRecords: any[]): Promise<void> {
|
|
180
|
+
await this.ensureJwt();
|
|
181
|
+
|
|
182
|
+
const payload = {
|
|
183
|
+
resourceLogs: [
|
|
184
|
+
{
|
|
185
|
+
resource: {
|
|
186
|
+
attributes: [
|
|
187
|
+
{ key: 'service.name', value: { stringValue: 'livekit-agents' } },
|
|
188
|
+
{ key: 'room_id', value: { stringValue: this.config.roomId } },
|
|
189
|
+
{ key: 'job_id', value: { stringValue: this.config.jobId } },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
scopeLogs: [
|
|
193
|
+
{
|
|
194
|
+
scope: {
|
|
195
|
+
name: this.loggerName,
|
|
196
|
+
attributes: [
|
|
197
|
+
{ key: 'room_id', value: { stringValue: this.config.roomId } },
|
|
198
|
+
{ key: 'job_id', value: { stringValue: this.config.jobId } },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
logRecords,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const endpoint = `https://${this.config.cloudHostname}/observability/logs/otlp/v0`;
|
|
209
|
+
|
|
210
|
+
const response = await fetch(endpoint, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: `Bearer ${this.jwt}`,
|
|
214
|
+
'Content-Type': 'application/json',
|
|
215
|
+
},
|
|
216
|
+
body: JSON.stringify(payload),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const text = await response.text();
|
|
221
|
+
throw new Error(`Log export failed: ${response.status} ${response.statusText} - ${text}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async ensureJwt(): Promise<void> {
|
|
226
|
+
if (this.jwt) return;
|
|
227
|
+
|
|
228
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
229
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
230
|
+
|
|
231
|
+
if (!apiKey || !apiSecret) {
|
|
232
|
+
throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });
|
|
236
|
+
token.addObservabilityGrant({ write: true });
|
|
237
|
+
this.jwt = await token.toJwt();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async shutdown(): Promise<void> {
|
|
241
|
+
await this.flush();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let globalExporter: PinoCloudExporter | null = null;
|
|
246
|
+
|
|
247
|
+
export function initPinoCloudExporter(config: PinoCloudExporterConfig): void {
|
|
248
|
+
globalExporter = new PinoCloudExporter(config);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function getPinoCloudExporter(): PinoCloudExporter | null {
|
|
252
|
+
return globalExporter;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function emitToOtel(logObj: PinoLogObject): void {
|
|
256
|
+
if (globalExporter) {
|
|
257
|
+
globalExporter.emit(logObj);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function flushPinoLogs(): Promise<void> {
|
|
262
|
+
if (globalExporter) {
|
|
263
|
+
await globalExporter.flush();
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/telemetry/traces.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import { MetricsRecordingHeader } from '@livekit/protocol';
|
|
4
5
|
import {
|
|
5
6
|
type Attributes,
|
|
6
7
|
type Context,
|
|
@@ -11,13 +12,20 @@ import {
|
|
|
11
12
|
context as otelContext,
|
|
12
13
|
trace,
|
|
13
14
|
} from '@opentelemetry/api';
|
|
14
|
-
import {
|
|
15
|
+
import { SeverityNumber } from '@opentelemetry/api-logs';
|
|
16
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
|
|
15
17
|
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
|
|
16
18
|
import { Resource } from '@opentelemetry/resources';
|
|
17
19
|
import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
18
20
|
import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
19
21
|
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
|
22
|
+
import FormData from 'form-data';
|
|
20
23
|
import { AccessToken } from 'livekit-server-sdk';
|
|
24
|
+
import type { ChatContent, ChatItem } from '../llm/index.js';
|
|
25
|
+
import { enableOtelLogging } from '../log.js';
|
|
26
|
+
import type { SessionReport } from '../voice/report.js';
|
|
27
|
+
import { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';
|
|
28
|
+
import { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';
|
|
21
29
|
|
|
22
30
|
export interface StartSpanOptions {
|
|
23
31
|
/** Name of the span */
|
|
@@ -94,19 +102,15 @@ class DynamicTracer {
|
|
|
94
102
|
const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true
|
|
95
103
|
const opts: SpanOptions = { attributes: options.attributes };
|
|
96
104
|
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} finally {
|
|
105
|
-
if (endOnExit) {
|
|
106
|
-
span.end();
|
|
107
|
-
}
|
|
105
|
+
// Directly return the tracer's startActiveSpan result - it handles async correctly
|
|
106
|
+
return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {
|
|
107
|
+
try {
|
|
108
|
+
return await fn(span);
|
|
109
|
+
} finally {
|
|
110
|
+
if (endOnExit) {
|
|
111
|
+
span.end();
|
|
108
112
|
}
|
|
109
|
-
}
|
|
113
|
+
}
|
|
110
114
|
});
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -162,10 +166,6 @@ class MetadataSpanProcessor implements SpanProcessor {
|
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
// TODO(brian): PR4 - Add MetadataLogProcessor for structured logging
|
|
166
|
-
|
|
167
|
-
// TODO(brian): PR4 - Add ExtraDetailsProcessor for structured logging
|
|
168
|
-
|
|
169
169
|
/**
|
|
170
170
|
* Set the tracer provider for the livekit-agents framework.
|
|
171
171
|
* This should be called before agent session start if using custom tracer providers.
|
|
@@ -254,13 +254,313 @@ export async function setupCloudTracer(options: {
|
|
|
254
254
|
});
|
|
255
255
|
tracerProvider.register();
|
|
256
256
|
|
|
257
|
-
// Metadata processor is already configured in the constructor above
|
|
258
257
|
setTracerProvider(tracerProvider);
|
|
259
258
|
|
|
260
|
-
//
|
|
261
|
-
|
|
259
|
+
// Initialize standalone Pino cloud exporter (no OTEL SDK dependency)
|
|
260
|
+
initPinoCloudExporter({
|
|
261
|
+
cloudHostname,
|
|
262
|
+
roomId,
|
|
263
|
+
jobId,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
enableOtelLogging();
|
|
262
267
|
} catch (error) {
|
|
263
268
|
console.error('Failed to setup cloud tracer:', error);
|
|
264
269
|
throw error;
|
|
265
270
|
}
|
|
266
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Flush all pending Pino logs to ensure they are exported.
|
|
275
|
+
* Call this before session/job ends to ensure all logs are sent.
|
|
276
|
+
*
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
export async function flushOtelLogs(): Promise<void> {
|
|
280
|
+
await flushPinoLogs();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Convert ChatItem to proto-compatible dictionary format.
|
|
285
|
+
* TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published
|
|
286
|
+
*/
|
|
287
|
+
function chatItemToProto(item: ChatItem): Record<string, any> {
|
|
288
|
+
const itemDict: Record<string, any> = {};
|
|
289
|
+
|
|
290
|
+
if (item.type === 'message') {
|
|
291
|
+
const roleMap: Record<string, string> = {
|
|
292
|
+
developer: 'DEVELOPER',
|
|
293
|
+
system: 'SYSTEM',
|
|
294
|
+
user: 'USER',
|
|
295
|
+
assistant: 'ASSISTANT',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const msg: Record<string, any> = {
|
|
299
|
+
id: item.id,
|
|
300
|
+
role: roleMap[item.role] || item.role.toUpperCase(),
|
|
301
|
+
content: item.content.map((c: ChatContent) => ({ text: c })),
|
|
302
|
+
createdAt: toRFC3339(item.createdAt),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (item.interrupted) {
|
|
306
|
+
msg.interrupted = item.interrupted;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// TODO(brian): Add extra and transcriptConfidence to ChatMessage
|
|
310
|
+
// if (item.extra && Object.keys(item.extra).length > 0) {
|
|
311
|
+
// msg.extra = item.extra;
|
|
312
|
+
// }
|
|
313
|
+
|
|
314
|
+
// if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {
|
|
315
|
+
// msg.transcriptConfidence = item.transcriptConfidence;
|
|
316
|
+
// }
|
|
317
|
+
|
|
318
|
+
// TODO(brian): Add metrics to ChatMessage
|
|
319
|
+
// const metrics = item.metrics || {};
|
|
320
|
+
// if (Object.keys(metrics).length > 0) {
|
|
321
|
+
// msg.metrics = {};
|
|
322
|
+
// if (metrics.started_speaking_at) {
|
|
323
|
+
// msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);
|
|
324
|
+
// }
|
|
325
|
+
// if (metrics.stopped_speaking_at) {
|
|
326
|
+
// msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);
|
|
327
|
+
// }
|
|
328
|
+
// if (metrics.transcription_delay !== undefined) {
|
|
329
|
+
// msg.metrics.transcriptionDelay = metrics.transcription_delay;
|
|
330
|
+
// }
|
|
331
|
+
// if (metrics.end_of_turn_delay !== undefined) {
|
|
332
|
+
// msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;
|
|
333
|
+
// }
|
|
334
|
+
// if (metrics.on_user_turn_completed_delay !== undefined) {
|
|
335
|
+
// msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;
|
|
336
|
+
// }
|
|
337
|
+
// if (metrics.llm_node_ttft !== undefined) {
|
|
338
|
+
// msg.metrics.llmNodeTtft = metrics.llm_node_ttft;
|
|
339
|
+
// }
|
|
340
|
+
// if (metrics.tts_node_ttfb !== undefined) {
|
|
341
|
+
// msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;
|
|
342
|
+
// }
|
|
343
|
+
// if (metrics.e2e_latency !== undefined) {
|
|
344
|
+
// msg.metrics.e2eLatency = metrics.e2e_latency;
|
|
345
|
+
// }
|
|
346
|
+
// }
|
|
347
|
+
|
|
348
|
+
itemDict.message = msg;
|
|
349
|
+
} else if (item.type === 'function_call') {
|
|
350
|
+
itemDict.functionCall = {
|
|
351
|
+
id: item.id,
|
|
352
|
+
callId: item.callId,
|
|
353
|
+
arguments: item.args,
|
|
354
|
+
name: item.name,
|
|
355
|
+
createdAt: toRFC3339(item.createdAt),
|
|
356
|
+
};
|
|
357
|
+
} else if (item.type === 'function_call_output') {
|
|
358
|
+
itemDict.functionCallOutput = {
|
|
359
|
+
id: item.id,
|
|
360
|
+
name: item.name,
|
|
361
|
+
callId: item.callId,
|
|
362
|
+
output: item.output,
|
|
363
|
+
isError: item.isError,
|
|
364
|
+
createdAt: toRFC3339(item.createdAt),
|
|
365
|
+
};
|
|
366
|
+
} else if (item.type === 'agent_handoff') {
|
|
367
|
+
const handoff: Record<string, any> = {
|
|
368
|
+
id: item.id,
|
|
369
|
+
newAgentId: item.newAgentId,
|
|
370
|
+
createdAt: toRFC3339(item.createdAt),
|
|
371
|
+
};
|
|
372
|
+
if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {
|
|
373
|
+
handoff.oldAgentId = item.oldAgentId;
|
|
374
|
+
}
|
|
375
|
+
itemDict.agentHandoff = handoff;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {
|
|
380
|
+
itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);
|
|
381
|
+
} else if (
|
|
382
|
+
item.type === 'function_call_output' &&
|
|
383
|
+
typeof itemDict.functionCallOutput?.output === 'string'
|
|
384
|
+
) {
|
|
385
|
+
itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// ignore parsing errors
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return itemDict;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Convert timestamp to RFC3339 format matching Python's _to_rfc3339.
|
|
396
|
+
* Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.
|
|
397
|
+
* @internal
|
|
398
|
+
*/
|
|
399
|
+
function toRFC3339(valueMs: number | Date): string {
|
|
400
|
+
// valueMs is already in milliseconds (from Date.now())
|
|
401
|
+
const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);
|
|
402
|
+
// Truncate sub-millisecond precision
|
|
403
|
+
const truncated = new Date(Math.floor(dt.getTime()));
|
|
404
|
+
return truncated.toISOString();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Upload session report to LiveKit Cloud observability.
|
|
409
|
+
* @param options - Configuration with agentName, cloudHostname, and report
|
|
410
|
+
*/
|
|
411
|
+
export async function uploadSessionReport(options: {
|
|
412
|
+
agentName: string;
|
|
413
|
+
cloudHostname: string;
|
|
414
|
+
report: SessionReport;
|
|
415
|
+
}): Promise<void> {
|
|
416
|
+
const { agentName, cloudHostname, report } = options;
|
|
417
|
+
|
|
418
|
+
// Create OTLP HTTP exporter for chat history logs
|
|
419
|
+
// Uses raw HTTP JSON format which is required by LiveKit Cloud
|
|
420
|
+
const logExporter = new SimpleOTLPHttpLogExporter({
|
|
421
|
+
cloudHostname,
|
|
422
|
+
resourceAttributes: {
|
|
423
|
+
room_id: report.roomId,
|
|
424
|
+
job_id: report.jobId,
|
|
425
|
+
},
|
|
426
|
+
scopeName: 'chat_history',
|
|
427
|
+
scopeAttributes: {
|
|
428
|
+
room_id: report.roomId,
|
|
429
|
+
job_id: report.jobId,
|
|
430
|
+
room: report.room,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Build log records for session report and chat items
|
|
435
|
+
const logRecords: SimpleLogRecord[] = [];
|
|
436
|
+
|
|
437
|
+
const commonAttrs = {
|
|
438
|
+
room_id: report.roomId,
|
|
439
|
+
job_id: report.jobId,
|
|
440
|
+
'logger.name': 'chat_history',
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
logRecords.push({
|
|
444
|
+
body: 'session report',
|
|
445
|
+
timestampMs: report.startedAt || report.timestamp || 0,
|
|
446
|
+
attributes: {
|
|
447
|
+
...commonAttrs,
|
|
448
|
+
'session.options': report.options || {},
|
|
449
|
+
'session.report_timestamp': report.timestamp,
|
|
450
|
+
agent_name: agentName,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Track last timestamp to ensure monotonic ordering when items have identical timestamps
|
|
455
|
+
// This fixes the issue where function_call and function_call_output with same timestamp
|
|
456
|
+
// get reordered by the dashboard
|
|
457
|
+
let lastTimestamp = 0;
|
|
458
|
+
for (const item of report.chatHistory.items) {
|
|
459
|
+
// Ensure monotonically increasing timestamps for proper ordering
|
|
460
|
+
// Add 0.001ms (1 microsecond) offset when timestamps collide
|
|
461
|
+
let itemTimestamp = item.createdAt;
|
|
462
|
+
if (itemTimestamp <= lastTimestamp) {
|
|
463
|
+
itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond
|
|
464
|
+
}
|
|
465
|
+
lastTimestamp = itemTimestamp;
|
|
466
|
+
|
|
467
|
+
const itemProto = chatItemToProto(item);
|
|
468
|
+
let severityNumber = SeverityNumber.UNSPECIFIED;
|
|
469
|
+
let severityText = 'unspecified';
|
|
470
|
+
|
|
471
|
+
if (item.type === 'function_call_output' && item.isError) {
|
|
472
|
+
severityNumber = SeverityNumber.ERROR;
|
|
473
|
+
severityText = 'error';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
logRecords.push({
|
|
477
|
+
body: 'chat item',
|
|
478
|
+
timestampMs: itemTimestamp, // Adjusted for monotonic ordering
|
|
479
|
+
attributes: { 'chat.item': itemProto, ...commonAttrs },
|
|
480
|
+
severityNumber,
|
|
481
|
+
severityText,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
await logExporter.export(logRecords);
|
|
485
|
+
|
|
486
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
487
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
488
|
+
|
|
489
|
+
if (!apiKey || !apiSecret) {
|
|
490
|
+
throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });
|
|
494
|
+
token.addObservabilityGrant({ write: true });
|
|
495
|
+
const jwt = await token.toJwt();
|
|
496
|
+
|
|
497
|
+
const formData = new FormData();
|
|
498
|
+
|
|
499
|
+
// Add header (protobuf MetricsRecordingHeader)
|
|
500
|
+
const headerMsg = new MetricsRecordingHeader({
|
|
501
|
+
roomId: report.roomId,
|
|
502
|
+
duration: BigInt(0), // TODO: Calculate actual duration from report
|
|
503
|
+
startTime: {
|
|
504
|
+
seconds: BigInt(Math.floor(report.timestamp / 1000)),
|
|
505
|
+
nanos: Math.floor((report.timestamp % 1000) * 1e6),
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const headerBytes = Buffer.from(headerMsg.toBinary());
|
|
510
|
+
formData.append('header', headerBytes, {
|
|
511
|
+
filename: 'header.binpb',
|
|
512
|
+
contentType: 'application/protobuf',
|
|
513
|
+
knownLength: headerBytes.length,
|
|
514
|
+
header: {
|
|
515
|
+
'Content-Type': 'application/protobuf',
|
|
516
|
+
'Content-Length': headerBytes.length.toString(),
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Add chat_history JSON
|
|
521
|
+
const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));
|
|
522
|
+
const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');
|
|
523
|
+
formData.append('chat_history', chatHistoryBuffer, {
|
|
524
|
+
filename: 'chat_history.json',
|
|
525
|
+
contentType: 'application/json',
|
|
526
|
+
knownLength: chatHistoryBuffer.length,
|
|
527
|
+
header: {
|
|
528
|
+
'Content-Type': 'application/json',
|
|
529
|
+
'Content-Length': chatHistoryBuffer.length.toString(),
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// TODO(brian): Add audio recording file when recorder IO is implemented
|
|
534
|
+
|
|
535
|
+
// Upload to LiveKit Cloud using form-data's submit method
|
|
536
|
+
// This properly streams the multipart form with all headers including Content-Length
|
|
537
|
+
return new Promise<void>((resolve, reject) => {
|
|
538
|
+
formData.submit(
|
|
539
|
+
{
|
|
540
|
+
protocol: 'https:',
|
|
541
|
+
host: cloudHostname,
|
|
542
|
+
path: '/observability/recordings/v0',
|
|
543
|
+
method: 'POST',
|
|
544
|
+
headers: {
|
|
545
|
+
Authorization: `Bearer ${jwt}`,
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
(err, res) => {
|
|
549
|
+
if (err) {
|
|
550
|
+
reject(new Error(`Failed to upload session report: ${err.message}`));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
555
|
+
reject(
|
|
556
|
+
new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`),
|
|
557
|
+
);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
res.resume(); // Drain the response
|
|
562
|
+
res.on('end', () => resolve());
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
}
|