@livekit/agents 1.0.31 → 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.
Files changed (98) hide show
  1. package/dist/ipc/inference_proc_executor.cjs +6 -3
  2. package/dist/ipc/inference_proc_executor.cjs.map +1 -1
  3. package/dist/ipc/inference_proc_executor.d.ts.map +1 -1
  4. package/dist/ipc/inference_proc_executor.js +6 -3
  5. package/dist/ipc/inference_proc_executor.js.map +1 -1
  6. package/dist/ipc/job_proc_executor.cjs +6 -1
  7. package/dist/ipc/job_proc_executor.cjs.map +1 -1
  8. package/dist/ipc/job_proc_executor.d.ts.map +1 -1
  9. package/dist/ipc/job_proc_executor.js +6 -1
  10. package/dist/ipc/job_proc_executor.js.map +1 -1
  11. package/dist/ipc/job_proc_lazy_main.cjs +1 -1
  12. package/dist/ipc/job_proc_lazy_main.cjs.map +1 -1
  13. package/dist/ipc/job_proc_lazy_main.js +1 -1
  14. package/dist/ipc/job_proc_lazy_main.js.map +1 -1
  15. package/dist/ipc/supervised_proc.cjs +29 -7
  16. package/dist/ipc/supervised_proc.cjs.map +1 -1
  17. package/dist/ipc/supervised_proc.d.ts.map +1 -1
  18. package/dist/ipc/supervised_proc.js +29 -7
  19. package/dist/ipc/supervised_proc.js.map +1 -1
  20. package/dist/ipc/supervised_proc.test.cjs +145 -0
  21. package/dist/ipc/supervised_proc.test.cjs.map +1 -0
  22. package/dist/ipc/supervised_proc.test.js +122 -0
  23. package/dist/ipc/supervised_proc.test.js.map +1 -0
  24. package/dist/job.cjs +5 -1
  25. package/dist/job.cjs.map +1 -1
  26. package/dist/job.d.ts.map +1 -1
  27. package/dist/job.js +5 -1
  28. package/dist/job.js.map +1 -1
  29. package/dist/llm/chat_context.cjs +19 -2
  30. package/dist/llm/chat_context.cjs.map +1 -1
  31. package/dist/llm/chat_context.d.cts +8 -0
  32. package/dist/llm/chat_context.d.ts +8 -0
  33. package/dist/llm/chat_context.d.ts.map +1 -1
  34. package/dist/llm/chat_context.js +19 -2
  35. package/dist/llm/chat_context.js.map +1 -1
  36. package/dist/llm/provider_format/google.cjs +6 -2
  37. package/dist/llm/provider_format/google.cjs.map +1 -1
  38. package/dist/llm/provider_format/google.d.ts.map +1 -1
  39. package/dist/llm/provider_format/google.js +6 -2
  40. package/dist/llm/provider_format/google.js.map +1 -1
  41. package/dist/llm/realtime.cjs.map +1 -1
  42. package/dist/llm/realtime.d.cts +4 -0
  43. package/dist/llm/realtime.d.ts +4 -0
  44. package/dist/llm/realtime.d.ts.map +1 -1
  45. package/dist/llm/realtime.js.map +1 -1
  46. package/dist/log.cjs +3 -3
  47. package/dist/log.cjs.map +1 -1
  48. package/dist/log.d.cts +5 -0
  49. package/dist/log.d.ts +5 -0
  50. package/dist/log.d.ts.map +1 -1
  51. package/dist/log.js +3 -3
  52. package/dist/log.js.map +1 -1
  53. package/dist/stream/stream_channel.cjs +8 -1
  54. package/dist/stream/stream_channel.cjs.map +1 -1
  55. package/dist/stream/stream_channel.d.cts +1 -0
  56. package/dist/stream/stream_channel.d.ts +1 -0
  57. package/dist/stream/stream_channel.d.ts.map +1 -1
  58. package/dist/stream/stream_channel.js +8 -1
  59. package/dist/stream/stream_channel.js.map +1 -1
  60. package/dist/telemetry/otel_http_exporter.cjs +13 -10
  61. package/dist/telemetry/otel_http_exporter.cjs.map +1 -1
  62. package/dist/telemetry/otel_http_exporter.d.ts.map +1 -1
  63. package/dist/telemetry/otel_http_exporter.js +13 -10
  64. package/dist/telemetry/otel_http_exporter.js.map +1 -1
  65. package/dist/telemetry/traces.cjs +22 -4
  66. package/dist/telemetry/traces.cjs.map +1 -1
  67. package/dist/telemetry/traces.d.ts.map +1 -1
  68. package/dist/telemetry/traces.js +22 -4
  69. package/dist/telemetry/traces.js.map +1 -1
  70. package/dist/voice/agent_activity.cjs +25 -5
  71. package/dist/voice/agent_activity.cjs.map +1 -1
  72. package/dist/voice/agent_activity.d.cts +1 -0
  73. package/dist/voice/agent_activity.d.ts +1 -0
  74. package/dist/voice/agent_activity.d.ts.map +1 -1
  75. package/dist/voice/agent_activity.js +26 -6
  76. package/dist/voice/agent_activity.js.map +1 -1
  77. package/dist/voice/generation.cjs +3 -1
  78. package/dist/voice/generation.cjs.map +1 -1
  79. package/dist/voice/generation.d.ts.map +1 -1
  80. package/dist/voice/generation.js +3 -1
  81. package/dist/voice/generation.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/ipc/inference_proc_executor.ts +11 -3
  84. package/src/ipc/job_proc_executor.ts +11 -1
  85. package/src/ipc/job_proc_lazy_main.ts +1 -1
  86. package/src/ipc/supervised_proc.test.ts +153 -0
  87. package/src/ipc/supervised_proc.ts +27 -9
  88. package/src/job.ts +4 -1
  89. package/src/llm/chat_context.ts +28 -2
  90. package/src/llm/provider_format/google.ts +6 -2
  91. package/src/llm/realtime.ts +5 -0
  92. package/src/log.ts +9 -3
  93. package/src/stream/stream_channel.ts +9 -1
  94. package/src/telemetry/otel_http_exporter.ts +14 -10
  95. package/src/telemetry/traces.ts +28 -4
  96. package/src/voice/agent_activity.ts +27 -2
  97. package/src/voice/generation.ts +2 -0
  98. package/src/llm/__snapshots__/utils.test.ts.snap +0 -65
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/telemetry/traces.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { MetricsRecordingHeader } from '@livekit/protocol';\nimport {\n type Attributes,\n type Context,\n type Span,\n type SpanOptions,\n type Tracer,\n type TracerProvider,\n context as otelContext,\n trace,\n} from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';\nimport { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';\nimport { Resource } from '@opentelemetry/resources';\nimport type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport FormData from 'form-data';\nimport { AccessToken } from 'livekit-server-sdk';\nimport fs from 'node:fs/promises';\nimport type { ChatContent, ChatItem } from '../llm/index.js';\nimport { enableOtelLogging } from '../log.js';\nimport type { SessionReport } from '../voice/report.js';\nimport { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';\nimport { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';\n\nexport interface StartSpanOptions {\n /** Name of the span */\n name: string;\n /** Optional parent context to use for this span */\n context?: Context;\n /** Attributes to set on the span when it starts */\n attributes?: Attributes;\n /** Whether to end the span when the function exits (default: true) */\n endOnExit?: boolean;\n}\n\n/**\n * A dynamic tracer that allows the tracer provider to be changed at runtime.\n */\nclass DynamicTracer {\n private tracerProvider: TracerProvider;\n private tracer: Tracer;\n private readonly instrumentingModuleName: string;\n\n constructor(instrumentingModuleName: string) {\n this.instrumentingModuleName = instrumentingModuleName;\n this.tracerProvider = trace.getTracerProvider();\n this.tracer = trace.getTracer(instrumentingModuleName);\n }\n\n /**\n * Set a new tracer provider. This updates the underlying tracer instance.\n * @param provider - The new tracer provider to use\n */\n setProvider(provider: TracerProvider): void {\n this.tracerProvider = provider;\n this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);\n }\n\n /**\n * Get the underlying OpenTelemetry tracer.\n * Use this to access the full Tracer API when needed.\n */\n getTracer(): Tracer {\n return this.tracer;\n }\n\n /**\n * Start a span manually (without making it active).\n * You must call span.end() when done.\n *\n * @param options - Span configuration including name\n * @returns The created span\n */\n startSpan(options: StartSpanOptions): Span {\n const ctx = options.context || otelContext.active();\n const span = this.tracer.startSpan(\n options.name,\n {\n attributes: options.attributes,\n },\n ctx,\n );\n\n return span;\n }\n\n /**\n * Start a new span and make it active in the current context.\n * The span will automatically be ended when the provided function completes (unless endOnExit=false).\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n async startActiveSpan<T>(fn: (span: Span) => Promise<T>, options: StartSpanOptions): Promise<T> {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n // Directly return the tracer's startActiveSpan result - it handles async correctly\n return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {\n try {\n return await fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n\n /**\n * Synchronous version of startActiveSpan for non-async operations.\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n startActiveSpanSync<T>(fn: (span: Span) => T, options: StartSpanOptions): T {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {\n try {\n return fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n}\n\n/**\n * The global tracer instance used throughout the agents framework.\n * This tracer can have its provider updated at runtime via setTracerProvider().\n */\nexport const tracer = new DynamicTracer('livekit-agents');\n\nclass MetadataSpanProcessor implements SpanProcessor {\n private metadata: Attributes;\n\n constructor(metadata: Attributes) {\n this.metadata = metadata;\n }\n\n onStart(span: Span, _parentContext: Context): void {\n span.setAttributes(this.metadata);\n }\n\n onEnd(_span: ReadableSpan): void {}\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Set the tracer provider for the livekit-agents framework.\n * This should be called before agent session start if using custom tracer providers.\n *\n * @param provider - The tracer provider to use (must be a NodeTracerProvider)\n * @param options - Optional configuration with metadata property to inject into all spans\n *\n * @example\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\n * import { setTracerProvider } from '@livekit/agents/telemetry';\n *\n * const provider = new NodeTracerProvider();\n * setTracerProvider(provider, {\n * metadata: { room_id: 'room123', job_id: 'job456' }\n * });\n * ```\n */\nexport function setTracerProvider(\n provider: NodeTracerProvider,\n options?: { metadata?: Attributes },\n): void {\n if (options?.metadata) {\n provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));\n }\n\n tracer.setProvider(provider);\n}\n\n/**\n * Setup OpenTelemetry tracer for LiveKit Cloud observability.\n * This configures OTLP exporters to send traces to LiveKit Cloud.\n *\n * @param options - Configuration for cloud tracer with roomId, jobId, and cloudHostname properties\n *\n * @internal\n */\nexport async function setupCloudTracer(options: {\n roomId: string;\n jobId: string;\n cloudHostname: string;\n}): Promise<void> {\n const { roomId, jobId, cloudHostname } = options;\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing');\n }\n\n const token = new AccessToken(apiKey, apiSecret, {\n identity: 'livekit-agents-telemetry',\n ttl: '6h',\n });\n token.addObservabilityGrant({ write: true });\n\n try {\n const jwt = await token.toJwt();\n\n const headers = {\n Authorization: `Bearer ${jwt}`,\n };\n\n const metadata: Attributes = {\n room_id: roomId,\n job_id: jobId,\n };\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: 'livekit-agents',\n room_id: roomId,\n job_id: jobId,\n });\n\n // Configure OTLP exporter to send traces to LiveKit Cloud\n const spanExporter = new OTLPTraceExporter({\n url: `https://${cloudHostname}/observability/traces/otlp/v0`,\n headers,\n compression: CompressionAlgorithm.GZIP,\n });\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)],\n });\n tracerProvider.register();\n\n setTracerProvider(tracerProvider);\n\n // Initialize standalone Pino cloud exporter (no OTEL SDK dependency)\n initPinoCloudExporter({\n cloudHostname,\n roomId,\n jobId,\n });\n\n enableOtelLogging();\n } catch (error) {\n console.error('Failed to setup cloud tracer:', error);\n throw error;\n }\n}\n\n/**\n * Flush all pending Pino logs to ensure they are exported.\n * Call this before session/job ends to ensure all logs are sent.\n *\n * @internal\n */\nexport async function flushOtelLogs(): Promise<void> {\n await flushPinoLogs();\n}\n\n/**\n * Convert ChatItem to proto-compatible dictionary format.\n * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published\n */\nfunction chatItemToProto(item: ChatItem): Record<string, any> {\n const itemDict: Record<string, any> = {};\n\n if (item.type === 'message') {\n const roleMap: Record<string, string> = {\n developer: 'DEVELOPER',\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n const msg: Record<string, any> = {\n id: item.id,\n role: roleMap[item.role] || item.role.toUpperCase(),\n content: item.content.map((c: ChatContent) => ({ text: c })),\n createdAt: toRFC3339(item.createdAt),\n };\n\n if (item.interrupted) {\n msg.interrupted = item.interrupted;\n }\n\n // TODO(brian): Add extra and transcriptConfidence to ChatMessage\n // if (item.extra && Object.keys(item.extra).length > 0) {\n // msg.extra = item.extra;\n // }\n\n // if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {\n // msg.transcriptConfidence = item.transcriptConfidence;\n // }\n\n // TODO(brian): Add metrics to ChatMessage\n // const metrics = item.metrics || {};\n // if (Object.keys(metrics).length > 0) {\n // msg.metrics = {};\n // if (metrics.started_speaking_at) {\n // msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);\n // }\n // if (metrics.stopped_speaking_at) {\n // msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);\n // }\n // if (metrics.transcription_delay !== undefined) {\n // msg.metrics.transcriptionDelay = metrics.transcription_delay;\n // }\n // if (metrics.end_of_turn_delay !== undefined) {\n // msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;\n // }\n // if (metrics.on_user_turn_completed_delay !== undefined) {\n // msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;\n // }\n // if (metrics.llm_node_ttft !== undefined) {\n // msg.metrics.llmNodeTtft = metrics.llm_node_ttft;\n // }\n // if (metrics.tts_node_ttfb !== undefined) {\n // msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;\n // }\n // if (metrics.e2e_latency !== undefined) {\n // msg.metrics.e2eLatency = metrics.e2e_latency;\n // }\n // }\n\n itemDict.message = msg;\n } else if (item.type === 'function_call') {\n itemDict.functionCall = {\n id: item.id,\n callId: item.callId,\n arguments: item.args,\n name: item.name,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'function_call_output') {\n itemDict.functionCallOutput = {\n id: item.id,\n name: item.name,\n callId: item.callId,\n output: item.output,\n isError: item.isError,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'agent_handoff') {\n const handoff: Record<string, any> = {\n id: item.id,\n newAgentId: item.newAgentId,\n createdAt: toRFC3339(item.createdAt),\n };\n if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {\n handoff.oldAgentId = item.oldAgentId;\n }\n itemDict.agentHandoff = handoff;\n }\n\n try {\n if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {\n itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);\n } else if (\n item.type === 'function_call_output' &&\n typeof itemDict.functionCallOutput?.output === 'string'\n ) {\n itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);\n }\n } catch {\n // ignore parsing errors\n }\n\n return itemDict;\n}\n\n/**\n * Convert timestamp to RFC3339 format matching Python's _to_rfc3339.\n * Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.\n * @internal\n */\nfunction toRFC3339(valueMs: number | Date): string {\n // valueMs is already in milliseconds (from Date.now())\n const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);\n // Truncate sub-millisecond precision\n const truncated = new Date(Math.floor(dt.getTime()));\n return truncated.toISOString();\n}\n\n/**\n * Upload session report to LiveKit Cloud observability.\n * @param options - Configuration with agentName, cloudHostname, and report\n */\nexport async function uploadSessionReport(options: {\n agentName: string;\n cloudHostname: string;\n report: SessionReport;\n}): Promise<void> {\n const { agentName, cloudHostname, report } = options;\n\n // Create OTLP HTTP exporter for chat history logs\n // Uses raw HTTP JSON format which is required by LiveKit Cloud\n const logExporter = new SimpleOTLPHttpLogExporter({\n cloudHostname,\n resourceAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n },\n scopeName: 'chat_history',\n scopeAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n room: report.room,\n },\n });\n\n // Build log records for session report and chat items\n const logRecords: SimpleLogRecord[] = [];\n\n const commonAttrs = {\n room_id: report.roomId,\n job_id: report.jobId,\n 'logger.name': 'chat_history',\n };\n\n logRecords.push({\n body: 'session report',\n timestampMs: report.startedAt || report.timestamp || 0,\n attributes: {\n ...commonAttrs,\n 'session.options': report.options || {},\n 'session.report_timestamp': report.timestamp,\n agent_name: agentName,\n },\n });\n\n // Track last timestamp to ensure monotonic ordering when items have identical timestamps\n // This fixes the issue where function_call and function_call_output with same timestamp\n // get reordered by the dashboard\n let lastTimestamp = 0;\n for (const item of report.chatHistory.items) {\n // Ensure monotonically increasing timestamps for proper ordering\n // Add 0.001ms (1 microsecond) offset when timestamps collide\n let itemTimestamp = item.createdAt;\n if (itemTimestamp <= lastTimestamp) {\n itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond\n }\n lastTimestamp = itemTimestamp;\n\n const itemProto = chatItemToProto(item);\n let severityNumber = SeverityNumber.UNSPECIFIED;\n let severityText = 'unspecified';\n\n if (item.type === 'function_call_output' && item.isError) {\n severityNumber = SeverityNumber.ERROR;\n severityText = 'error';\n }\n\n logRecords.push({\n body: 'chat item',\n timestampMs: itemTimestamp, // Adjusted for monotonic ordering\n attributes: { 'chat.item': itemProto, ...commonAttrs },\n severityNumber,\n severityText,\n });\n }\n await logExporter.export(logRecords);\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');\n }\n\n const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });\n token.addObservabilityGrant({ write: true });\n const jwt = await token.toJwt();\n\n const formData = new FormData();\n\n // Add header (protobuf MetricsRecordingHeader)\n const audioStartTime = report.audioRecordingStartedAt ?? 0;\n const headerMsg = new MetricsRecordingHeader({\n roomId: report.roomId,\n duration: BigInt(0), // TODO: Calculate actual duration from report\n startTime: {\n seconds: BigInt(Math.floor(audioStartTime / 1000)),\n nanos: Math.floor((audioStartTime % 1000) * 1e6),\n },\n });\n\n const headerBytes = Buffer.from(headerMsg.toBinary());\n formData.append('header', headerBytes, {\n filename: 'header.binpb',\n contentType: 'application/protobuf',\n knownLength: headerBytes.length,\n header: {\n 'Content-Type': 'application/protobuf',\n 'Content-Length': headerBytes.length.toString(),\n },\n });\n\n // Add chat_history JSON\n const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));\n const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');\n formData.append('chat_history', chatHistoryBuffer, {\n filename: 'chat_history.json',\n contentType: 'application/json',\n knownLength: chatHistoryBuffer.length,\n header: {\n 'Content-Type': 'application/json',\n 'Content-Length': chatHistoryBuffer.length.toString(),\n },\n });\n\n // Add audio recording file if available\n if (report.audioRecordingPath && report.audioRecordingStartedAt) {\n let audioBytes: Buffer;\n try {\n audioBytes = await fs.readFile(report.audioRecordingPath);\n } catch {\n audioBytes = Buffer.alloc(0);\n }\n\n if (audioBytes.length > 0) {\n formData.append('audio', audioBytes, {\n filename: 'recording.ogg',\n contentType: 'audio/ogg',\n knownLength: audioBytes.length,\n header: {\n 'Content-Type': 'audio/ogg',\n 'Content-Length': audioBytes.length.toString(),\n },\n });\n }\n }\n\n // Upload to LiveKit Cloud using form-data's submit method\n // This properly streams the multipart form with all headers including Content-Length\n return new Promise<void>((resolve, reject) => {\n formData.submit(\n {\n protocol: 'https:',\n host: cloudHostname,\n path: '/observability/recordings/v0',\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n },\n },\n (err, res) => {\n if (err) {\n reject(new Error(`Failed to upload session report: ${err.message}`));\n return;\n }\n\n if (res.statusCode && res.statusCode >= 400) {\n reject(\n new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`),\n );\n return;\n }\n\n res.resume(); // Drain the response\n res.on('end', () => resolve());\n },\n );\n });\n}\n"],"mappings":"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","names":["otelContext","FormData","fs"]}
1
+ {"version":3,"sources":["../../src/telemetry/traces.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { MetricsRecordingHeader } from '@livekit/protocol';\nimport {\n type Attributes,\n type Context,\n type Span,\n type SpanOptions,\n type Tracer,\n type TracerProvider,\n context as otelContext,\n trace,\n} from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';\nimport { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';\nimport { Resource } from '@opentelemetry/resources';\nimport type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport FormData from 'form-data';\nimport { AccessToken } from 'livekit-server-sdk';\nimport fs from 'node:fs/promises';\nimport type { ChatContent, ChatItem } from '../llm/index.js';\nimport { enableOtelLogging } from '../log.js';\nimport type { SessionReport } from '../voice/report.js';\nimport { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';\nimport { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';\n\nexport interface StartSpanOptions {\n /** Name of the span */\n name: string;\n /** Optional parent context to use for this span */\n context?: Context;\n /** Attributes to set on the span when it starts */\n attributes?: Attributes;\n /** Whether to end the span when the function exits (default: true) */\n endOnExit?: boolean;\n}\n\n/**\n * A dynamic tracer that allows the tracer provider to be changed at runtime.\n */\nclass DynamicTracer {\n private tracerProvider: TracerProvider;\n private tracer: Tracer;\n private readonly instrumentingModuleName: string;\n\n constructor(instrumentingModuleName: string) {\n this.instrumentingModuleName = instrumentingModuleName;\n this.tracerProvider = trace.getTracerProvider();\n this.tracer = trace.getTracer(instrumentingModuleName);\n }\n\n /**\n * Set a new tracer provider. This updates the underlying tracer instance.\n * @param provider - The new tracer provider to use\n */\n setProvider(provider: TracerProvider): void {\n this.tracerProvider = provider;\n this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);\n }\n\n /**\n * Get the underlying OpenTelemetry tracer.\n * Use this to access the full Tracer API when needed.\n */\n getTracer(): Tracer {\n return this.tracer;\n }\n\n /**\n * Start a span manually (without making it active).\n * You must call span.end() when done.\n *\n * @param options - Span configuration including name\n * @returns The created span\n */\n startSpan(options: StartSpanOptions): Span {\n const ctx = options.context || otelContext.active();\n const span = this.tracer.startSpan(\n options.name,\n {\n attributes: options.attributes,\n },\n ctx,\n );\n\n return span;\n }\n\n /**\n * Start a new span and make it active in the current context.\n * The span will automatically be ended when the provided function completes (unless endOnExit=false).\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n async startActiveSpan<T>(fn: (span: Span) => Promise<T>, options: StartSpanOptions): Promise<T> {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n // Directly return the tracer's startActiveSpan result - it handles async correctly\n return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {\n try {\n return await fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n\n /**\n * Synchronous version of startActiveSpan for non-async operations.\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n startActiveSpanSync<T>(fn: (span: Span) => T, options: StartSpanOptions): T {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {\n try {\n return fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n}\n\n/**\n * The global tracer instance used throughout the agents framework.\n * This tracer can have its provider updated at runtime via setTracerProvider().\n */\nexport const tracer = new DynamicTracer('livekit-agents');\n\nclass MetadataSpanProcessor implements SpanProcessor {\n private metadata: Attributes;\n\n constructor(metadata: Attributes) {\n this.metadata = metadata;\n }\n\n onStart(span: Span, _parentContext: Context): void {\n span.setAttributes(this.metadata);\n }\n\n onEnd(_span: ReadableSpan): void {}\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Set the tracer provider for the livekit-agents framework.\n * This should be called before agent session start if using custom tracer providers.\n *\n * @param provider - The tracer provider to use (must be a NodeTracerProvider)\n * @param options - Optional configuration with metadata property to inject into all spans\n *\n * @example\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\n * import { setTracerProvider } from '@livekit/agents/telemetry';\n *\n * const provider = new NodeTracerProvider();\n * setTracerProvider(provider, {\n * metadata: { room_id: 'room123', job_id: 'job456' }\n * });\n * ```\n */\nexport function setTracerProvider(\n provider: NodeTracerProvider,\n options?: { metadata?: Attributes },\n): void {\n if (options?.metadata) {\n provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));\n }\n\n tracer.setProvider(provider);\n}\n\n/**\n * Setup OpenTelemetry tracer for LiveKit Cloud observability.\n * This configures OTLP exporters to send traces to LiveKit Cloud.\n *\n * @param options - Configuration for cloud tracer with roomId, jobId, and cloudHostname properties\n *\n * @internal\n */\nexport async function setupCloudTracer(options: {\n roomId: string;\n jobId: string;\n cloudHostname: string;\n}): Promise<void> {\n const { roomId, jobId, cloudHostname } = options;\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing');\n }\n\n const token = new AccessToken(apiKey, apiSecret, {\n identity: 'livekit-agents-telemetry',\n ttl: '6h',\n });\n token.addObservabilityGrant({ write: true });\n\n try {\n const jwt = await token.toJwt();\n\n const headers = {\n Authorization: `Bearer ${jwt}`,\n };\n\n const metadata: Attributes = {\n room_id: roomId,\n job_id: jobId,\n };\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: 'livekit-agents',\n room_id: roomId,\n job_id: jobId,\n });\n\n // Configure OTLP exporter to send traces to LiveKit Cloud\n const spanExporter = new OTLPTraceExporter({\n url: `https://${cloudHostname}/observability/traces/otlp/v0`,\n headers,\n compression: CompressionAlgorithm.GZIP,\n });\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)],\n });\n tracerProvider.register();\n\n setTracerProvider(tracerProvider);\n\n // Initialize standalone Pino cloud exporter (no OTEL SDK dependency)\n initPinoCloudExporter({\n cloudHostname,\n roomId,\n jobId,\n });\n\n enableOtelLogging();\n } catch (error) {\n console.error('Failed to setup cloud tracer:', error);\n throw error;\n }\n}\n\n/**\n * Flush all pending Pino logs to ensure they are exported.\n * Call this before session/job ends to ensure all logs are sent.\n *\n * @internal\n */\nexport async function flushOtelLogs(): Promise<void> {\n await flushPinoLogs();\n}\n\n/**\n * Convert ChatItem to proto-compatible dictionary format.\n * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published\n */\nfunction chatItemToProto(item: ChatItem): Record<string, any> {\n const itemDict: Record<string, any> = {};\n\n if (item.type === 'message') {\n const roleMap: Record<string, string> = {\n developer: 'DEVELOPER',\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n const msg: Record<string, any> = {\n id: item.id,\n role: roleMap[item.role] || item.role.toUpperCase(),\n content: item.content.map((c: ChatContent) => ({ text: c })),\n createdAt: toRFC3339(item.createdAt),\n };\n\n if (item.interrupted) {\n msg.interrupted = item.interrupted;\n }\n\n // TODO(brian): Add extra and transcriptConfidence to ChatMessage\n // if (item.extra && Object.keys(item.extra).length > 0) {\n // msg.extra = item.extra;\n // }\n\n // if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {\n // msg.transcriptConfidence = item.transcriptConfidence;\n // }\n\n // TODO(brian): Add metrics to ChatMessage\n // const metrics = item.metrics || {};\n // if (Object.keys(metrics).length > 0) {\n // msg.metrics = {};\n // if (metrics.started_speaking_at) {\n // msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);\n // }\n // if (metrics.stopped_speaking_at) {\n // msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);\n // }\n // if (metrics.transcription_delay !== undefined) {\n // msg.metrics.transcriptionDelay = metrics.transcription_delay;\n // }\n // if (metrics.end_of_turn_delay !== undefined) {\n // msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;\n // }\n // if (metrics.on_user_turn_completed_delay !== undefined) {\n // msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;\n // }\n // if (metrics.llm_node_ttft !== undefined) {\n // msg.metrics.llmNodeTtft = metrics.llm_node_ttft;\n // }\n // if (metrics.tts_node_ttfb !== undefined) {\n // msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;\n // }\n // if (metrics.e2e_latency !== undefined) {\n // msg.metrics.e2eLatency = metrics.e2e_latency;\n // }\n // }\n\n itemDict.message = msg;\n } else if (item.type === 'function_call') {\n itemDict.functionCall = {\n id: item.id,\n callId: item.callId,\n arguments: item.args,\n name: item.name,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'function_call_output') {\n itemDict.functionCallOutput = {\n id: item.id,\n name: item.name,\n callId: item.callId,\n output: item.output,\n isError: item.isError,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'agent_handoff') {\n const handoff: Record<string, any> = {\n id: item.id,\n newAgentId: item.newAgentId,\n createdAt: toRFC3339(item.createdAt),\n };\n if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {\n handoff.oldAgentId = item.oldAgentId;\n }\n itemDict.agentHandoff = handoff;\n }\n\n try {\n if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {\n itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);\n } else if (\n item.type === 'function_call_output' &&\n typeof itemDict.functionCallOutput?.output === 'string'\n ) {\n itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);\n }\n } catch {\n // ignore parsing errors\n }\n\n return itemDict;\n}\n\n/**\n * Convert timestamp to RFC3339 format matching Python's _to_rfc3339.\n * Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.\n * @internal\n */\nfunction toRFC3339(valueMs: number | Date): string {\n // valueMs is already in milliseconds (from Date.now())\n const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);\n // Truncate sub-millisecond precision\n const truncated = new Date(Math.floor(dt.getTime()));\n return truncated.toISOString();\n}\n\n/**\n * Upload session report to LiveKit Cloud observability.\n * @param options - Configuration with agentName, cloudHostname, and report\n */\nexport async function uploadSessionReport(options: {\n agentName: string;\n cloudHostname: string;\n report: SessionReport;\n}): Promise<void> {\n const { agentName, cloudHostname, report } = options;\n\n // Create OTLP HTTP exporter for chat history logs\n // Uses raw HTTP JSON format which is required by LiveKit Cloud\n const logExporter = new SimpleOTLPHttpLogExporter({\n cloudHostname,\n resourceAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n },\n scopeName: 'chat_history',\n scopeAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n room: report.room,\n },\n });\n\n // Build log records for session report and chat items\n const logRecords: SimpleLogRecord[] = [];\n\n const commonAttrs = {\n room_id: report.roomId,\n job_id: report.jobId,\n 'logger.name': 'chat_history',\n };\n\n logRecords.push({\n body: 'session report',\n timestampMs: report.startedAt || report.timestamp || 0,\n attributes: {\n ...commonAttrs,\n 'session.options': report.options || {},\n 'session.report_timestamp': report.timestamp,\n agent_name: agentName,\n },\n });\n\n // Track last timestamp to ensure monotonic ordering when items have identical timestamps\n // This fixes the issue where function_call and function_call_output with same timestamp\n // get reordered by the dashboard\n let lastTimestamp = 0;\n for (const item of report.chatHistory.items) {\n // Skip null/undefined items\n if (!item) continue;\n\n // Ensure monotonically increasing timestamps for proper ordering\n // Add 0.001ms (1 microsecond) offset when timestamps collide\n // Also handle undefined/NaN timestamps from realtime mode (defensive)\n const hasValidTimestamp = Number.isFinite(item.createdAt);\n let itemTimestamp = hasValidTimestamp ? item.createdAt : Date.now();\n\n if (itemTimestamp <= lastTimestamp) {\n itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond\n }\n lastTimestamp = itemTimestamp;\n\n const itemProto = chatItemToProto(item);\n let severityNumber = SeverityNumber.UNSPECIFIED;\n let severityText = 'unspecified';\n\n if (item.type === 'function_call_output' && item.isError) {\n severityNumber = SeverityNumber.ERROR;\n severityText = 'error';\n }\n\n logRecords.push({\n body: 'chat item',\n timestampMs: itemTimestamp, // Adjusted for monotonic ordering\n attributes: { 'chat.item': itemProto, ...commonAttrs },\n severityNumber,\n severityText,\n });\n }\n\n await logExporter.export(logRecords);\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');\n }\n\n const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });\n token.addObservabilityGrant({ write: true });\n const jwt = await token.toJwt();\n\n const formData = new FormData();\n\n // Add header (protobuf MetricsRecordingHeader)\n const audioStartTime = report.audioRecordingStartedAt ?? 0;\n const headerMsg = new MetricsRecordingHeader({\n roomId: report.roomId,\n duration: BigInt(0), // TODO: Calculate actual duration from report\n startTime: {\n seconds: BigInt(Math.floor(audioStartTime / 1000)),\n nanos: Math.floor((audioStartTime % 1000) * 1e6),\n },\n });\n\n const headerBytes = Buffer.from(headerMsg.toBinary());\n formData.append('header', headerBytes, {\n filename: 'header.binpb',\n contentType: 'application/protobuf',\n knownLength: headerBytes.length,\n header: {\n 'Content-Type': 'application/protobuf',\n 'Content-Length': headerBytes.length.toString(),\n },\n });\n\n // Add chat_history JSON\n const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));\n const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');\n formData.append('chat_history', chatHistoryBuffer, {\n filename: 'chat_history.json',\n contentType: 'application/json',\n knownLength: chatHistoryBuffer.length,\n header: {\n 'Content-Type': 'application/json',\n 'Content-Length': chatHistoryBuffer.length.toString(),\n },\n });\n\n // Add audio recording file if available\n if (report.audioRecordingPath && report.audioRecordingStartedAt) {\n let audioBytes: Buffer;\n try {\n audioBytes = await fs.readFile(report.audioRecordingPath);\n } catch {\n audioBytes = Buffer.alloc(0);\n }\n\n if (audioBytes.length > 0) {\n formData.append('audio', audioBytes, {\n filename: 'recording.ogg',\n contentType: 'audio/ogg',\n knownLength: audioBytes.length,\n header: {\n 'Content-Type': 'audio/ogg',\n 'Content-Length': audioBytes.length.toString(),\n },\n });\n }\n }\n\n // Upload to LiveKit Cloud using form-data's submit method\n // This properly streams the multipart form with all headers including Content-Length\n return new Promise<void>((resolve, reject) => {\n formData.submit(\n {\n protocol: 'https:',\n host: cloudHostname,\n path: '/observability/recordings/v0',\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n },\n },\n (err, res) => {\n if (err) {\n reject(new Error(`Failed to upload session report: ${err.message}`));\n return;\n }\n\n if (res.statusCode && res.statusCode >= 400) {\n // Read response body for error details\n let body = '';\n res.on('data', (chunk) => {\n body += chunk.toString();\n });\n res.on('error', (readErr) => {\n reject(\n new Error(\n `Failed to upload session report: ${res.statusCode} ${res.statusMessage} (body read error: ${readErr.message})`,\n ),\n );\n });\n res.on('end', () => {\n reject(\n new Error(\n `Failed to upload session report: ${res.statusCode} ${res.statusMessage} - ${body}`,\n ),\n );\n });\n return;\n }\n\n res.resume(); // Drain the response\n res.on('error', (readErr) => reject(new Error(`Response read error: ${readErr.message}`)));\n res.on('end', () => resolve());\n },\n );\n });\n}\n"],"mappings":"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","names":["otelContext","FormData","fs"]}
@@ -1 +1 @@
1
- {"version":3,"file":"traces.d.ts","sourceRoot":"","sources":["../../src/telemetry/traces.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,IAAI,EAET,KAAK,MAAM,EACX,KAAK,cAAc,EAGpB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EAAsB,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAOvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mDAAmD;IACnD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,cAAM,aAAa;IACjB,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;gBAErC,uBAAuB,EAAE,MAAM;IAM3C;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAK3C;;;OAGG;IACH,SAAS,IAAI,MAAM;IAInB;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAa1C;;;;;;;OAOG;IACG,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;IAiB/F;;;;;;OAMG;IACH,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,CAAC;CAe5E;AAED;;;GAGG;AACH,eAAO,MAAM,MAAM,eAAsC,CAAC;AAwB1D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,UAAU,CAAA;CAAE,GAClC,IAAI,CAMN;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DhB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AA8HD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4KhB"}
1
+ {"version":3,"file":"traces.d.ts","sourceRoot":"","sources":["../../src/telemetry/traces.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,IAAI,EAET,KAAK,MAAM,EACX,KAAK,cAAc,EAGpB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EAAsB,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAOvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mDAAmD;IACnD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,cAAM,aAAa;IACjB,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;gBAErC,uBAAuB,EAAE,MAAM;IAM3C;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAK3C;;;OAGG;IACH,SAAS,IAAI,MAAM;IAInB;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAa1C;;;;;;;OAOG;IACG,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;IAiB/F;;;;;;OAMG;IACH,mBAAmB,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,CAAC;CAe5E;AAED;;;GAGG;AACH,eAAO,MAAM,MAAM,eAAsC,CAAC;AAwB1D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,UAAU,CAAA;CAAE,GAClC,IAAI,CAMN;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DhB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AA8HD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoMhB"}
@@ -272,7 +272,9 @@ async function uploadSessionReport(options) {
272
272
  });
273
273
  let lastTimestamp = 0;
274
274
  for (const item of report.chatHistory.items) {
275
- let itemTimestamp = item.createdAt;
275
+ if (!item) continue;
276
+ const hasValidTimestamp = Number.isFinite(item.createdAt);
277
+ let itemTimestamp = hasValidTimestamp ? item.createdAt : Date.now();
276
278
  if (itemTimestamp <= lastTimestamp) {
277
279
  itemTimestamp = lastTimestamp + 1e-3;
278
280
  }
@@ -370,12 +372,28 @@ async function uploadSessionReport(options) {
370
372
  return;
371
373
  }
372
374
  if (res.statusCode && res.statusCode >= 400) {
373
- reject(
374
- new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`)
375
- );
375
+ let body = "";
376
+ res.on("data", (chunk) => {
377
+ body += chunk.toString();
378
+ });
379
+ res.on("error", (readErr) => {
380
+ reject(
381
+ new Error(
382
+ `Failed to upload session report: ${res.statusCode} ${res.statusMessage} (body read error: ${readErr.message})`
383
+ )
384
+ );
385
+ });
386
+ res.on("end", () => {
387
+ reject(
388
+ new Error(
389
+ `Failed to upload session report: ${res.statusCode} ${res.statusMessage} - ${body}`
390
+ )
391
+ );
392
+ });
376
393
  return;
377
394
  }
378
395
  res.resume();
396
+ res.on("error", (readErr) => reject(new Error(`Response read error: ${readErr.message}`)));
379
397
  res.on("end", () => resolve());
380
398
  }
381
399
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/telemetry/traces.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { MetricsRecordingHeader } from '@livekit/protocol';\nimport {\n type Attributes,\n type Context,\n type Span,\n type SpanOptions,\n type Tracer,\n type TracerProvider,\n context as otelContext,\n trace,\n} from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';\nimport { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';\nimport { Resource } from '@opentelemetry/resources';\nimport type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport FormData from 'form-data';\nimport { AccessToken } from 'livekit-server-sdk';\nimport fs from 'node:fs/promises';\nimport type { ChatContent, ChatItem } from '../llm/index.js';\nimport { enableOtelLogging } from '../log.js';\nimport type { SessionReport } from '../voice/report.js';\nimport { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';\nimport { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';\n\nexport interface StartSpanOptions {\n /** Name of the span */\n name: string;\n /** Optional parent context to use for this span */\n context?: Context;\n /** Attributes to set on the span when it starts */\n attributes?: Attributes;\n /** Whether to end the span when the function exits (default: true) */\n endOnExit?: boolean;\n}\n\n/**\n * A dynamic tracer that allows the tracer provider to be changed at runtime.\n */\nclass DynamicTracer {\n private tracerProvider: TracerProvider;\n private tracer: Tracer;\n private readonly instrumentingModuleName: string;\n\n constructor(instrumentingModuleName: string) {\n this.instrumentingModuleName = instrumentingModuleName;\n this.tracerProvider = trace.getTracerProvider();\n this.tracer = trace.getTracer(instrumentingModuleName);\n }\n\n /**\n * Set a new tracer provider. This updates the underlying tracer instance.\n * @param provider - The new tracer provider to use\n */\n setProvider(provider: TracerProvider): void {\n this.tracerProvider = provider;\n this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);\n }\n\n /**\n * Get the underlying OpenTelemetry tracer.\n * Use this to access the full Tracer API when needed.\n */\n getTracer(): Tracer {\n return this.tracer;\n }\n\n /**\n * Start a span manually (without making it active).\n * You must call span.end() when done.\n *\n * @param options - Span configuration including name\n * @returns The created span\n */\n startSpan(options: StartSpanOptions): Span {\n const ctx = options.context || otelContext.active();\n const span = this.tracer.startSpan(\n options.name,\n {\n attributes: options.attributes,\n },\n ctx,\n );\n\n return span;\n }\n\n /**\n * Start a new span and make it active in the current context.\n * The span will automatically be ended when the provided function completes (unless endOnExit=false).\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n async startActiveSpan<T>(fn: (span: Span) => Promise<T>, options: StartSpanOptions): Promise<T> {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n // Directly return the tracer's startActiveSpan result - it handles async correctly\n return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {\n try {\n return await fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n\n /**\n * Synchronous version of startActiveSpan for non-async operations.\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n startActiveSpanSync<T>(fn: (span: Span) => T, options: StartSpanOptions): T {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {\n try {\n return fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n}\n\n/**\n * The global tracer instance used throughout the agents framework.\n * This tracer can have its provider updated at runtime via setTracerProvider().\n */\nexport const tracer = new DynamicTracer('livekit-agents');\n\nclass MetadataSpanProcessor implements SpanProcessor {\n private metadata: Attributes;\n\n constructor(metadata: Attributes) {\n this.metadata = metadata;\n }\n\n onStart(span: Span, _parentContext: Context): void {\n span.setAttributes(this.metadata);\n }\n\n onEnd(_span: ReadableSpan): void {}\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Set the tracer provider for the livekit-agents framework.\n * This should be called before agent session start if using custom tracer providers.\n *\n * @param provider - The tracer provider to use (must be a NodeTracerProvider)\n * @param options - Optional configuration with metadata property to inject into all spans\n *\n * @example\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\n * import { setTracerProvider } from '@livekit/agents/telemetry';\n *\n * const provider = new NodeTracerProvider();\n * setTracerProvider(provider, {\n * metadata: { room_id: 'room123', job_id: 'job456' }\n * });\n * ```\n */\nexport function setTracerProvider(\n provider: NodeTracerProvider,\n options?: { metadata?: Attributes },\n): void {\n if (options?.metadata) {\n provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));\n }\n\n tracer.setProvider(provider);\n}\n\n/**\n * Setup OpenTelemetry tracer for LiveKit Cloud observability.\n * This configures OTLP exporters to send traces to LiveKit Cloud.\n *\n * @param options - Configuration for cloud tracer with roomId, jobId, and cloudHostname properties\n *\n * @internal\n */\nexport async function setupCloudTracer(options: {\n roomId: string;\n jobId: string;\n cloudHostname: string;\n}): Promise<void> {\n const { roomId, jobId, cloudHostname } = options;\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing');\n }\n\n const token = new AccessToken(apiKey, apiSecret, {\n identity: 'livekit-agents-telemetry',\n ttl: '6h',\n });\n token.addObservabilityGrant({ write: true });\n\n try {\n const jwt = await token.toJwt();\n\n const headers = {\n Authorization: `Bearer ${jwt}`,\n };\n\n const metadata: Attributes = {\n room_id: roomId,\n job_id: jobId,\n };\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: 'livekit-agents',\n room_id: roomId,\n job_id: jobId,\n });\n\n // Configure OTLP exporter to send traces to LiveKit Cloud\n const spanExporter = new OTLPTraceExporter({\n url: `https://${cloudHostname}/observability/traces/otlp/v0`,\n headers,\n compression: CompressionAlgorithm.GZIP,\n });\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)],\n });\n tracerProvider.register();\n\n setTracerProvider(tracerProvider);\n\n // Initialize standalone Pino cloud exporter (no OTEL SDK dependency)\n initPinoCloudExporter({\n cloudHostname,\n roomId,\n jobId,\n });\n\n enableOtelLogging();\n } catch (error) {\n console.error('Failed to setup cloud tracer:', error);\n throw error;\n }\n}\n\n/**\n * Flush all pending Pino logs to ensure they are exported.\n * Call this before session/job ends to ensure all logs are sent.\n *\n * @internal\n */\nexport async function flushOtelLogs(): Promise<void> {\n await flushPinoLogs();\n}\n\n/**\n * Convert ChatItem to proto-compatible dictionary format.\n * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published\n */\nfunction chatItemToProto(item: ChatItem): Record<string, any> {\n const itemDict: Record<string, any> = {};\n\n if (item.type === 'message') {\n const roleMap: Record<string, string> = {\n developer: 'DEVELOPER',\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n const msg: Record<string, any> = {\n id: item.id,\n role: roleMap[item.role] || item.role.toUpperCase(),\n content: item.content.map((c: ChatContent) => ({ text: c })),\n createdAt: toRFC3339(item.createdAt),\n };\n\n if (item.interrupted) {\n msg.interrupted = item.interrupted;\n }\n\n // TODO(brian): Add extra and transcriptConfidence to ChatMessage\n // if (item.extra && Object.keys(item.extra).length > 0) {\n // msg.extra = item.extra;\n // }\n\n // if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {\n // msg.transcriptConfidence = item.transcriptConfidence;\n // }\n\n // TODO(brian): Add metrics to ChatMessage\n // const metrics = item.metrics || {};\n // if (Object.keys(metrics).length > 0) {\n // msg.metrics = {};\n // if (metrics.started_speaking_at) {\n // msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);\n // }\n // if (metrics.stopped_speaking_at) {\n // msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);\n // }\n // if (metrics.transcription_delay !== undefined) {\n // msg.metrics.transcriptionDelay = metrics.transcription_delay;\n // }\n // if (metrics.end_of_turn_delay !== undefined) {\n // msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;\n // }\n // if (metrics.on_user_turn_completed_delay !== undefined) {\n // msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;\n // }\n // if (metrics.llm_node_ttft !== undefined) {\n // msg.metrics.llmNodeTtft = metrics.llm_node_ttft;\n // }\n // if (metrics.tts_node_ttfb !== undefined) {\n // msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;\n // }\n // if (metrics.e2e_latency !== undefined) {\n // msg.metrics.e2eLatency = metrics.e2e_latency;\n // }\n // }\n\n itemDict.message = msg;\n } else if (item.type === 'function_call') {\n itemDict.functionCall = {\n id: item.id,\n callId: item.callId,\n arguments: item.args,\n name: item.name,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'function_call_output') {\n itemDict.functionCallOutput = {\n id: item.id,\n name: item.name,\n callId: item.callId,\n output: item.output,\n isError: item.isError,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'agent_handoff') {\n const handoff: Record<string, any> = {\n id: item.id,\n newAgentId: item.newAgentId,\n createdAt: toRFC3339(item.createdAt),\n };\n if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {\n handoff.oldAgentId = item.oldAgentId;\n }\n itemDict.agentHandoff = handoff;\n }\n\n try {\n if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {\n itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);\n } else if (\n item.type === 'function_call_output' &&\n typeof itemDict.functionCallOutput?.output === 'string'\n ) {\n itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);\n }\n } catch {\n // ignore parsing errors\n }\n\n return itemDict;\n}\n\n/**\n * Convert timestamp to RFC3339 format matching Python's _to_rfc3339.\n * Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.\n * @internal\n */\nfunction toRFC3339(valueMs: number | Date): string {\n // valueMs is already in milliseconds (from Date.now())\n const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);\n // Truncate sub-millisecond precision\n const truncated = new Date(Math.floor(dt.getTime()));\n return truncated.toISOString();\n}\n\n/**\n * Upload session report to LiveKit Cloud observability.\n * @param options - Configuration with agentName, cloudHostname, and report\n */\nexport async function uploadSessionReport(options: {\n agentName: string;\n cloudHostname: string;\n report: SessionReport;\n}): Promise<void> {\n const { agentName, cloudHostname, report } = options;\n\n // Create OTLP HTTP exporter for chat history logs\n // Uses raw HTTP JSON format which is required by LiveKit Cloud\n const logExporter = new SimpleOTLPHttpLogExporter({\n cloudHostname,\n resourceAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n },\n scopeName: 'chat_history',\n scopeAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n room: report.room,\n },\n });\n\n // Build log records for session report and chat items\n const logRecords: SimpleLogRecord[] = [];\n\n const commonAttrs = {\n room_id: report.roomId,\n job_id: report.jobId,\n 'logger.name': 'chat_history',\n };\n\n logRecords.push({\n body: 'session report',\n timestampMs: report.startedAt || report.timestamp || 0,\n attributes: {\n ...commonAttrs,\n 'session.options': report.options || {},\n 'session.report_timestamp': report.timestamp,\n agent_name: agentName,\n },\n });\n\n // Track last timestamp to ensure monotonic ordering when items have identical timestamps\n // This fixes the issue where function_call and function_call_output with same timestamp\n // get reordered by the dashboard\n let lastTimestamp = 0;\n for (const item of report.chatHistory.items) {\n // Ensure monotonically increasing timestamps for proper ordering\n // Add 0.001ms (1 microsecond) offset when timestamps collide\n let itemTimestamp = item.createdAt;\n if (itemTimestamp <= lastTimestamp) {\n itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond\n }\n lastTimestamp = itemTimestamp;\n\n const itemProto = chatItemToProto(item);\n let severityNumber = SeverityNumber.UNSPECIFIED;\n let severityText = 'unspecified';\n\n if (item.type === 'function_call_output' && item.isError) {\n severityNumber = SeverityNumber.ERROR;\n severityText = 'error';\n }\n\n logRecords.push({\n body: 'chat item',\n timestampMs: itemTimestamp, // Adjusted for monotonic ordering\n attributes: { 'chat.item': itemProto, ...commonAttrs },\n severityNumber,\n severityText,\n });\n }\n await logExporter.export(logRecords);\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');\n }\n\n const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });\n token.addObservabilityGrant({ write: true });\n const jwt = await token.toJwt();\n\n const formData = new FormData();\n\n // Add header (protobuf MetricsRecordingHeader)\n const audioStartTime = report.audioRecordingStartedAt ?? 0;\n const headerMsg = new MetricsRecordingHeader({\n roomId: report.roomId,\n duration: BigInt(0), // TODO: Calculate actual duration from report\n startTime: {\n seconds: BigInt(Math.floor(audioStartTime / 1000)),\n nanos: Math.floor((audioStartTime % 1000) * 1e6),\n },\n });\n\n const headerBytes = Buffer.from(headerMsg.toBinary());\n formData.append('header', headerBytes, {\n filename: 'header.binpb',\n contentType: 'application/protobuf',\n knownLength: headerBytes.length,\n header: {\n 'Content-Type': 'application/protobuf',\n 'Content-Length': headerBytes.length.toString(),\n },\n });\n\n // Add chat_history JSON\n const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));\n const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');\n formData.append('chat_history', chatHistoryBuffer, {\n filename: 'chat_history.json',\n contentType: 'application/json',\n knownLength: chatHistoryBuffer.length,\n header: {\n 'Content-Type': 'application/json',\n 'Content-Length': chatHistoryBuffer.length.toString(),\n },\n });\n\n // Add audio recording file if available\n if (report.audioRecordingPath && report.audioRecordingStartedAt) {\n let audioBytes: Buffer;\n try {\n audioBytes = await fs.readFile(report.audioRecordingPath);\n } catch {\n audioBytes = Buffer.alloc(0);\n }\n\n if (audioBytes.length > 0) {\n formData.append('audio', audioBytes, {\n filename: 'recording.ogg',\n contentType: 'audio/ogg',\n knownLength: audioBytes.length,\n header: {\n 'Content-Type': 'audio/ogg',\n 'Content-Length': audioBytes.length.toString(),\n },\n });\n }\n }\n\n // Upload to LiveKit Cloud using form-data's submit method\n // This properly streams the multipart form with all headers including Content-Length\n return new Promise<void>((resolve, reject) => {\n formData.submit(\n {\n protocol: 'https:',\n host: cloudHostname,\n path: '/observability/recordings/v0',\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n },\n },\n (err, res) => {\n if (err) {\n reject(new Error(`Failed to upload session report: ${err.message}`));\n return;\n }\n\n if (res.statusCode && res.statusCode >= 400) {\n reject(\n new Error(`Failed to upload session report: ${res.statusCode} ${res.statusMessage}`),\n );\n return;\n }\n\n res.resume(); // Drain the response\n res.on('end', () => resolve());\n },\n );\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/telemetry/traces.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2025 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport { MetricsRecordingHeader } from '@livekit/protocol';\nimport {\n type Attributes,\n type Context,\n type Span,\n type SpanOptions,\n type Tracer,\n type TracerProvider,\n context as otelContext,\n trace,\n} from '@opentelemetry/api';\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';\nimport { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';\nimport { Resource } from '@opentelemetry/resources';\nimport type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport FormData from 'form-data';\nimport { AccessToken } from 'livekit-server-sdk';\nimport fs from 'node:fs/promises';\nimport type { ChatContent, ChatItem } from '../llm/index.js';\nimport { enableOtelLogging } from '../log.js';\nimport type { SessionReport } from '../voice/report.js';\nimport { type SimpleLogRecord, SimpleOTLPHttpLogExporter } from './otel_http_exporter.js';\nimport { flushPinoLogs, initPinoCloudExporter } from './pino_otel_transport.js';\n\nexport interface StartSpanOptions {\n /** Name of the span */\n name: string;\n /** Optional parent context to use for this span */\n context?: Context;\n /** Attributes to set on the span when it starts */\n attributes?: Attributes;\n /** Whether to end the span when the function exits (default: true) */\n endOnExit?: boolean;\n}\n\n/**\n * A dynamic tracer that allows the tracer provider to be changed at runtime.\n */\nclass DynamicTracer {\n private tracerProvider: TracerProvider;\n private tracer: Tracer;\n private readonly instrumentingModuleName: string;\n\n constructor(instrumentingModuleName: string) {\n this.instrumentingModuleName = instrumentingModuleName;\n this.tracerProvider = trace.getTracerProvider();\n this.tracer = trace.getTracer(instrumentingModuleName);\n }\n\n /**\n * Set a new tracer provider. This updates the underlying tracer instance.\n * @param provider - The new tracer provider to use\n */\n setProvider(provider: TracerProvider): void {\n this.tracerProvider = provider;\n this.tracer = this.tracerProvider.getTracer(this.instrumentingModuleName);\n }\n\n /**\n * Get the underlying OpenTelemetry tracer.\n * Use this to access the full Tracer API when needed.\n */\n getTracer(): Tracer {\n return this.tracer;\n }\n\n /**\n * Start a span manually (without making it active).\n * You must call span.end() when done.\n *\n * @param options - Span configuration including name\n * @returns The created span\n */\n startSpan(options: StartSpanOptions): Span {\n const ctx = options.context || otelContext.active();\n const span = this.tracer.startSpan(\n options.name,\n {\n attributes: options.attributes,\n },\n ctx,\n );\n\n return span;\n }\n\n /**\n * Start a new span and make it active in the current context.\n * The span will automatically be ended when the provided function completes (unless endOnExit=false).\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n async startActiveSpan<T>(fn: (span: Span) => Promise<T>, options: StartSpanOptions): Promise<T> {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n // Directly return the tracer's startActiveSpan result - it handles async correctly\n return await this.tracer.startActiveSpan(options.name, opts, ctx, async (span) => {\n try {\n return await fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n\n /**\n * Synchronous version of startActiveSpan for non-async operations.\n *\n * @param fn - The function to execute within the span context\n * @param options - Span configuration including name\n * @returns The result of the provided function\n */\n startActiveSpanSync<T>(fn: (span: Span) => T, options: StartSpanOptions): T {\n const ctx = options.context || otelContext.active();\n const endOnExit = options.endOnExit === undefined ? true : options.endOnExit; // default true\n const opts: SpanOptions = { attributes: options.attributes };\n\n return this.tracer.startActiveSpan(options.name, opts, ctx, (span) => {\n try {\n return fn(span);\n } finally {\n if (endOnExit) {\n span.end();\n }\n }\n });\n }\n}\n\n/**\n * The global tracer instance used throughout the agents framework.\n * This tracer can have its provider updated at runtime via setTracerProvider().\n */\nexport const tracer = new DynamicTracer('livekit-agents');\n\nclass MetadataSpanProcessor implements SpanProcessor {\n private metadata: Attributes;\n\n constructor(metadata: Attributes) {\n this.metadata = metadata;\n }\n\n onStart(span: Span, _parentContext: Context): void {\n span.setAttributes(this.metadata);\n }\n\n onEnd(_span: ReadableSpan): void {}\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n}\n\n/**\n * Set the tracer provider for the livekit-agents framework.\n * This should be called before agent session start if using custom tracer providers.\n *\n * @param provider - The tracer provider to use (must be a NodeTracerProvider)\n * @param options - Optional configuration with metadata property to inject into all spans\n *\n * @example\n * ```typescript\n * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\n * import { setTracerProvider } from '@livekit/agents/telemetry';\n *\n * const provider = new NodeTracerProvider();\n * setTracerProvider(provider, {\n * metadata: { room_id: 'room123', job_id: 'job456' }\n * });\n * ```\n */\nexport function setTracerProvider(\n provider: NodeTracerProvider,\n options?: { metadata?: Attributes },\n): void {\n if (options?.metadata) {\n provider.addSpanProcessor(new MetadataSpanProcessor(options.metadata));\n }\n\n tracer.setProvider(provider);\n}\n\n/**\n * Setup OpenTelemetry tracer for LiveKit Cloud observability.\n * This configures OTLP exporters to send traces to LiveKit Cloud.\n *\n * @param options - Configuration for cloud tracer with roomId, jobId, and cloudHostname properties\n *\n * @internal\n */\nexport async function setupCloudTracer(options: {\n roomId: string;\n jobId: string;\n cloudHostname: string;\n}): Promise<void> {\n const { roomId, jobId, cloudHostname } = options;\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for cloud tracing');\n }\n\n const token = new AccessToken(apiKey, apiSecret, {\n identity: 'livekit-agents-telemetry',\n ttl: '6h',\n });\n token.addObservabilityGrant({ write: true });\n\n try {\n const jwt = await token.toJwt();\n\n const headers = {\n Authorization: `Bearer ${jwt}`,\n };\n\n const metadata: Attributes = {\n room_id: roomId,\n job_id: jobId,\n };\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: 'livekit-agents',\n room_id: roomId,\n job_id: jobId,\n });\n\n // Configure OTLP exporter to send traces to LiveKit Cloud\n const spanExporter = new OTLPTraceExporter({\n url: `https://${cloudHostname}/observability/traces/otlp/v0`,\n headers,\n compression: CompressionAlgorithm.GZIP,\n });\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [new MetadataSpanProcessor(metadata), new BatchSpanProcessor(spanExporter)],\n });\n tracerProvider.register();\n\n setTracerProvider(tracerProvider);\n\n // Initialize standalone Pino cloud exporter (no OTEL SDK dependency)\n initPinoCloudExporter({\n cloudHostname,\n roomId,\n jobId,\n });\n\n enableOtelLogging();\n } catch (error) {\n console.error('Failed to setup cloud tracer:', error);\n throw error;\n }\n}\n\n/**\n * Flush all pending Pino logs to ensure they are exported.\n * Call this before session/job ends to ensure all logs are sent.\n *\n * @internal\n */\nexport async function flushOtelLogs(): Promise<void> {\n await flushPinoLogs();\n}\n\n/**\n * Convert ChatItem to proto-compatible dictionary format.\n * TODO: Use actual agent_session proto types once @livekit/protocol v1.43.1+ is published\n */\nfunction chatItemToProto(item: ChatItem): Record<string, any> {\n const itemDict: Record<string, any> = {};\n\n if (item.type === 'message') {\n const roleMap: Record<string, string> = {\n developer: 'DEVELOPER',\n system: 'SYSTEM',\n user: 'USER',\n assistant: 'ASSISTANT',\n };\n\n const msg: Record<string, any> = {\n id: item.id,\n role: roleMap[item.role] || item.role.toUpperCase(),\n content: item.content.map((c: ChatContent) => ({ text: c })),\n createdAt: toRFC3339(item.createdAt),\n };\n\n if (item.interrupted) {\n msg.interrupted = item.interrupted;\n }\n\n // TODO(brian): Add extra and transcriptConfidence to ChatMessage\n // if (item.extra && Object.keys(item.extra).length > 0) {\n // msg.extra = item.extra;\n // }\n\n // if (item.transcriptConfidence !== undefined && item.transcriptConfidence !== null) {\n // msg.transcriptConfidence = item.transcriptConfidence;\n // }\n\n // TODO(brian): Add metrics to ChatMessage\n // const metrics = item.metrics || {};\n // if (Object.keys(metrics).length > 0) {\n // msg.metrics = {};\n // if (metrics.started_speaking_at) {\n // msg.metrics.startedSpeakingAt = toRFC3339(metrics.started_speaking_at);\n // }\n // if (metrics.stopped_speaking_at) {\n // msg.metrics.stoppedSpeakingAt = toRFC3339(metrics.stopped_speaking_at);\n // }\n // if (metrics.transcription_delay !== undefined) {\n // msg.metrics.transcriptionDelay = metrics.transcription_delay;\n // }\n // if (metrics.end_of_turn_delay !== undefined) {\n // msg.metrics.endOfTurnDelay = metrics.end_of_turn_delay;\n // }\n // if (metrics.on_user_turn_completed_delay !== undefined) {\n // msg.metrics.onUserTurnCompletedDelay = metrics.on_user_turn_completed_delay;\n // }\n // if (metrics.llm_node_ttft !== undefined) {\n // msg.metrics.llmNodeTtft = metrics.llm_node_ttft;\n // }\n // if (metrics.tts_node_ttfb !== undefined) {\n // msg.metrics.ttsNodeTtfb = metrics.tts_node_ttfb;\n // }\n // if (metrics.e2e_latency !== undefined) {\n // msg.metrics.e2eLatency = metrics.e2e_latency;\n // }\n // }\n\n itemDict.message = msg;\n } else if (item.type === 'function_call') {\n itemDict.functionCall = {\n id: item.id,\n callId: item.callId,\n arguments: item.args,\n name: item.name,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'function_call_output') {\n itemDict.functionCallOutput = {\n id: item.id,\n name: item.name,\n callId: item.callId,\n output: item.output,\n isError: item.isError,\n createdAt: toRFC3339(item.createdAt),\n };\n } else if (item.type === 'agent_handoff') {\n const handoff: Record<string, any> = {\n id: item.id,\n newAgentId: item.newAgentId,\n createdAt: toRFC3339(item.createdAt),\n };\n if (item.oldAgentId !== undefined && item.oldAgentId !== null && item.oldAgentId !== '') {\n handoff.oldAgentId = item.oldAgentId;\n }\n itemDict.agentHandoff = handoff;\n }\n\n try {\n if (item.type === 'function_call' && typeof itemDict.functionCall?.arguments === 'string') {\n itemDict.functionCall.arguments = JSON.parse(itemDict.functionCall.arguments);\n } else if (\n item.type === 'function_call_output' &&\n typeof itemDict.functionCallOutput?.output === 'string'\n ) {\n itemDict.functionCallOutput.output = JSON.parse(itemDict.functionCallOutput.output);\n }\n } catch {\n // ignore parsing errors\n }\n\n return itemDict;\n}\n\n/**\n * Convert timestamp to RFC3339 format matching Python's _to_rfc3339.\n * Note: TypeScript createdAt is in milliseconds (Date.now()), not seconds like Python.\n * @internal\n */\nfunction toRFC3339(valueMs: number | Date): string {\n // valueMs is already in milliseconds (from Date.now())\n const dt = valueMs instanceof Date ? valueMs : new Date(valueMs);\n // Truncate sub-millisecond precision\n const truncated = new Date(Math.floor(dt.getTime()));\n return truncated.toISOString();\n}\n\n/**\n * Upload session report to LiveKit Cloud observability.\n * @param options - Configuration with agentName, cloudHostname, and report\n */\nexport async function uploadSessionReport(options: {\n agentName: string;\n cloudHostname: string;\n report: SessionReport;\n}): Promise<void> {\n const { agentName, cloudHostname, report } = options;\n\n // Create OTLP HTTP exporter for chat history logs\n // Uses raw HTTP JSON format which is required by LiveKit Cloud\n const logExporter = new SimpleOTLPHttpLogExporter({\n cloudHostname,\n resourceAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n },\n scopeName: 'chat_history',\n scopeAttributes: {\n room_id: report.roomId,\n job_id: report.jobId,\n room: report.room,\n },\n });\n\n // Build log records for session report and chat items\n const logRecords: SimpleLogRecord[] = [];\n\n const commonAttrs = {\n room_id: report.roomId,\n job_id: report.jobId,\n 'logger.name': 'chat_history',\n };\n\n logRecords.push({\n body: 'session report',\n timestampMs: report.startedAt || report.timestamp || 0,\n attributes: {\n ...commonAttrs,\n 'session.options': report.options || {},\n 'session.report_timestamp': report.timestamp,\n agent_name: agentName,\n },\n });\n\n // Track last timestamp to ensure monotonic ordering when items have identical timestamps\n // This fixes the issue where function_call and function_call_output with same timestamp\n // get reordered by the dashboard\n let lastTimestamp = 0;\n for (const item of report.chatHistory.items) {\n // Skip null/undefined items\n if (!item) continue;\n\n // Ensure monotonically increasing timestamps for proper ordering\n // Add 0.001ms (1 microsecond) offset when timestamps collide\n // Also handle undefined/NaN timestamps from realtime mode (defensive)\n const hasValidTimestamp = Number.isFinite(item.createdAt);\n let itemTimestamp = hasValidTimestamp ? item.createdAt : Date.now();\n\n if (itemTimestamp <= lastTimestamp) {\n itemTimestamp = lastTimestamp + 0.001; // Add 1 microsecond\n }\n lastTimestamp = itemTimestamp;\n\n const itemProto = chatItemToProto(item);\n let severityNumber = SeverityNumber.UNSPECIFIED;\n let severityText = 'unspecified';\n\n if (item.type === 'function_call_output' && item.isError) {\n severityNumber = SeverityNumber.ERROR;\n severityText = 'error';\n }\n\n logRecords.push({\n body: 'chat item',\n timestampMs: itemTimestamp, // Adjusted for monotonic ordering\n attributes: { 'chat.item': itemProto, ...commonAttrs },\n severityNumber,\n severityText,\n });\n }\n\n await logExporter.export(logRecords);\n\n const apiKey = process.env.LIVEKIT_API_KEY;\n const apiSecret = process.env.LIVEKIT_API_SECRET;\n\n if (!apiKey || !apiSecret) {\n throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set for session upload');\n }\n\n const token = new AccessToken(apiKey, apiSecret, { ttl: '6h' });\n token.addObservabilityGrant({ write: true });\n const jwt = await token.toJwt();\n\n const formData = new FormData();\n\n // Add header (protobuf MetricsRecordingHeader)\n const audioStartTime = report.audioRecordingStartedAt ?? 0;\n const headerMsg = new MetricsRecordingHeader({\n roomId: report.roomId,\n duration: BigInt(0), // TODO: Calculate actual duration from report\n startTime: {\n seconds: BigInt(Math.floor(audioStartTime / 1000)),\n nanos: Math.floor((audioStartTime % 1000) * 1e6),\n },\n });\n\n const headerBytes = Buffer.from(headerMsg.toBinary());\n formData.append('header', headerBytes, {\n filename: 'header.binpb',\n contentType: 'application/protobuf',\n knownLength: headerBytes.length,\n header: {\n 'Content-Type': 'application/protobuf',\n 'Content-Length': headerBytes.length.toString(),\n },\n });\n\n // Add chat_history JSON\n const chatHistoryJson = JSON.stringify(report.chatHistory.toJSON({ excludeTimestamp: false }));\n const chatHistoryBuffer = Buffer.from(chatHistoryJson, 'utf-8');\n formData.append('chat_history', chatHistoryBuffer, {\n filename: 'chat_history.json',\n contentType: 'application/json',\n knownLength: chatHistoryBuffer.length,\n header: {\n 'Content-Type': 'application/json',\n 'Content-Length': chatHistoryBuffer.length.toString(),\n },\n });\n\n // Add audio recording file if available\n if (report.audioRecordingPath && report.audioRecordingStartedAt) {\n let audioBytes: Buffer;\n try {\n audioBytes = await fs.readFile(report.audioRecordingPath);\n } catch {\n audioBytes = Buffer.alloc(0);\n }\n\n if (audioBytes.length > 0) {\n formData.append('audio', audioBytes, {\n filename: 'recording.ogg',\n contentType: 'audio/ogg',\n knownLength: audioBytes.length,\n header: {\n 'Content-Type': 'audio/ogg',\n 'Content-Length': audioBytes.length.toString(),\n },\n });\n }\n }\n\n // Upload to LiveKit Cloud using form-data's submit method\n // This properly streams the multipart form with all headers including Content-Length\n return new Promise<void>((resolve, reject) => {\n formData.submit(\n {\n protocol: 'https:',\n host: cloudHostname,\n path: '/observability/recordings/v0',\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n },\n },\n (err, res) => {\n if (err) {\n reject(new Error(`Failed to upload session report: ${err.message}`));\n return;\n }\n\n if (res.statusCode && res.statusCode >= 400) {\n // Read response body for error details\n let body = '';\n res.on('data', (chunk) => {\n body += chunk.toString();\n });\n res.on('error', (readErr) => {\n reject(\n new Error(\n `Failed to upload session report: ${res.statusCode} ${res.statusMessage} (body read error: ${readErr.message})`,\n ),\n );\n });\n res.on('end', () => {\n reject(\n new Error(\n `Failed to upload session report: ${res.statusCode} ${res.statusMessage} - ${body}`,\n ),\n );\n });\n return;\n }\n\n res.resume(); // Drain the response\n res.on('error', (readErr) => reject(new Error(`Response read error: ${readErr.message}`)));\n res.on('end', () => resolve());\n },\n );\n });\n}\n"],"mappings":"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","names":[]}
@@ -49,6 +49,8 @@ class AgentActivity {
49
49
  started = false;
50
50
  audioRecognition;
51
51
  realtimeSession;
52
+ realtimeSpans;
53
+ // Maps response_id to OTEL span for metrics recording
52
54
  turnDetectionMode;
53
55
  logger = (0, import_log.log)();
54
56
  _draining = false;
@@ -137,6 +139,7 @@ class AgentActivity {
137
139
  this.agent._agentActivity = this;
138
140
  if (this.llm instanceof import_llm.RealtimeModel) {
139
141
  this.realtimeSession = this.llm.session();
142
+ this.realtimeSpans = /* @__PURE__ */ new Map();
140
143
  this.realtimeSession.on("generation_created", (ev) => this.onGenerationCreated(ev));
141
144
  this.realtimeSession.on("input_speech_started", (ev) => this.onInputSpeechStarted(ev));
142
145
  this.realtimeSession.on("input_speech_stopped", (ev) => this.onInputSpeechStopped(ev));
@@ -349,6 +352,13 @@ class AgentActivity {
349
352
  if (speechHandle && (ev.type === "llm_metrics" || ev.type === "tts_metrics")) {
350
353
  ev.speechId = speechHandle.id;
351
354
  }
355
+ if (ev.type === "realtime_model_metrics" && this.realtimeSpans) {
356
+ const span = this.realtimeSpans.get(ev.requestId);
357
+ if (span) {
358
+ (0, import_telemetry.recordRealtimeMetrics)(span, ev);
359
+ this.realtimeSpans.delete(ev.requestId);
360
+ }
361
+ }
352
362
  this.agentSession.emit(
353
363
  import_events.AgentSessionEventTypes.MetricsCollected,
354
364
  (0, import_events.createMetricsCollectedEvent)({ metrics: ev })
@@ -1262,6 +1272,10 @@ ${instructions}` : instructions,
1262
1272
  if (!(this.llm instanceof import_llm.RealtimeModel)) {
1263
1273
  throw new Error("llm is not a realtime model");
1264
1274
  }
1275
+ span.setAttribute(import_telemetry.traceTypes.ATTR_GEN_AI_REQUEST_MODEL, this.llm.model);
1276
+ if (this.realtimeSpans && ev.responseId) {
1277
+ this.realtimeSpans.set(ev.responseId, span);
1278
+ }
1265
1279
  this.logger.debug(
1266
1280
  { speech_id: speechHandle.id, stepIndex: speechHandle.numSteps },
1267
1281
  "realtime generation started"
@@ -1489,7 +1503,12 @@ ${instructions}` : instructions,
1489
1503
  this.agentSession._updateAgentState("thinking");
1490
1504
  });
1491
1505
  await executeToolsTask.result;
1492
- if (toolOutput.output.length === 0) return;
1506
+ if (toolOutput.output.length === 0) {
1507
+ if (!speechHandle.interrupted) {
1508
+ this.agentSession._updateAgentState("listening");
1509
+ }
1510
+ return;
1511
+ }
1493
1512
  const { maxToolSteps } = this.agentSession.options;
1494
1513
  if (speechHandle.numSteps >= maxToolSteps) {
1495
1514
  this.logger.warn(
@@ -1667,7 +1686,7 @@ ${instructions}` : instructions,
1667
1686
  }
1668
1687
  }
1669
1688
  async close() {
1670
- var _a, _b, _c;
1689
+ var _a, _b, _c, _d;
1671
1690
  const unlock = await this.lock.lock();
1672
1691
  try {
1673
1692
  if (!this._draining) {
@@ -1697,9 +1716,10 @@ ${instructions}` : instructions,
1697
1716
  this.vad.off("metrics_collected", this.onMetricsCollected);
1698
1717
  }
1699
1718
  this.detachAudioInput();
1700
- await ((_a = this.realtimeSession) == null ? void 0 : _a.close());
1701
- await ((_b = this.audioRecognition) == null ? void 0 : _b.close());
1702
- await ((_c = this._mainTask) == null ? void 0 : _c.cancelAndWait());
1719
+ (_a = this.realtimeSpans) == null ? void 0 : _a.clear();
1720
+ await ((_b = this.realtimeSession) == null ? void 0 : _b.close());
1721
+ await ((_c = this.audioRecognition) == null ? void 0 : _c.close());
1722
+ await ((_d = this._mainTask) == null ? void 0 : _d.cancelAndWait());
1703
1723
  } finally {
1704
1724
  unlock();
1705
1725
  }