@livekit/agents 1.0.18 → 1.0.19

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.
Files changed (152) hide show
  1. package/dist/index.cjs +3 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +2 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/inference/api_protos.d.cts +12 -12
  9. package/dist/inference/api_protos.d.ts +12 -12
  10. package/dist/inference/tts.cjs +1 -1
  11. package/dist/inference/tts.cjs.map +1 -1
  12. package/dist/inference/tts.js +1 -1
  13. package/dist/inference/tts.js.map +1 -1
  14. package/dist/ipc/job_proc_lazy_main.cjs +6 -2
  15. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  16. package/dist/ipc/job_proc_lazy_main.js +6 -2
  17. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  18. package/dist/job.cjs +31 -0
  19. package/dist/job.cjs.map +1 -1
  20. package/dist/job.d.cts +6 -0
  21. package/dist/job.d.ts +6 -0
  22. package/dist/job.d.ts.map +1 -1
  23. package/dist/job.js +31 -0
  24. package/dist/job.js.map +1 -1
  25. package/dist/llm/chat_context.cjs +33 -0
  26. package/dist/llm/chat_context.cjs.map +1 -1
  27. package/dist/llm/chat_context.d.cts +22 -2
  28. package/dist/llm/chat_context.d.ts +22 -2
  29. package/dist/llm/chat_context.d.ts.map +1 -1
  30. package/dist/llm/chat_context.js +32 -0
  31. package/dist/llm/chat_context.js.map +1 -1
  32. package/dist/llm/index.cjs +2 -0
  33. package/dist/llm/index.cjs.map +1 -1
  34. package/dist/llm/index.d.cts +1 -1
  35. package/dist/llm/index.d.ts +1 -1
  36. package/dist/llm/index.d.ts.map +1 -1
  37. package/dist/llm/index.js +2 -0
  38. package/dist/llm/index.js.map +1 -1
  39. package/dist/llm/llm.cjs.map +1 -1
  40. package/dist/llm/llm.d.ts.map +1 -1
  41. package/dist/llm/llm.js.map +1 -1
  42. package/dist/llm/provider_format/google.test.cjs +48 -0
  43. package/dist/llm/provider_format/google.test.cjs.map +1 -1
  44. package/dist/llm/provider_format/google.test.js +54 -1
  45. package/dist/llm/provider_format/google.test.js.map +1 -1
  46. package/dist/llm/provider_format/index.d.cts +1 -1
  47. package/dist/llm/provider_format/index.d.ts +1 -1
  48. package/dist/llm/provider_format/openai.cjs +1 -2
  49. package/dist/llm/provider_format/openai.cjs.map +1 -1
  50. package/dist/llm/provider_format/openai.js +1 -2
  51. package/dist/llm/provider_format/openai.js.map +1 -1
  52. package/dist/llm/provider_format/openai.test.cjs +32 -0
  53. package/dist/llm/provider_format/openai.test.cjs.map +1 -1
  54. package/dist/llm/provider_format/openai.test.js +38 -1
  55. package/dist/llm/provider_format/openai.test.js.map +1 -1
  56. package/dist/log.cjs.map +1 -1
  57. package/dist/log.d.ts.map +1 -1
  58. package/dist/log.js.map +1 -1
  59. package/dist/telemetry/index.cjs +51 -0
  60. package/dist/telemetry/index.cjs.map +1 -0
  61. package/dist/telemetry/index.d.cts +4 -0
  62. package/dist/telemetry/index.d.ts +4 -0
  63. package/dist/telemetry/index.d.ts.map +1 -0
  64. package/dist/telemetry/index.js +12 -0
  65. package/dist/telemetry/index.js.map +1 -0
  66. package/dist/telemetry/trace_types.cjs +191 -0
  67. package/dist/telemetry/trace_types.cjs.map +1 -0
  68. package/dist/telemetry/trace_types.d.cts +56 -0
  69. package/dist/telemetry/trace_types.d.ts +56 -0
  70. package/dist/telemetry/trace_types.d.ts.map +1 -0
  71. package/dist/telemetry/trace_types.js +113 -0
  72. package/dist/telemetry/trace_types.js.map +1 -0
  73. package/dist/telemetry/traces.cjs +196 -0
  74. package/dist/telemetry/traces.cjs.map +1 -0
  75. package/dist/telemetry/traces.d.cts +97 -0
  76. package/dist/telemetry/traces.d.ts +97 -0
  77. package/dist/telemetry/traces.d.ts.map +1 -0
  78. package/dist/telemetry/traces.js +173 -0
  79. package/dist/telemetry/traces.js.map +1 -0
  80. package/dist/telemetry/utils.cjs +86 -0
  81. package/dist/telemetry/utils.cjs.map +1 -0
  82. package/dist/telemetry/utils.d.cts +5 -0
  83. package/dist/telemetry/utils.d.ts +5 -0
  84. package/dist/telemetry/utils.d.ts.map +1 -0
  85. package/dist/telemetry/utils.js +51 -0
  86. package/dist/telemetry/utils.js.map +1 -0
  87. package/dist/tts/tts.cjs.map +1 -1
  88. package/dist/tts/tts.d.ts.map +1 -1
  89. package/dist/tts/tts.js.map +1 -1
  90. package/dist/voice/agent.cjs +15 -0
  91. package/dist/voice/agent.cjs.map +1 -1
  92. package/dist/voice/agent.d.cts +4 -1
  93. package/dist/voice/agent.d.ts +4 -1
  94. package/dist/voice/agent.d.ts.map +1 -1
  95. package/dist/voice/agent.js +15 -0
  96. package/dist/voice/agent.js.map +1 -1
  97. package/dist/voice/agent_activity.cjs +2 -0
  98. package/dist/voice/agent_activity.cjs.map +1 -1
  99. package/dist/voice/agent_activity.d.ts.map +1 -1
  100. package/dist/voice/agent_activity.js +2 -0
  101. package/dist/voice/agent_activity.js.map +1 -1
  102. package/dist/voice/agent_session.cjs +29 -1
  103. package/dist/voice/agent_session.cjs.map +1 -1
  104. package/dist/voice/agent_session.d.cts +6 -2
  105. package/dist/voice/agent_session.d.ts +6 -2
  106. package/dist/voice/agent_session.d.ts.map +1 -1
  107. package/dist/voice/agent_session.js +30 -2
  108. package/dist/voice/agent_session.js.map +1 -1
  109. package/dist/voice/audio_recognition.cjs.map +1 -1
  110. package/dist/voice/audio_recognition.d.ts.map +1 -1
  111. package/dist/voice/audio_recognition.js.map +1 -1
  112. package/dist/voice/generation.cjs.map +1 -1
  113. package/dist/voice/generation.d.ts.map +1 -1
  114. package/dist/voice/generation.js.map +1 -1
  115. package/dist/voice/index.cjs +2 -0
  116. package/dist/voice/index.cjs.map +1 -1
  117. package/dist/voice/index.d.cts +1 -0
  118. package/dist/voice/index.d.ts +1 -0
  119. package/dist/voice/index.d.ts.map +1 -1
  120. package/dist/voice/index.js +1 -0
  121. package/dist/voice/index.js.map +1 -1
  122. package/dist/voice/report.cjs +69 -0
  123. package/dist/voice/report.cjs.map +1 -0
  124. package/dist/voice/report.d.cts +26 -0
  125. package/dist/voice/report.d.ts +26 -0
  126. package/dist/voice/report.d.ts.map +1 -0
  127. package/dist/voice/report.js +44 -0
  128. package/dist/voice/report.js.map +1 -0
  129. package/package.json +10 -3
  130. package/src/index.ts +2 -1
  131. package/src/inference/tts.ts +1 -1
  132. package/src/ipc/job_proc_lazy_main.ts +10 -2
  133. package/src/job.ts +48 -0
  134. package/src/llm/chat_context.ts +53 -1
  135. package/src/llm/index.ts +1 -0
  136. package/src/llm/llm.ts +2 -0
  137. package/src/llm/provider_format/google.test.ts +72 -1
  138. package/src/llm/provider_format/openai.test.ts +55 -1
  139. package/src/llm/provider_format/openai.ts +3 -2
  140. package/src/log.ts +1 -0
  141. package/src/telemetry/index.ts +10 -0
  142. package/src/telemetry/trace_types.ts +88 -0
  143. package/src/telemetry/traces.ts +266 -0
  144. package/src/telemetry/utils.ts +61 -0
  145. package/src/tts/tts.ts +4 -0
  146. package/src/voice/agent.ts +22 -0
  147. package/src/voice/agent_activity.ts +6 -0
  148. package/src/voice/agent_session.ts +44 -1
  149. package/src/voice/audio_recognition.ts +2 -0
  150. package/src/voice/generation.ts +3 -0
  151. package/src/voice/index.ts +1 -0
  152. package/src/voice/report.ts +77 -0
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ // LiveKit custom attributes
6
+ export const ATTR_SPEECH_ID = 'lk.speech_id';
7
+ export const ATTR_AGENT_LABEL = 'lk.agent_label';
8
+ export const ATTR_START_TIME = 'lk.start_time';
9
+ export const ATTR_END_TIME = 'lk.end_time';
10
+ export const ATTR_RETRY_COUNT = 'lk.retry_count';
11
+
12
+ export const ATTR_PARTICIPANT_ID = 'lk.participant_id';
13
+ export const ATTR_PARTICIPANT_IDENTITY = 'lk.participant_identity';
14
+ export const ATTR_PARTICIPANT_KIND = 'lk.participant_kind';
15
+
16
+ // session start
17
+ export const ATTR_JOB_ID = 'lk.job_id';
18
+ export const ATTR_AGENT_NAME = 'lk.agent_name';
19
+ export const ATTR_ROOM_NAME = 'lk.room_name';
20
+ export const ATTR_SESSION_OPTIONS = 'lk.session_options';
21
+
22
+ // assistant turn
23
+ export const ATTR_USER_INPUT = 'lk.user_input';
24
+ export const ATTR_INSTRUCTIONS = 'lk.instructions';
25
+ export const ATTR_SPEECH_INTERRUPTED = 'lk.interrupted';
26
+
27
+ // llm node
28
+ export const ATTR_CHAT_CTX = 'lk.chat_ctx';
29
+ export const ATTR_FUNCTION_TOOLS = 'lk.function_tools';
30
+ export const ATTR_RESPONSE_TEXT = 'lk.response.text';
31
+ export const ATTR_RESPONSE_FUNCTION_CALLS = 'lk.response.function_calls';
32
+
33
+ // function tool
34
+ export const ATTR_FUNCTION_TOOL_NAME = 'lk.function_tool.name';
35
+ export const ATTR_FUNCTION_TOOL_ARGS = 'lk.function_tool.arguments';
36
+ export const ATTR_FUNCTION_TOOL_IS_ERROR = 'lk.function_tool.is_error';
37
+ export const ATTR_FUNCTION_TOOL_OUTPUT = 'lk.function_tool.output';
38
+
39
+ // tts node
40
+ export const ATTR_TTS_INPUT_TEXT = 'lk.input_text';
41
+ export const ATTR_TTS_STREAMING = 'lk.tts.streaming';
42
+ export const ATTR_TTS_LABEL = 'lk.tts.label';
43
+
44
+ // eou detection
45
+ export const ATTR_EOU_PROBABILITY = 'lk.eou.probability';
46
+ export const ATTR_EOU_UNLIKELY_THRESHOLD = 'lk.eou.unlikely_threshold';
47
+ export const ATTR_EOU_DELAY = 'lk.eou.endpointing_delay';
48
+ export const ATTR_EOU_LANGUAGE = 'lk.eou.language';
49
+ export const ATTR_USER_TRANSCRIPT = 'lk.user_transcript';
50
+ export const ATTR_TRANSCRIPT_CONFIDENCE = 'lk.transcript_confidence';
51
+ export const ATTR_TRANSCRIPTION_DELAY = 'lk.transcription_delay';
52
+ export const ATTR_END_OF_TURN_DELAY = 'lk.end_of_turn_delay';
53
+
54
+ // metrics
55
+ export const ATTR_LLM_METRICS = 'lk.llm_metrics';
56
+ export const ATTR_TTS_METRICS = 'lk.tts_metrics';
57
+ export const ATTR_REALTIME_MODEL_METRICS = 'lk.realtime_model_metrics';
58
+
59
+ // OpenTelemetry GenAI attributes
60
+ // OpenTelemetry specification: https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
61
+ export const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name';
62
+ export const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model';
63
+ export const ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens';
64
+ export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens';
65
+
66
+ // Unofficial OpenTelemetry GenAI attributes, recognized by LangFuse
67
+ // https://langfuse.com/integrations/native/opentelemetry#usage
68
+ // but not yet in the official OpenTelemetry specification.
69
+ export const ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS = 'gen_ai.usage.input_text_tokens';
70
+ export const ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS = 'gen_ai.usage.input_audio_tokens';
71
+ export const ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS = 'gen_ai.usage.input_cached_tokens';
72
+ export const ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS = 'gen_ai.usage.output_text_tokens';
73
+ export const ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS = 'gen_ai.usage.output_audio_tokens';
74
+
75
+ // OpenTelemetry GenAI event names (for structured logging)
76
+ export const EVENT_GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message';
77
+ export const EVENT_GEN_AI_USER_MESSAGE = 'gen_ai.user.message';
78
+ export const EVENT_GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message';
79
+ export const EVENT_GEN_AI_TOOL_MESSAGE = 'gen_ai.tool.message';
80
+ export const EVENT_GEN_AI_CHOICE = 'gen_ai.choice';
81
+
82
+ // Exception attributes
83
+ export const ATTR_EXCEPTION_TRACE = 'exception.stacktrace';
84
+ export const ATTR_EXCEPTION_TYPE = 'exception.type';
85
+ export const ATTR_EXCEPTION_MESSAGE = 'exception.message';
86
+
87
+ // Platform-specific attributes
88
+ export const ATTR_LANGFUSE_COMPLETION_START_TIME = 'langfuse.observation.completion_start_time';
@@ -0,0 +1,266 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import {
5
+ type Attributes,
6
+ type Context,
7
+ type Span,
8
+ type SpanOptions,
9
+ type Tracer,
10
+ type TracerProvider,
11
+ context as otelContext,
12
+ trace,
13
+ } from '@opentelemetry/api';
14
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
15
+ import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
16
+ import { Resource } from '@opentelemetry/resources';
17
+ import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
18
+ import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
19
+ import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
20
+ import { AccessToken } from 'livekit-server-sdk';
21
+
22
+ export interface StartSpanOptions {
23
+ /** Name of the span */
24
+ name: string;
25
+ /** Optional parent context to use for this span */
26
+ context?: Context;
27
+ /** Attributes to set on the span when it starts */
28
+ attributes?: Attributes;
29
+ /** Whether to end the span when the function exits (default: true) */
30
+ endOnExit?: boolean;
31
+ }
32
+
33
+ /**
34
+ * A dynamic tracer that allows the tracer provider to be changed at runtime.
35
+ */
36
+ class DynamicTracer {
37
+ private tracerProvider: TracerProvider;
38
+ private tracer: Tracer;
39
+ private readonly instrumentingModuleName: string;
40
+
41
+ constructor(instrumentingModuleName: string) {
42
+ this.instrumentingModuleName = instrumentingModuleName;
43
+ this.tracerProvider = trace.getTracerProvider();
44
+ this.tracer = trace.getTracer(instrumentingModuleName);
45
+ }
46
+
47
+ /**
48
+ * Set a new tracer provider. This updates the underlying tracer instance.
49
+ * @param provider - The new tracer provider to use
50
+ */
51
+ setProvider(provider: TracerProvider): void {
52
+ this.tracerProvider = provider;
53
+ this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);
54
+ }
55
+
56
+ /**
57
+ * Get the underlying OpenTelemetry tracer.
58
+ * Use this to access the full Tracer API when needed.
59
+ */
60
+ getTracer(): Tracer {
61
+ return this.tracer;
62
+ }
63
+
64
+ /**
65
+ * Start a span manually (without making it active).
66
+ * You must call span.end() when done.
67
+ *
68
+ * @param options - Span configuration including name
69
+ * @returns The created span
70
+ */
71
+ startSpan(options: StartSpanOptions): Span {
72
+ const ctx = options.context || otelContext.active();
73
+ const span = this.tracer.startSpan(
74
+ options.name,
75
+ {
76
+ attributes: options.attributes,
77
+ },
78
+ ctx,
79
+ );
80
+
81
+ return span;
82
+ }
83
+
84
+ /**
85
+ * Start a new span and make it active in the current context.
86
+ * The span will automatically be ended when the provided function completes (unless endOnExit=false).
87
+ *
88
+ * @param fn - The function to execute within the span context
89
+ * @param options - Span configuration including name
90
+ * @returns The result of the provided function
91
+ */
92
+ async startActiveSpan<T>(fn: (span: Span) => Promise<T>, options: StartSpanOptions): Promise<T> {
93
+ const ctx = options.context || otelContext.active();
94
+ const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true
95
+ const opts: SpanOptions = { attributes: options.attributes };
96
+
97
+ return new Promise((resolve, reject) => {
98
+ this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {
99
+ try {
100
+ const result = await fn(span);
101
+ resolve(result);
102
+ } catch (error) {
103
+ reject(error);
104
+ } finally {
105
+ if (endOnExit) {
106
+ span.end();
107
+ }
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Synchronous version of startActiveSpan for non-async operations.
115
+ *
116
+ * @param fn - The function to execute within the span context
117
+ * @param options - Span configuration including name
118
+ * @returns The result of the provided function
119
+ */
120
+ startActiveSpanSync<T>(fn: (span: Span) => T, options: StartSpanOptions): T {
121
+ const ctx = options.context || otelContext.active();
122
+ const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true
123
+ const opts: SpanOptions = { attributes: options.attributes };
124
+
125
+ return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {
126
+ try {
127
+ return fn(span);
128
+ } finally {
129
+ if (endOnExit) {
130
+ span.end();
131
+ }
132
+ }
133
+ });
134
+ }
135
+ }
136
+
137
+ /**
138
+ * The global tracer instance used throughout the agents framework.
139
+ * This tracer can have its provider updated at runtime via setTracerProvider().
140
+ */
141
+ export const tracer = new DynamicTracer('livekit-agents');
142
+
143
+ class MetadataSpanProcessor implements SpanProcessor {
144
+ private metadata: Attributes;
145
+
146
+ constructor(metadata: Attributes) {
147
+ this.metadata = metadata;
148
+ }
149
+
150
+ onStart(span: Span, _parentContext: Context): void {
151
+ span.setAttributes(this.metadata);
152
+ }
153
+
154
+ onEnd(_span: ReadableSpan): void {}
155
+
156
+ shutdown(): Promise<void> {
157
+ return Promise.resolve();
158
+ }
159
+
160
+ forceFlush(): Promise<void> {
161
+ return Promise.resolve();
162
+ }
163
+ }
164
+
165
+ // TODO(brian): PR4 - Add MetadataLogProcessor for structured logging
166
+
167
+ // TODO(brian): PR4 - Add ExtraDetailsProcessor for structured logging
168
+
169
+ /**
170
+ * Set the tracer provider for the livekit-agents framework.
171
+ * This should be called before agent session start if using custom tracer providers.
172
+ *
173
+ * @param provider - The tracer provider to use (must be a NodeTracerProvider)
174
+ * @param options - Optional configuration with metadata property to inject into all spans
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
179
+ * import { setTracerProvider } from '@livekit/agents/telemetry';
180
+ *
181
+ * const provider = new NodeTracerProvider();
182
+ * setTracerProvider(provider, {
183
+ * metadata: { room_id: 'room123', job_id: 'job456' }
184
+ * });
185
+ * ```
186
+ */
187
+ export function setTracerProvider(
188
+ provider: NodeTracerProvider,
189
+ options?: { metadata?: Attributes },
190
+ ): void {
191
+ if (options?.metadata) {
192
+ provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));
193
+ }
194
+
195
+ tracer.setProvider(provider);
196
+ }
197
+
198
+ /**
199
+ * Setup OpenTelemetry tracer for LiveKit Cloud observability.
200
+ * This configures OTLP exporters to send traces to LiveKit Cloud.
201
+ *
202
+ * @param options - Configuration for cloud tracer with roomId, jobId, and cloudHostname properties
203
+ *
204
+ * @internal
205
+ */
206
+ export async function setupCloudTracer(options: {
207
+ roomId: string;
208
+ jobId: string;
209
+ cloudHostname: string;
210
+ }): Promise<void> {
211
+ const { roomId, jobId, cloudHostname } = options;
212
+
213
+ const apiKey = process.env.LIVEKIT_API_KEY;
214
+ const apiSecret = process.env.LIVEKIT_API_SECRET;
215
+
216
+ if (!apiKey || !apiSecret) {
217
+ throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing');
218
+ }
219
+
220
+ const token = new AccessToken(apiKey, apiSecret, {
221
+ identity: 'livekit-agents-telemetry',
222
+ ttl: '6h',
223
+ });
224
+ token.addObservabilityGrant({ write: true });
225
+
226
+ try {
227
+ const jwt = await token.toJwt();
228
+
229
+ const headers = {
230
+ Authorization: `Bearer ${jwt}`,
231
+ };
232
+
233
+ const metadata: Attributes = {
234
+ room_id: roomId,
235
+ job_id: jobId,
236
+ };
237
+
238
+ const resource = new Resource({
239
+ [ATTR_SERVICE_NAME]: 'livekit-agents',
240
+ room_id: roomId,
241
+ job_id: jobId,
242
+ });
243
+
244
+ // Configure OTLP exporter to send traces to LiveKit Cloud
245
+ const spanExporter = new OTLPTraceExporter({
246
+ url: `https://${cloudHostname}/observability/traces/otlp/v0`,
247
+ headers,
248
+ compression: CompressionAlgorithm.GZIP,
249
+ });
250
+
251
+ const tracerProvider = new NodeTracerProvider({
252
+ resource,
253
+ spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)],
254
+ });
255
+ tracerProvider.register();
256
+
257
+ // Metadata processor is already configured in the constructor above
258
+ setTracerProvider(tracerProvider);
259
+
260
+ // TODO(brian): PR4 - Add logger provider setup here for structured logging
261
+ // Similar to Python's setup: LoggerProvider, OTLPLogExporter, BatchLogRecordProcessor
262
+ } catch (error) {
263
+ console.error('Failed to setup cloud tracer:', error);
264
+ throw error;
265
+ }
266
+ }
@@ -0,0 +1,61 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { type Span, SpanStatusCode, context as otelContext, trace } from '@opentelemetry/api';
5
+ import type { RealtimeModelMetrics } from '../metrics/base.js';
6
+ import * as traceTypes from './trace_types.js';
7
+ import { tracer } from './traces.js';
8
+
9
+ export function recordException(span: Span, error: Error): void {
10
+ span.recordException(error);
11
+ span.setStatus({
12
+ code: SpanStatusCode.ERROR,
13
+ message: error.message,
14
+ });
15
+
16
+ // Set exception attributes for better visibility
17
+ // (in case the exception event is not rendered by the backend)
18
+ span.setAttributes({
19
+ [traceTypes.ATTR_EXCEPTION_TYPE]: error.constructor.name,
20
+ [traceTypes.ATTR_EXCEPTION_MESSAGE]: error.message,
21
+ [traceTypes.ATTR_EXCEPTION_TRACE]: error.stack || '',
22
+ });
23
+ }
24
+
25
+ export function recordRealtimeMetrics(span: Span, metrics: RealtimeModelMetrics): void {
26
+ const attrs: Record<string, string | number> = {
27
+ [traceTypes.ATTR_GEN_AI_REQUEST_MODEL]: metrics.label || 'unknown',
28
+ [traceTypes.ATTR_REALTIME_MODEL_METRICS]: JSON.stringify(metrics),
29
+ [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TOKENS]: metrics.inputTokens,
30
+ [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: metrics.outputTokens,
31
+ [traceTypes.ATTR_GEN_AI_USAGE_INPUT_TEXT_TOKENS]: metrics.inputTokenDetails.textTokens,
32
+ [traceTypes.ATTR_GEN_AI_USAGE_INPUT_AUDIO_TOKENS]: metrics.inputTokenDetails.audioTokens,
33
+ [traceTypes.ATTR_GEN_AI_USAGE_INPUT_CACHED_TOKENS]: metrics.inputTokenDetails.cachedTokens,
34
+ [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_TEXT_TOKENS]: metrics.outputTokenDetails.textTokens,
35
+ [traceTypes.ATTR_GEN_AI_USAGE_OUTPUT_AUDIO_TOKENS]: metrics.outputTokenDetails.audioTokens,
36
+ };
37
+
38
+ // Add LangFuse-specific completion start time if TTFT is available
39
+ if (metrics.ttftMs !== undefined && metrics.ttftMs !== -1) {
40
+ const completionStartTime = metrics.timestamp + metrics.ttftMs;
41
+ // Convert to UTC ISO string for LangFuse compatibility
42
+ const completionStartTimeUtc = new Date(completionStartTime).toISOString();
43
+ attrs[traceTypes.ATTR_LANGFUSE_COMPLETION_START_TIME] = completionStartTimeUtc;
44
+ }
45
+
46
+ if (span.isRecording()) {
47
+ span.setAttributes(attrs);
48
+ } else {
49
+ const currentContext = otelContext.active();
50
+ const spanContext = trace.setSpan(currentContext, span);
51
+
52
+ // Create a dedicated child span for orphaned metrics
53
+ tracer.getTracer().startActiveSpan('realtime_metrics', {}, spanContext, (child) => {
54
+ try {
55
+ child.setAttributes(attrs);
56
+ } finally {
57
+ child.end();
58
+ }
59
+ });
60
+ }
61
+ }
package/src/tts/tts.ts CHANGED
@@ -157,8 +157,10 @@ export abstract class SynthesizeStream
157
157
  }
158
158
 
159
159
  private async mainTask() {
160
+ // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('tts_request', ..., { endOnExit: false })
160
161
  for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
161
162
  try {
163
+ // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('tts_request_run', ...)
162
164
  return await this.run();
163
165
  } catch (error) {
164
166
  if (error instanceof APIError) {
@@ -385,8 +387,10 @@ export abstract class ChunkedStream implements AsyncIterableIterator<Synthesized
385
387
  }
386
388
 
387
389
  private async mainTask() {
390
+ // TODO(brian): PR3 - Add span wrapping: tracer.startActiveSpan('tts_request', ..., { endOnExit: false })
388
391
  for (let i = 0; i < this._connOptions.maxRetry + 1; i++) {
389
392
  try {
393
+ // TODO(brian): PR3 - Add span for retry attempts: tracer.startActiveSpan('tts_request_run', ...)
390
394
  return await this.run();
391
395
  } catch (error) {
392
396
  if (error instanceof APIError) {
@@ -59,6 +59,7 @@ export interface ModelSettings {
59
59
  }
60
60
 
61
61
  export interface AgentOptions<UserData> {
62
+ id?: string;
62
63
  instructions: string;
63
64
  chatCtx?: ChatContext;
64
65
  tools?: ToolContext<UserData>;
@@ -72,6 +73,7 @@ export interface AgentOptions<UserData> {
72
73
  }
73
74
 
74
75
  export class Agent<UserData = any> {
76
+ private _id: string;
75
77
  private turnDetection?: TurnDetectionMode;
76
78
  private _stt?: STT;
77
79
  private _vad?: VAD;
@@ -91,6 +93,7 @@ export class Agent<UserData = any> {
91
93
  _tools?: ToolContext<UserData>;
92
94
 
93
95
  constructor({
96
+ id,
94
97
  instructions,
95
98
  chatCtx,
96
99
  tools,
@@ -100,6 +103,21 @@ export class Agent<UserData = any> {
100
103
  llm,
101
104
  tts,
102
105
  }: AgentOptions<UserData>) {
106
+ if (id) {
107
+ this._id = id;
108
+ } else {
109
+ // Convert class name to snake_case
110
+ const className = this.constructor.name;
111
+ if (className === 'Agent') {
112
+ this._id = 'default_agent';
113
+ } else {
114
+ this._id = className
115
+ .replace(/([A-Z])/g, '_$1')
116
+ .toLowerCase()
117
+ .replace(/^_/, '');
118
+ }
119
+ }
120
+
103
121
  this._instructions = instructions;
104
122
  this._tools = { ...tools };
105
123
  this._chatCtx = chatCtx
@@ -152,6 +170,10 @@ export class Agent<UserData = any> {
152
170
  return new ReadonlyChatContext(this._chatCtx.items);
153
171
  }
154
172
 
173
+ get id(): string {
174
+ return this._id;
175
+ }
176
+
155
177
  get instructions(): string {
156
178
  return this._instructions;
157
179
  }
@@ -202,6 +202,8 @@ export class AgentActivity implements RecognitionHooks {
202
202
  }
203
203
 
204
204
  async start(): Promise<void> {
205
+ // TODO(brian): PR3 - Add span: startSpan = tracer.startSpan('start_agent_activity', { attributes: { 'lk.agent_label': this.agent.label } })
206
+ // TODO(brian): PR3 - Wrap prewarm calls with trace.useSpan(startSpan, endOnExit: false)
205
207
  const unlock = await this.lock.lock();
206
208
  try {
207
209
  this.agent._agentActivity = this;
@@ -289,6 +291,7 @@ export class AgentActivity implements RecognitionHooks {
289
291
  this.started = true;
290
292
 
291
293
  this._mainTask = Task.from(({ signal }) => this.mainTask(signal));
294
+ // TODO(brian): PR3 - Wrap onEnter with tracer.startActiveSpan('on_enter', { attributes: { 'lk.agent_label': this.agent.label }, context: startSpan context })
292
295
  this.createSpeechTask({
293
296
  task: Task.from(() => this.agent.onEnter()),
294
297
  name: 'AgentActivity_onEnter',
@@ -1251,6 +1254,7 @@ export class AgentActivity implements RecognitionHooks {
1251
1254
  }
1252
1255
  }
1253
1256
 
1257
+ // TODO(brian): PR3 - Wrap entire pipelineReplyTask() method with tracer.startActiveSpan('agent_turn')
1254
1258
  private async pipelineReplyTask(
1255
1259
  speechHandle: SpeechHandle,
1256
1260
  chatCtx: ChatContext,
@@ -2092,12 +2096,14 @@ export class AgentActivity implements RecognitionHooks {
2092
2096
  this.wakeupMainTask();
2093
2097
  }
2094
2098
 
2099
+ // TODO(brian): PR3 - Wrap entire drain() method with tracer.startActiveSpan('drain_agent_activity', { attributes: { 'lk.agent_label': this.agent.label } })
2095
2100
  async drain(): Promise<void> {
2096
2101
  const unlock = await this.lock.lock();
2097
2102
  try {
2098
2103
  if (this._draining) return;
2099
2104
 
2100
2105
  this.cancelPreemptiveGeneration();
2106
+ // TODO(brian): PR3 - Wrap onExit with tracer.startActiveSpan('on_exit', { attributes: { 'lk.agent_label': this.agent.label } })
2101
2107
  this.createSpeechTask({
2102
2108
  task: Task.from(() => this.agent.onExit()),
2103
2109
  name: 'AgentActivity_onExit',
@@ -14,7 +14,7 @@ import {
14
14
  type TTSModelString,
15
15
  } from '../inference/index.js';
16
16
  import { getJobContext } from '../job.js';
17
- import { ChatContext, ChatMessage } from '../llm/chat_context.js';
17
+ import { AgentHandoffItem, ChatContext, ChatMessage } from '../llm/chat_context.js';
18
18
  import type { LLM, RealtimeModel, RealtimeModelError, ToolChoice } from '../llm/index.js';
19
19
  import type { LLMError } from '../llm/llm.js';
20
20
  import { log } from '../log.js';
@@ -26,6 +26,7 @@ import type { Agent } from './agent.js';
26
26
  import { AgentActivity } from './agent_activity.js';
27
27
  import type { _TurnDetector } from './audio_recognition.js';
28
28
  import {
29
+ type AgentEvent,
29
30
  AgentSessionEventTypes,
30
31
  type AgentState,
31
32
  type AgentStateChangedEvent,
@@ -127,6 +128,9 @@ export class AgentSession<
127
128
  private closingTask: Promise<void> | null = null;
128
129
  private userAwayTimer: NodeJS.Timeout | null = null;
129
130
 
131
+ /** @internal */
132
+ _recordedEvents: AgentEvent[] = [];
133
+
130
134
  constructor(opts: AgentSessionOptions<UserData>) {
131
135
  super();
132
136
 
@@ -174,6 +178,15 @@ export class AgentSession<
174
178
  this.on(AgentSessionEventTypes.UserInputTranscribed, this._onUserInputTranscribed.bind(this));
175
179
  }
176
180
 
181
+ emit<K extends keyof AgentSessionCallbacks>(
182
+ event: K,
183
+ ...args: Parameters<AgentSessionCallbacks[K]>
184
+ ): boolean {
185
+ const eventData = args[0] as AgentEvent;
186
+ this._recordedEvents.push(eventData);
187
+ return super.emit(event, ...args);
188
+ }
189
+
177
190
  get input(): AgentInput {
178
191
  return this._input;
179
192
  }
@@ -199,15 +212,20 @@ export class AgentSession<
199
212
  }
200
213
 
201
214
  async start({
215
+ // TODO(brian): PR2 - Add setupCloudTracer() call if on LiveKit Cloud with recording enabled
216
+ // TODO(brian): PR3 - Add span: this._sessionSpan = tracer.startSpan('agent_session'), store as instance property
217
+ // TODO(brian): PR4 - Add setupCloudLogger() call in setupCloudTracer() to setup OTEL logging with Pino bridge
202
218
  agent,
203
219
  room,
204
220
  inputOptions,
205
221
  outputOptions,
222
+ record = true,
206
223
  }: {
207
224
  agent: Agent;
208
225
  room: Room;
209
226
  inputOptions?: Partial<RoomInputOptions>;
210
227
  outputOptions?: Partial<RoomOutputOptions>;
228
+ record?: boolean;
211
229
  }): Promise<void> {
212
230
  if (this.started) {
213
231
  return;
@@ -247,6 +265,17 @@ export class AgentSession<
247
265
  this.logger.debug('Auto-connecting to room via job context');
248
266
  tasks.push(ctx.connect());
249
267
  }
268
+
269
+ if (record) {
270
+ if (ctx._primaryAgentSession === undefined) {
271
+ ctx._primaryAgentSession = this;
272
+ } else {
273
+ throw new Error(
274
+ 'Only one `AgentSession` can be the primary at a time. If you want to ignore primary designation, use session.start(record=False).',
275
+ );
276
+ }
277
+ }
278
+
250
279
  // TODO(AJS-265): add shutdown callback to job context
251
280
  tasks.push(this.updateActivity(this.agent));
252
281
 
@@ -341,6 +370,8 @@ export class AgentSession<
341
370
  // TODO(AJS-129): add lock to agent activity core lifecycle
342
371
  this.nextActivity = new AgentActivity(agent, this);
343
372
 
373
+ const previousActivity = this.activity;
374
+
344
375
  if (this.activity) {
345
376
  await this.activity.drain();
346
377
  await this.activity.close();
@@ -349,6 +380,14 @@ export class AgentSession<
349
380
  this.activity = this.nextActivity;
350
381
  this.nextActivity = undefined;
351
382
 
383
+ this._chatCtx.insert(
384
+ new AgentHandoffItem({
385
+ oldAgentId: previousActivity?.agent.id,
386
+ newAgentId: agent.id,
387
+ }),
388
+ );
389
+ this.logger.debug({ previousActivity, agent }, 'Agent handoff inserted into chat context');
390
+
352
391
  await this.activity.start();
353
392
 
354
393
  if (this._input.audio) {
@@ -419,6 +458,8 @@ export class AgentSession<
419
458
  return;
420
459
  }
421
460
 
461
+ // TODO(brian): PR3 - Add span: if state === 'speaking' && !this._agentSpeakingSpan, create tracer.startSpan('agent_speaking') with participant attributes
462
+ // TODO(brian): PR3 - Add span: if state !== 'speaking' && this._agentSpeakingSpan, end and clear this._agentSpeakingSpan
422
463
  const oldState = this._agentState;
423
464
  this._agentState = state;
424
465
 
@@ -441,6 +482,8 @@ export class AgentSession<
441
482
  return;
442
483
  }
443
484
 
485
+ // TODO(brian): PR3 - Add span: if state === 'speaking' && !this._userSpeakingSpan, create tracer.startSpan('user_speaking') with participant attributes
486
+ // TODO(brian): PR3 - Add span: if state !== 'speaking' && this._userSpeakingSpan, end and clear this._userSpeakingSpan
444
487
  const oldState = this.userState;
445
488
  this.userState = state;
446
489
 
@@ -57,6 +57,8 @@ export interface AudioRecognitionOptions {
57
57
  maxEndpointingDelay: number;
58
58
  }
59
59
 
60
+ // TODO(brian): PR3 - Add span: private _userTurnSpan?: Span, create lazily in _ensureUserTurnSpan() method (tracer.startSpan('user_turn') with participant attributes)
61
+ // TODO(brian): PR3 - Add span: 'eou_detection' span when running EOU detection (in runEOUDetection method)
60
62
  export class AudioRecognition {
61
63
  private hooks: RecognitionHooks;
62
64
  private stt?: STTNode;
@@ -377,6 +377,7 @@ export function updateInstructions(options: {
377
377
  }
378
378
  }
379
379
 
380
+ // TODO(brian): PR3 - Add @tracer.startActiveSpan('llm_node') decorator/wrapper
380
381
  export function performLLMInference(
381
382
  node: LLMNode,
382
383
  chatCtx: ChatContext,
@@ -467,6 +468,7 @@ export function performLLMInference(
467
468
  ];
468
469
  }
469
470
 
471
+ // TODO(brian): PR3 - Add @tracer.startActiveSpan('tts_node') decorator/wrapper
470
472
  export function performTTSInference(
471
473
  node: TTSNode,
472
474
  text: ReadableStream<string>,
@@ -650,6 +652,7 @@ export function performAudioForwarding(
650
652
  ];
651
653
  }
652
654
 
655
+ // TODO(brian): PR3 - Add @tracer.startActiveSpan('function_tool') wrapper for each tool execution
653
656
  export function performToolExecutions({
654
657
  session,
655
658
  speechHandle,