@cloudbase/agent-observability 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -1,10 +1,95 @@
1
+ import {
2
+ OBSERVABILITY_TRACER_NAME,
3
+ init_constants
4
+ } from "./chunk-5EXUNUGP.mjs";
1
5
  import "./chunk-NFEGQTCC.mjs";
2
6
 
7
+ // src/server/SingleLineConsoleSpanExporter.ts
8
+ init_constants();
9
+ var SingleLineConsoleSpanExporter = class {
10
+ /**
11
+ * Export spans as single-line JSON.
12
+ */
13
+ export(spans, resultCallback) {
14
+ try {
15
+ for (const span of spans) {
16
+ const spanDict = this.spanToDict(span);
17
+ const jsonLine = JSON.stringify(spanDict);
18
+ console.log(jsonLine);
19
+ }
20
+ resultCallback({ code: 0 /* SUCCESS */ });
21
+ } catch (error) {
22
+ resultCallback({ code: 1 /* FAILED */, error });
23
+ }
24
+ }
25
+ /**
26
+ * Shutdown the exporter.
27
+ */
28
+ shutdown() {
29
+ return Promise.resolve();
30
+ }
31
+ /**
32
+ * Force flush the exporter.
33
+ */
34
+ forceFlush() {
35
+ return Promise.resolve();
36
+ }
37
+ /**
38
+ * Convert a ReadableSpan to a dictionary for JSON serialization.
39
+ */
40
+ spanToDict(span) {
41
+ try {
42
+ const context = span.spanContext();
43
+ const parentId = span.parentSpanId || span.parentSpanContext?.spanId;
44
+ const result = {
45
+ name: span.name,
46
+ context: {
47
+ trace_id: context.traceId,
48
+ span_id: context.spanId,
49
+ trace_flags: context.traceFlags
50
+ },
51
+ kind: span.kind,
52
+ parent_id: parentId,
53
+ start_time: span.startTime,
54
+ end_time: span.endTime,
55
+ status: {
56
+ status_code: span.status.code,
57
+ description: span.status.message
58
+ },
59
+ attributes: span.attributes,
60
+ events: span.events.map((event) => ({
61
+ name: event.name,
62
+ timestamp: event.time,
63
+ attributes: event.attributes
64
+ })),
65
+ links: span.links.map((link) => ({
66
+ context: {
67
+ trace_id: link.context.traceId,
68
+ span_id: link.context.spanId
69
+ },
70
+ attributes: link.attributes
71
+ })),
72
+ resource: {
73
+ attributes: span.resource.attributes
74
+ }
75
+ };
76
+ result["_log_from"] = OBSERVABILITY_TRACER_NAME;
77
+ return result;
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+ };
83
+ function isSingleLineConsoleExporterEnabled() {
84
+ const value = process.env.CONSOLE_EXPORTER_SINGLE_LINE?.toLowerCase() || "true";
85
+ return ["true", "1", "yes", "on"].includes(value);
86
+ }
87
+
3
88
  // src/server/setup.ts
4
89
  var TRUTHY_ENV_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
5
90
  var DEFAULT_BATCH_CONFIG = {
6
91
  maxExportBatchSize: 100,
7
- scheduledDelayMillis: 5e3,
92
+ scheduledDelayMillis: 1e3,
8
93
  maxQueueSize: 2048,
9
94
  exportTimeoutMillis: 3e4
10
95
  };
@@ -52,33 +137,29 @@ async function setupConsoleExporter(config) {
52
137
  const { trace } = await import("@opentelemetry/api");
53
138
  const { resourceFromAttributes } = await import("@opentelemetry/resources");
54
139
  const { NodeTracerProvider } = await import("@opentelemetry/sdk-trace-node");
55
- const { ConsoleSpanExporter, BatchSpanProcessor } = await import("@opentelemetry/sdk-trace-base");
56
- const batchConfig = resolveBatchConfig(config.batch);
140
+ const { ConsoleSpanExporter, SimpleSpanProcessor } = await import("@opentelemetry/sdk-trace-base");
141
+ const useSingleLine = isSingleLineConsoleExporterEnabled();
142
+ const exporter = useSingleLine ? new SingleLineConsoleSpanExporter() : new ConsoleSpanExporter();
143
+ const exporterType = useSingleLine ? "single-line" : "multi-line";
57
144
  let provider = trace.getTracerProvider();
58
145
  const isRealProvider = "addSpanProcessor" in provider;
146
+ const processor = new SimpleSpanProcessor(exporter);
59
147
  if (isRealProvider) {
60
- const exporter = new ConsoleSpanExporter();
61
- const processor = new BatchSpanProcessor(exporter, batchConfig);
62
148
  provider.addSpanProcessor(processor);
63
- console.info(
64
- `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`
65
- );
66
149
  } else {
67
150
  const resource = resourceFromAttributes({
68
151
  "service.name": process.env.OTEL_SERVICE_NAME || "ag-ui-server",
69
152
  "service.version": "1.0.0"
70
153
  });
71
- const exporter = new ConsoleSpanExporter();
72
- const processor = new BatchSpanProcessor(exporter, batchConfig);
73
154
  const tracerProvider = new NodeTracerProvider({
74
155
  resource,
75
156
  spanProcessors: [processor]
76
157
  });
77
158
  tracerProvider.register();
78
- console.info(
79
- `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`
80
- );
81
159
  }
160
+ console.info(
161
+ `[Observability] Console exporter configured (${exporterType}, simple processor)`
162
+ );
82
163
  }
83
164
  async function setupOTLPExporter(config) {
84
165
  const { trace } = await import("@opentelemetry/api");
@@ -172,6 +253,8 @@ var ExporterType = {
172
253
  };
173
254
  export {
174
255
  ExporterType,
256
+ SingleLineConsoleSpanExporter,
257
+ isSingleLineConsoleExporterEnabled,
175
258
  setupObservability
176
259
  };
177
260
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/setup.ts","../src/server/config.ts"],"sourcesContent":["/**\n * Observability setup implementation for AG-Kit Server.\n *\n * Merges configuration from environment variables and parameters,\n * then applies each exporter configuration.\n *\n * @packageDocumentation\n */\n\nimport type {\n BatchConfig,\n ObservabilityConfig,\n ConsoleTraceConfig,\n OTLPTraceConfig,\n CustomTraceConfig,\n} from './config';\n\nexport type {\n BatchConfig,\n ObservabilityConfig,\n ConsoleTraceConfig,\n OTLPTraceConfig,\n CustomTraceConfig,\n} from './config';\n\n/**\n * Environment variable truthy values.\n * Matches Python SDK implementation for consistency.\n */\nconst TRUTHY_ENV_VALUES = new Set(['true', '1', 'yes', 'on']);\n\n/**\n * Merged configuration result.\n * Console config allows merge (param overrides env),\n * while OTLP and custom configs are arrays (additive).\n */\ninterface MergedConfig {\n console?: ConsoleTraceConfig | null;\n otlp: OTLPTraceConfig[];\n custom: CustomTraceConfig[];\n}\n\n/**\n * Default batch configuration.\n */\nconst DEFAULT_BATCH_CONFIG: Required<BatchConfig> = {\n maxExportBatchSize: 100,\n scheduledDelayMillis: 5000,\n maxQueueSize: 2048,\n exportTimeoutMillis: 30000,\n};\n\n/**\n * Merge environment variable and parameter configurations.\n *\n * - AUTO_TRACES_STDOUT env adds a console config\n * - Parameter configs override/extend env configs\n */\nfunction mergeConfigs(paramConfigs: ObservabilityConfig[]): MergedConfig {\n const result: MergedConfig = {\n console: null,\n otlp: [],\n custom: [],\n };\n\n // 1. Check AUTO_TRACES_STDOUT env\n const autoTracesStdout = process.env.AUTO_TRACES_STDOUT?.toLowerCase() || '';\n if (TRUTHY_ENV_VALUES.has(autoTracesStdout)) {\n result.console = { type: 'console' };\n console.debug(\n `[Observability] AUTO_TRACES_STDOUT=${autoTracesStdout}, console exporter enabled`\n );\n }\n\n // 2. Process parameter configs\n for (const config of paramConfigs) {\n switch (config.type) {\n case 'console':\n // Parameter overrides env (merge batch config)\n result.console = { ...result.console, ...config };\n break;\n\n case 'otlp':\n result.otlp.push(config);\n break;\n\n case 'custom':\n result.custom.push(config);\n break;\n }\n }\n\n return result;\n}\n\n/**\n * Apply batch configuration with defaults.\n */\nfunction resolveBatchConfig(batch?: BatchConfig): Required<BatchConfig> {\n return { ...DEFAULT_BATCH_CONFIG, ...batch };\n}\n\n/**\n * Safe wrapper for exporter setup functions.\n * Ensures individual exporter failures don't crash the entire setup.\n */\nasync function safeSetup(\n name: string,\n setupFn: () => Promise<void>\n): Promise<void> {\n try {\n await setupFn();\n } catch (error) {\n console.warn(\n `[Observability] ${name} setup failed (non-fatal): ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n // Don't rethrow - allow other exporters to continue\n }\n}\n\n/**\n * Setup console exporter.\n */\nasync function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {\n const { trace } = await import('@opentelemetry/api');\n const { resourceFromAttributes } = await import('@opentelemetry/resources');\n const { NodeTracerProvider } = await import('@opentelemetry/sdk-trace-node');\n const { ConsoleSpanExporter, BatchSpanProcessor } = await import(\n '@opentelemetry/sdk-trace-base'\n );\n\n const batchConfig = resolveBatchConfig(config.batch);\n\n // Check if a real TracerProvider already exists\n let provider = trace.getTracerProvider();\n const isRealProvider = 'addSpanProcessor' in provider;\n\n if (isRealProvider) {\n // Add processor to existing provider\n const exporter = new ConsoleSpanExporter();\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n (provider as any).addSpanProcessor(processor);\n\n console.info(\n `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, ` +\n `delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n } else {\n // Create new provider with console exporter\n const resource = resourceFromAttributes({\n 'service.name': process.env.OTEL_SERVICE_NAME || 'ag-ui-server',\n 'service.version': '1.0.0',\n });\n\n const exporter = new ConsoleSpanExporter();\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [processor],\n });\n\n tracerProvider.register();\n\n console.info(\n `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, ` +\n `delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n }\n}\n\n/**\n * Setup OTLP exporter.\n */\nasync function setupOTLPExporter(config: OTLPTraceConfig): Promise<void> {\n const { trace } = await import('@opentelemetry/api');\n const { resourceFromAttributes } = await import('@opentelemetry/resources');\n const { NodeTracerProvider } = await import('@opentelemetry/sdk-trace-node');\n const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http');\n const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-base');\n\n const batchConfig = resolveBatchConfig(config.batch);\n\n // Check if a real TracerProvider already exists\n let provider = trace.getTracerProvider();\n const isRealProvider = 'addSpanProcessor' in provider;\n\n if (isRealProvider) {\n // Add processor to existing provider\n const exporter = new OTLPTraceExporter({\n url: config.url,\n headers: config.headers,\n timeoutMillis: config.timeout ?? 10000,\n });\n\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n (provider as any).addSpanProcessor(processor);\n\n console.info(\n `[Observability] OTLP exporter configured (url=${config.url}, ` +\n `batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n } else {\n // Create new provider with OTLP exporter\n const resource = resourceFromAttributes({\n 'service.name': process.env.OTEL_SERVICE_NAME || 'ag-ui-server',\n 'service.version': '1.0.0',\n });\n\n const exporter = new OTLPTraceExporter({\n url: config.url,\n headers: config.headers,\n timeoutMillis: config.timeout ?? 10000,\n });\n\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [processor],\n });\n\n tracerProvider.register();\n\n console.info(\n `[Observability] OTLP exporter configured (url=${config.url}, ` +\n `batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n }\n}\n\n/**\n * Setup custom exporter.\n */\nasync function setupCustomExporter(config: CustomTraceConfig): Promise<void> {\n await config.setup();\n console.info(`[Observability] Custom exporter setup completed`);\n}\n\n/**\n * Setup observability from merged configuration.\n *\n * @internal\n */\nasync function applyMergedConfigs(merged: MergedConfig): Promise<void> {\n const setupTasks: Promise<void>[] = [];\n\n // Apply console (non-blocking)\n if (merged.console) {\n setupTasks.push(safeSetup('Console exporter', () => setupConsoleExporter(merged.console!)));\n }\n\n // Apply otlp (non-blocking)\n for (const otlp of merged.otlp) {\n setupTasks.push(safeSetup(`OTLP exporter (${otlp.url})`, () => setupOTLPExporter(otlp)));\n }\n\n // Apply custom (non-blocking)\n for (const custom of merged.custom) {\n setupTasks.push(safeSetup('Custom exporter', () => setupCustomExporter(custom)));\n }\n\n // Wait for all exporters to complete (or fail gracefully)\n await Promise.all(setupTasks);\n\n if (merged.console || merged.otlp.length > 0 || merged.custom.length > 0) {\n console.info(`[Observability] Setup completed`);\n }\n}\n\n/**\n * Setup observability from configuration.\n *\n * Merges environment variable (AUTO_TRACES_STDOUT) with parameter configs,\n * then applies each exporter configuration.\n *\n * Environment variables act as presets, parameter configs override or extend.\n *\n * Returns a promise that resolves when setup is complete. This allows callers\n * to await initialization before proceeding, eliminating race conditions.\n *\n * The returned promise is cached - subsequent calls return the same promise\n * to avoid duplicate initialization.\n *\n * @param configs - Observability configuration(s)\n *\n * @example\n * ```typescript\n * // Console only (from env)\n * await setupObservability();\n *\n * // Console + OTLP\n * await setupObservability([\n * { type: 'console' },\n * { type: 'otlp', url: 'http://localhost:4318/v1/traces' }\n * ]);\n *\n * // OTLP only\n * await setupObservability({\n * type: 'otlp',\n * url: 'https://cloud.langfuse.com/api/public/otlp/v1/traces',\n * headers: { 'Authorization': 'Basic xxx' }\n * });\n * ```\n *\n * @public\n */\n\nlet setupPromise: Promise<void> | null = null;\n\nexport async function setupObservability(\n configs?: ObservabilityConfig | ObservabilityConfig[]\n): Promise<void> {\n // Return cached promise if setup is in progress or completed\n if (setupPromise) {\n return setupPromise;\n }\n\n // Create the setup promise\n setupPromise = (async () => {\n try {\n // Normalize to array\n const configsArray = configs\n ? Array.isArray(configs)\n ? configs\n : [configs]\n : [];\n\n // Merge env and parameter configs\n const merged = mergeConfigs(configsArray);\n\n // Apply merged configs\n await applyMergedConfigs(merged);\n } catch (error) {\n // Reset promise on error to allow retry\n setupPromise = null;\n // Silent failure - observability should never block main flow\n console.warn(\n `[Observability] Setup failed: ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n }\n })();\n\n return setupPromise;\n}\n","/**\n * Observability configuration types for AG-Kit Server.\n *\n * Provides a unified configuration interface for trace exporters:\n * - Console: Development/debugging output\n * - OTLP: Production export to Langfuse, Jaeger, etc.\n * - Custom: User-defined setup logic\n *\n * @packageDocumentation\n */\n\n/**\n * Trace exporter type constants.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * { type: ExporterType.Console }\n * { type: ExporterType.OTLP }\n * { type: ExporterType.Custom }\n * ```\n *\n * @public\n */\nexport const ExporterType = {\n /** Console exporter - outputs traces to stdout */\n Console: 'console',\n /** OTLP exporter - sends traces to OTLP-compatible backend */\n OTLP: 'otlp',\n /** Custom exporter - user-defined setup logic */\n Custom: 'custom',\n} as const;\n\n/**\n * Trace exporter type literal values.\n *\n * @public\n */\nexport type ExporterType = typeof ExporterType[keyof typeof ExporterType];\n\n/**\n * Batch processing configuration for span exporters.\n *\n * Used by BatchSpanProcessor to optimize performance:\n * - Collects spans in memory and exports them in batches\n * - Reduces I/O operations (console) or network requests (OTLP)\n * - Recommended for production environments\n *\n * @public\n */\nexport interface BatchConfig {\n /** Maximum number of spans per export batch (default: 100) */\n maxExportBatchSize?: number;\n /** Maximum delay in milliseconds before exporting (default: 5000) */\n scheduledDelayMillis?: number;\n /** Maximum queue size (default: 2048) */\n maxQueueSize?: number;\n /** Export timeout in milliseconds (default: 30000) */\n exportTimeoutMillis?: number;\n}\n\n/**\n * Console trace exporter configuration.\n *\n * Outputs traces to stdout in JSON format using ConsoleSpanExporter.\n * Useful for development and debugging.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * { type: ExporterType.Console }\n * { type: ExporterType.Console, batch: { maxExportBatchSize: 200 } }\n * ```\n *\n * @public\n */\nexport interface ConsoleTraceConfig {\n /** Discriminator for console exporter */\n type: typeof ExporterType.Console;\n /** Optional batch processing configuration */\n batch?: BatchConfig;\n}\n\n/**\n * OTLP trace exporter configuration.\n *\n * Exports traces via OTLP protocol to any compatible backend:\n * - Langfuse: https://cloud.langfuse.com/api/public/otlp/v1/traces\n * - Jaeger: http://localhost:4318/v1/traces\n * - OTel Collector: custom endpoint\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * {\n * type: ExporterType.OTLP,\n * url: 'https://cloud.langfuse.com/api/public/otlp/v1/traces',\n * headers: {\n * 'Authorization': 'Basic ' + btoa('pk-lf-xxx:sk-lf-xxx')\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface OTLPTraceConfig {\n /** Discriminator for OTLP exporter */\n type: typeof ExporterType.OTLP;\n /** OTLP endpoint URL (http/https) */\n url: string;\n /** Optional HTTP headers for authentication */\n headers?: Record<string, string>;\n /** Request timeout in milliseconds (default: 10000) */\n timeout?: number;\n /** Optional batch processing configuration */\n batch?: BatchConfig;\n}\n\n/**\n * Custom trace exporter configuration.\n *\n * Allows users to provide custom trace setup logic.\n * Useful for integrations not covered by console/otlp.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * {\n * type: ExporterType.Custom,\n * setup: async () => {\n * const exporter = new MyCustomExporter();\n * const provider = new BasicTracerProvider();\n * provider.addSpanProcessor(new SimpleSpanProcessor(exporter));\n * provider.register();\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface CustomTraceConfig {\n /** Discriminator for custom setup */\n type: typeof ExporterType.Custom;\n /** User-defined setup function */\n setup: () => Promise<void> | void;\n}\n\n/**\n * Union type of all supported trace exporter configurations.\n *\n * @public\n */\nexport type ObservabilityConfig =\n | ConsoleTraceConfig\n | OTLPTraceConfig\n | CustomTraceConfig;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server/SingleLineConsoleSpanExporter.ts","../src/server/setup.ts","../src/server/config.ts"],"sourcesContent":["/**\n * Custom console exporter that outputs single-line JSON.\n *\n * This exporter outputs spans as single-line JSON for easier parsing\n * with line-based tools (grep, jq, etc.).\n *\n * To switch back to standard multi-line output, use ConsoleSpanExporter instead.\n *\n * @example\n * ```typescript\n * // Single-line (current default)\n * const exporter = new SingleLineConsoleSpanExporter();\n *\n * // Multi-line (standard OTel)\n * import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';\n * const exporter = new ConsoleSpanExporter();\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { OBSERVABILITY_TRACER_NAME } from '../core/constants';\n\n/**\n * Export result codes.\n */\nexport enum ExportResultCode {\n SUCCESS = 0,\n FAILED = 1,\n}\n\n/**\n * Export result interface.\n */\nexport interface ExportResult {\n code: ExportResultCode;\n error?: Error;\n}\n\n/**\n * Custom console exporter that outputs single-line JSON.\n *\n * This exporter outputs spans as single-line JSON for easier parsing\n * with line-based tools (grep, jq, etc.).\n */\nexport class SingleLineConsoleSpanExporter implements SpanExporter {\n /**\n * Export spans as single-line JSON.\n */\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n try {\n for (const span of spans) {\n const spanDict = this.spanToDict(span);\n // Single-line JSON output\n const jsonLine = JSON.stringify(spanDict);\n console.log(jsonLine);\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n } catch (error) {\n resultCallback({ code: ExportResultCode.FAILED, error: error as Error });\n }\n }\n\n /**\n * Shutdown the exporter.\n */\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Force flush the exporter.\n */\n forceFlush?(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Convert a ReadableSpan to a dictionary for JSON serialization.\n */\n private spanToDict(span: ReadableSpan): Record<string, unknown> {\n try {\n const context = span.spanContext();\n\n // Get parent span ID from parentSpanContext if available\n // Some versions use parentSpanId directly, others use parentSpanContext.spanId\n const parentId = (span as any).parentSpanId || span.parentSpanContext?.spanId;\n\n const result: Record<string, unknown> = {\n name: span.name,\n context: {\n trace_id: context.traceId,\n span_id: context.spanId,\n trace_flags: context.traceFlags,\n },\n kind: span.kind,\n parent_id: parentId,\n start_time: span.startTime,\n end_time: span.endTime,\n status: {\n status_code: span.status.code,\n description: span.status.message,\n },\n attributes: span.attributes,\n events: span.events.map((event) => ({\n name: event.name,\n timestamp: event.time,\n attributes: event.attributes,\n })),\n links: span.links.map((link) => ({\n context: {\n trace_id: link.context.traceId,\n span_id: link.context.spanId,\n },\n attributes: link.attributes,\n })),\n resource: {\n attributes: span.resource.attributes,\n },\n };\n\n // Add log source identifier for console output\n result['_log_from'] = OBSERVABILITY_TRACER_NAME;\n\n return result;\n } catch {\n return {};\n }\n }\n}\n\n/**\n * Check if single-line console exporter should be used.\n *\n * Set CONSOLE_EXPORTER_SINGLE_LINE=false to use standard multi-line output.\n */\nexport function isSingleLineConsoleExporterEnabled(): boolean {\n const value = process.env.CONSOLE_EXPORTER_SINGLE_LINE?.toLowerCase() || 'true';\n return ['true', '1', 'yes', 'on'].includes(value);\n}\n","/**\n * Observability setup implementation for the server.\n *\n * Merges configuration from environment variables and parameters,\n * then applies each exporter configuration.\n *\n * @packageDocumentation\n */\n\nimport type {\n BatchConfig,\n ObservabilityConfig,\n ConsoleTraceConfig,\n OTLPTraceConfig,\n CustomTraceConfig,\n} from './config';\nimport {\n SingleLineConsoleSpanExporter,\n isSingleLineConsoleExporterEnabled,\n} from './SingleLineConsoleSpanExporter';\n\nexport type {\n BatchConfig,\n ObservabilityConfig,\n ConsoleTraceConfig,\n OTLPTraceConfig,\n CustomTraceConfig,\n} from './config';\nexport { SingleLineConsoleSpanExporter } from './SingleLineConsoleSpanExporter';\n\n/**\n * Environment variable truthy values.\n * Matches Python SDK implementation for consistency.\n */\nconst TRUTHY_ENV_VALUES = new Set(['true', '1', 'yes', 'on']);\n\n/**\n * Merged configuration result.\n * Console config allows merge (param overrides env),\n * while OTLP and custom configs are arrays (additive).\n */\ninterface MergedConfig {\n console?: ConsoleTraceConfig | null;\n otlp: OTLPTraceConfig[];\n custom: CustomTraceConfig[];\n}\n\n/**\n * Default batch configuration (used by OTLP exporter).\n */\nconst DEFAULT_BATCH_CONFIG: Required<BatchConfig> = {\n maxExportBatchSize: 100,\n scheduledDelayMillis: 1000,\n maxQueueSize: 2048,\n exportTimeoutMillis: 30000,\n};\n\n/**\n * Merge environment variable and parameter configurations.\n *\n * - AUTO_TRACES_STDOUT env adds a console config\n * - Parameter configs override/extend env configs\n */\nfunction mergeConfigs(paramConfigs: ObservabilityConfig[]): MergedConfig {\n const result: MergedConfig = {\n console: null,\n otlp: [],\n custom: [],\n };\n\n // 1. Check AUTO_TRACES_STDOUT env\n const autoTracesStdout = process.env.AUTO_TRACES_STDOUT?.toLowerCase() || '';\n if (TRUTHY_ENV_VALUES.has(autoTracesStdout)) {\n result.console = { type: 'console' };\n console.debug(\n `[Observability] AUTO_TRACES_STDOUT=${autoTracesStdout}, console exporter enabled`\n );\n }\n\n // 2. Process parameter configs\n for (const config of paramConfigs) {\n switch (config.type) {\n case 'console':\n // Parameter overrides env (merge batch config)\n result.console = { ...result.console, ...config };\n break;\n\n case 'otlp':\n result.otlp.push(config);\n break;\n\n case 'custom':\n result.custom.push(config);\n break;\n }\n }\n\n return result;\n}\n\n/**\n * Apply batch configuration with defaults.\n */\nfunction resolveBatchConfig(batch?: BatchConfig): Required<BatchConfig> {\n return { ...DEFAULT_BATCH_CONFIG, ...batch };\n}\n\n/**\n * Safe wrapper for exporter setup functions.\n * Ensures individual exporter failures don't crash the entire setup.\n */\nasync function safeSetup(\n name: string,\n setupFn: () => Promise<void>\n): Promise<void> {\n try {\n await setupFn();\n } catch (error) {\n console.warn(\n `[Observability] ${name} setup failed (non-fatal): ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n // Don't rethrow - allow other exporters to continue\n }\n}\n\n/**\n * Setup console exporter.\n *\n * Uses SimpleSpanProcessor for immediate export without batching.\n * This is optimal for console output (stdout.write is microsecond-level)\n * and ensures reliable data export in serverless environments (e.g., SCF)\n * where batch background threads may be frozen.\n *\n * Set CONSOLE_EXPORTER_SINGLE_LINE=false to use standard multi-line output.\n */\nasync function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {\n const { trace } = await import('@opentelemetry/api');\n const { resourceFromAttributes } = await import('@opentelemetry/resources');\n const { NodeTracerProvider } = await import('@opentelemetry/sdk-trace-node');\n const { ConsoleSpanExporter, SimpleSpanProcessor } = await import(\n '@opentelemetry/sdk-trace-base'\n );\n\n // Choose exporter type based on CONSOLE_EXPORTER_SINGLE_LINE env var\n // Single-line: easier for parsing with line-based tools (grep, jq, etc.)\n // Multi-line: more human-readable for debugging\n const useSingleLine = isSingleLineConsoleExporterEnabled();\n const exporter = useSingleLine\n ? new SingleLineConsoleSpanExporter()\n : new ConsoleSpanExporter();\n const exporterType = useSingleLine ? 'single-line' : 'multi-line';\n\n // Check if a real TracerProvider already exists\n let provider = trace.getTracerProvider();\n const isRealProvider = 'addSpanProcessor' in provider;\n\n const processor = new SimpleSpanProcessor(exporter);\n\n if (isRealProvider) {\n // Add processor to existing provider\n (provider as any).addSpanProcessor(processor);\n } else {\n // Create new provider with console exporter\n const resource = resourceFromAttributes({\n 'service.name': process.env.OTEL_SERVICE_NAME || 'ag-ui-server',\n 'service.version': '1.0.0',\n });\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [processor],\n });\n\n tracerProvider.register();\n }\n\n console.info(\n `[Observability] Console exporter configured (${exporterType}, simple processor)`\n );\n}\n\n/**\n * Setup OTLP exporter.\n */\nasync function setupOTLPExporter(config: OTLPTraceConfig): Promise<void> {\n const { trace } = await import('@opentelemetry/api');\n const { resourceFromAttributes } = await import('@opentelemetry/resources');\n const { NodeTracerProvider } = await import('@opentelemetry/sdk-trace-node');\n const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http');\n const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-base');\n\n const batchConfig = resolveBatchConfig(config.batch);\n\n // Check if a real TracerProvider already exists\n let provider = trace.getTracerProvider();\n const isRealProvider = 'addSpanProcessor' in provider;\n\n if (isRealProvider) {\n // Add processor to existing provider\n const exporter = new OTLPTraceExporter({\n url: config.url,\n headers: config.headers,\n timeoutMillis: config.timeout ?? 10000,\n });\n\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n (provider as any).addSpanProcessor(processor);\n\n console.info(\n `[Observability] OTLP exporter configured (url=${config.url}, ` +\n `batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n } else {\n // Create new provider with OTLP exporter\n const resource = resourceFromAttributes({\n 'service.name': process.env.OTEL_SERVICE_NAME || 'ag-ui-server',\n 'service.version': '1.0.0',\n });\n\n const exporter = new OTLPTraceExporter({\n url: config.url,\n headers: config.headers,\n timeoutMillis: config.timeout ?? 10000,\n });\n\n const processor = new BatchSpanProcessor(exporter, batchConfig);\n\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [processor],\n });\n\n tracerProvider.register();\n\n console.info(\n `[Observability] OTLP exporter configured (url=${config.url}, ` +\n `batch=${batchConfig.maxExportBatchSize}, delay=${batchConfig.scheduledDelayMillis}ms)`\n );\n }\n}\n\n/**\n * Setup custom exporter.\n */\nasync function setupCustomExporter(config: CustomTraceConfig): Promise<void> {\n await config.setup();\n console.info(`[Observability] Custom exporter setup completed`);\n}\n\n/**\n * Setup observability from merged configuration.\n *\n * @internal\n */\nasync function applyMergedConfigs(merged: MergedConfig): Promise<void> {\n const setupTasks: Promise<void>[] = [];\n\n // Apply console (non-blocking)\n if (merged.console) {\n setupTasks.push(safeSetup('Console exporter', () => setupConsoleExporter(merged.console!)));\n }\n\n // Apply otlp (non-blocking)\n for (const otlp of merged.otlp) {\n setupTasks.push(safeSetup(`OTLP exporter (${otlp.url})`, () => setupOTLPExporter(otlp)));\n }\n\n // Apply custom (non-blocking)\n for (const custom of merged.custom) {\n setupTasks.push(safeSetup('Custom exporter', () => setupCustomExporter(custom)));\n }\n\n // Wait for all exporters to complete (or fail gracefully)\n await Promise.all(setupTasks);\n\n if (merged.console || merged.otlp.length > 0 || merged.custom.length > 0) {\n console.info(`[Observability] Setup completed`);\n }\n}\n\n/**\n * Setup observability from configuration.\n *\n * Merges environment variable (AUTO_TRACES_STDOUT) with parameter configs,\n * then applies each exporter configuration.\n *\n * Environment variables act as presets, parameter configs override or extend.\n *\n * Returns a promise that resolves when setup is complete. This allows callers\n * to await initialization before proceeding, eliminating race conditions.\n *\n * The returned promise is cached - subsequent calls return the same promise\n * to avoid duplicate initialization.\n *\n * @param configs - Observability configuration(s)\n *\n * @example\n * ```typescript\n * // Console only (from env)\n * await setupObservability();\n *\n * // Console + OTLP\n * await setupObservability([\n * { type: 'console' },\n * { type: 'otlp', url: 'http://localhost:4318/v1/traces' }\n * ]);\n *\n * // OTLP only\n * await setupObservability({\n * type: 'otlp',\n * url: 'https://cloud.langfuse.com/api/public/otlp/v1/traces',\n * headers: { 'Authorization': 'Basic xxx' }\n * });\n * ```\n *\n * @public\n */\n\nlet setupPromise: Promise<void> | null = null;\n\nexport async function setupObservability(\n configs?: ObservabilityConfig | ObservabilityConfig[]\n): Promise<void> {\n // Return cached promise if setup is in progress or completed\n if (setupPromise) {\n return setupPromise;\n }\n\n // Create the setup promise\n setupPromise = (async () => {\n try {\n // Normalize to array\n const configsArray = configs\n ? Array.isArray(configs)\n ? configs\n : [configs]\n : [];\n\n // Merge env and parameter configs\n const merged = mergeConfigs(configsArray);\n\n // Apply merged configs\n await applyMergedConfigs(merged);\n } catch (error) {\n // Reset promise on error to allow retry\n setupPromise = null;\n // Silent failure - observability should never block main flow\n console.warn(\n `[Observability] Setup failed: ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n }\n })();\n\n return setupPromise;\n}\n","/**\n * Observability configuration types for the server.\n *\n * Provides a unified configuration interface for trace exporters:\n * - Console: Development/debugging output\n * - OTLP: Production export to Langfuse, Jaeger, etc.\n * - Custom: User-defined setup logic\n *\n * @packageDocumentation\n */\n\n/**\n * Trace exporter type constants.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * { type: ExporterType.Console }\n * { type: ExporterType.OTLP }\n * { type: ExporterType.Custom }\n * ```\n *\n * @public\n */\nexport const ExporterType = {\n /** Console exporter - outputs traces to stdout */\n Console: 'console',\n /** OTLP exporter - sends traces to OTLP-compatible backend */\n OTLP: 'otlp',\n /** Custom exporter - user-defined setup logic */\n Custom: 'custom',\n} as const;\n\n/**\n * Trace exporter type literal values.\n *\n * @public\n */\nexport type ExporterType = typeof ExporterType[keyof typeof ExporterType];\n\n/**\n * Batch processing configuration for span exporters.\n *\n * Used by BatchSpanProcessor to optimize performance:\n * - Collects spans in memory and exports them in batches\n * - Reduces I/O operations (console) or network requests (OTLP)\n * - Recommended for production environments\n *\n * @public\n */\nexport interface BatchConfig {\n /** Maximum number of spans per export batch (default: 100) */\n maxExportBatchSize?: number;\n /** Maximum delay in milliseconds before exporting (default: 5000) */\n scheduledDelayMillis?: number;\n /** Maximum queue size (default: 2048) */\n maxQueueSize?: number;\n /** Export timeout in milliseconds (default: 30000) */\n exportTimeoutMillis?: number;\n}\n\n/**\n * Console trace exporter configuration.\n *\n * Outputs traces to stdout in JSON format using ConsoleSpanExporter.\n * Uses SimpleSpanProcessor for immediate export (no batching).\n * Useful for development and debugging.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * { type: ExporterType.Console }\n * ```\n *\n * @public\n */\nexport interface ConsoleTraceConfig {\n /** Discriminator for console exporter */\n type: typeof ExporterType.Console;\n}\n\n/**\n * OTLP trace exporter configuration.\n *\n * Exports traces via OTLP protocol to any compatible backend:\n * - Langfuse: https://cloud.langfuse.com/api/public/otlp/v1/traces\n * - Jaeger: http://localhost:4318/v1/traces\n * - OTel Collector: custom endpoint\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * {\n * type: ExporterType.OTLP,\n * url: 'https://cloud.langfuse.com/api/public/otlp/v1/traces',\n * headers: {\n * 'Authorization': 'Basic ' + btoa('pk-lf-xxx:sk-lf-xxx')\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface OTLPTraceConfig {\n /** Discriminator for OTLP exporter */\n type: typeof ExporterType.OTLP;\n /** OTLP endpoint URL (http/https) */\n url: string;\n /** Optional HTTP headers for authentication */\n headers?: Record<string, string>;\n /** Request timeout in milliseconds (default: 10000) */\n timeout?: number;\n /** Optional batch processing configuration */\n batch?: BatchConfig;\n}\n\n/**\n * Custom trace exporter configuration.\n *\n * Allows users to provide custom trace setup logic.\n * Useful for integrations not covered by console/otlp.\n *\n * @example\n * ```typescript\n * import { ExporterType } from '@cloudbase/agent-observability/server';\n *\n * {\n * type: ExporterType.Custom,\n * setup: async () => {\n * const exporter = new MyCustomExporter();\n * const provider = new BasicTracerProvider();\n * provider.addSpanProcessor(new SimpleSpanProcessor(exporter));\n * provider.register();\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface CustomTraceConfig {\n /** Discriminator for custom setup */\n type: typeof ExporterType.Custom;\n /** User-defined setup function */\n setup: () => Promise<void> | void;\n}\n\n/**\n * Union type of all supported trace exporter configurations.\n *\n * @public\n */\nexport type ObservabilityConfig =\n | ConsoleTraceConfig\n | OTLPTraceConfig\n | CustomTraceConfig;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/agent-observability",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "OpenInference-compatible observability for AG-Kit",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -65,7 +65,7 @@
65
65
  "@arizeai/openinference-semantic-conventions": "^2.1.7",
66
66
  "@opentelemetry/api": "^1.9.0",
67
67
  "@opentelemetry/semantic-conventions": "^1.39.0",
68
- "@cloudbase/agent-shared": "^0.0.16"
68
+ "@cloudbase/agent-shared": "^0.0.18"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
@@ -1,6 +1,6 @@
1
- import { OBSERVABILITY_TRACER_NAME, OtelSpanAttributes } from "./constants.js";
1
+ import { OtelSpanAttributes, OpenInferenceSpanKind } from "./constants";
2
2
  import { SemanticConventions } from "@arizeai/openinference-semantic-conventions";
3
- import { ObservationAttributes, TraceAttributes, ObservationType } from "../types.js";
3
+ import { ObservationAttributes, TraceAttributes, ObservationType, LLMMessage, Document, ToolCall } from "../types";
4
4
  import { type Attributes } from "@opentelemetry/api";
5
5
 
6
6
  /**
@@ -64,7 +64,7 @@ export function createTraceAttributes({
64
64
  * - Uses `openinference.span.kind` for span type
65
65
  * - Uses `llm.*` for LLM-specific attributes
66
66
  * - Uses `tool.*` for tool-specific attributes
67
- * - Falls back to `agkit.observation.*` for non-standard attributes
67
+ * - Uses `observation.*` for non-standard attributes
68
68
  *
69
69
  * @param type - Observation type (llm, tool, chain, etc.)
70
70
  * @param attributes - Observation attributes to convert
@@ -89,20 +89,24 @@ export function createObservationAttributes(
89
89
  usageDetails,
90
90
  } = attributes;
91
91
 
92
+ // Map observation type to OpenInference span kind
93
+ // Falls back to CHAIN for unknown types (CHAIN is the generic catch-all in OpenInference)
94
+ const spanKind = (OpenInferenceSpanKind as Record<string, string>)[type.toUpperCase()] || "CHAIN";
95
+
92
96
  // Base attributes for all observation types
93
97
  const otelAttributes: Attributes = {
94
- [SemanticConventions.OPENINFERENCE_SPAN_KIND]: type.toUpperCase(),
98
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]: spanKind,
95
99
  [OtelSpanAttributes.OBSERVATION_TYPE]: type,
96
100
  [OtelSpanAttributes.OBSERVATION_LEVEL]: level,
97
101
  [OtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]: statusMessage,
98
102
  [OtelSpanAttributes.VERSION]: version,
99
103
  // Use OpenInference input.value convention
100
104
  [SemanticConventions.INPUT_VALUE]: _serialize(input),
101
- // Also set legacy agkit.observation.input for compatibility
105
+ // Also set observation.input for compatibility
102
106
  [OtelSpanAttributes.OBSERVATION_INPUT]: _serialize(input),
103
107
  // Use OpenInference output.value convention
104
108
  [SemanticConventions.OUTPUT_VALUE]: _serialize(output),
105
- // Also set legacy agkit.observation.output for compatibility
109
+ // Also set observation.output for compatibility
106
110
  [OtelSpanAttributes.OBSERVATION_OUTPUT]: _serialize(output),
107
111
  };
108
112
 
@@ -111,10 +115,24 @@ export function createObservationAttributes(
111
115
  if (model) {
112
116
  otelAttributes[SemanticConventions.LLM_MODEL_NAME] = model;
113
117
  }
118
+ // Extract system and provider safely
119
+ const system = (attributes as Record<string, unknown>)?.system;
120
+ const provider = (attributes as Record<string, unknown>)?.provider;
121
+ const inputMessages = (attributes as Record<string, unknown>)?.inputMessages;
122
+ const outputMessages = (attributes as Record<string, unknown>)?.outputMessages;
123
+ const inputMimeType = (attributes as Record<string, unknown>)?.inputMimeType;
124
+ const outputMimeType = (attributes as Record<string, unknown>)?.outputMimeType;
125
+
126
+ if (system !== undefined) {
127
+ otelAttributes[SemanticConventions.LLM_SYSTEM] = String(system);
128
+ }
129
+ if (provider !== undefined) {
130
+ otelAttributes[SemanticConventions.LLM_PROVIDER] = String(provider);
131
+ }
114
132
  if (modelParameters) {
115
133
  otelAttributes[SemanticConventions.LLM_INVOCATION_PARAMETERS] =
116
134
  _serialize(modelParameters);
117
- // Also set agkit.llm.model_parameters for compatibility
135
+ // Also set llm.model_parameters for compatibility
118
136
  otelAttributes[OtelSpanAttributes.LLM_MODEL_PARAMETERS] =
119
137
  _serialize(modelParameters);
120
138
  }
@@ -135,7 +153,7 @@ export function createObservationAttributes(
135
153
  usage.totalTokens;
136
154
  }
137
155
  }
138
- // Also set legacy agkit.llm.usage_details for compatibility
156
+ // Also set llm.usage_details for compatibility
139
157
  otelAttributes[OtelSpanAttributes.LLM_USAGE_DETAILS] =
140
158
  _serialize(usageDetails);
141
159
  }
@@ -143,6 +161,22 @@ export function createObservationAttributes(
143
161
  otelAttributes[OtelSpanAttributes.LLM_COMPLETION_START_TIME] =
144
162
  _serialize(completionStartTime);
145
163
  }
164
+ // Flatten LLM messages
165
+ if (inputMessages !== undefined && Array.isArray(inputMessages)) {
166
+ const messageAttrs = _flattenLLMMessages(inputMessages as LLMMessage[], "llm.input_messages");
167
+ Object.assign(otelAttributes, messageAttrs);
168
+ }
169
+ if (outputMessages !== undefined && Array.isArray(outputMessages)) {
170
+ const messageAttrs = _flattenLLMMessages(outputMessages as LLMMessage[], "llm.output_messages");
171
+ Object.assign(otelAttributes, messageAttrs);
172
+ }
173
+ // Add MIME types
174
+ if (inputMimeType !== undefined) {
175
+ otelAttributes[SemanticConventions.INPUT_MIME_TYPE] = String(inputMimeType);
176
+ }
177
+ if (outputMimeType !== undefined) {
178
+ otelAttributes[SemanticConventions.OUTPUT_MIME_TYPE] = String(outputMimeType);
179
+ }
146
180
  }
147
181
 
148
182
  // Embedding-specific attributes
@@ -156,6 +190,51 @@ export function createObservationAttributes(
156
190
  }
157
191
  }
158
192
 
193
+ // Tool-specific attributes
194
+ if (type === "tool") {
195
+ const toolName = (attributes as Record<string, unknown>)?.toolName ?? (attributes as Record<string, unknown>)?.tool_name;
196
+ const toolDescription = (attributes as Record<string, unknown>)?.toolDescription;
197
+ const toolParameters = (attributes as Record<string, unknown>)?.toolParameters;
198
+ const toolCall = (attributes as Record<string, unknown>)?.toolCall;
199
+
200
+ if (toolName !== undefined) {
201
+ otelAttributes[SemanticConventions.TOOL_NAME] = String(toolName);
202
+ }
203
+ if (toolDescription !== undefined) {
204
+ otelAttributes[SemanticConventions.TOOL_DESCRIPTION] = String(toolDescription);
205
+ }
206
+ if (toolParameters !== undefined) {
207
+ otelAttributes[SemanticConventions.TOOL_PARAMETERS] = _serialize(toolParameters);
208
+ }
209
+ // Flatten tool call if present
210
+ if (toolCall !== undefined && typeof toolCall === "object") {
211
+ const toolCallAttrs = _flattenToolCall(toolCall as ToolCall);
212
+ Object.assign(otelAttributes, toolCallAttrs);
213
+ }
214
+ }
215
+
216
+ // Agent-specific attributes
217
+ if (type === "agent") {
218
+ const agentName = (attributes as Record<string, unknown>)?.agentName;
219
+ if (agentName !== undefined) {
220
+ otelAttributes[SemanticConventions.AGENT_NAME] = String(agentName);
221
+ }
222
+ }
223
+
224
+ // Retriever-specific attributes
225
+ if (type === "retriever") {
226
+ const documents = (attributes as Record<string, unknown>)?.documents;
227
+ const query = (attributes as Record<string, unknown>)?.query;
228
+
229
+ if (documents !== undefined && Array.isArray(documents)) {
230
+ const docAttrs = _flattenDocuments(documents as Document[]);
231
+ Object.assign(otelAttributes, docAttrs);
232
+ }
233
+ if (query !== undefined) {
234
+ otelAttributes["retriever.query"] = String(query);
235
+ }
236
+ }
237
+
159
238
  // Add metadata (use OpenInference metadata convention)
160
239
  const metadataAttrs = _flattenAndSerializeMetadata(
161
240
  metadata,
@@ -163,13 +242,21 @@ export function createObservationAttributes(
163
242
  );
164
243
  Object.assign(otelAttributes, metadataAttrs);
165
244
 
166
- // Also add agkit.observation.metadata for compatibility
245
+ // Also add observation.metadata for compatibility
167
246
  const obsetvabilityMetadataAttrs = _flattenAndSerializeMetadata(
168
247
  metadata,
169
248
  OtelSpanAttributes.OBSERVATION_METADATA
170
249
  );
171
250
  Object.assign(otelAttributes, obsetvabilityMetadataAttrs);
172
251
 
252
+ // Pass through any additional custom attributes (e.g., agui.thread_id, agui.run_id)
253
+ // Only pass through attributes that haven't been set yet to avoid overwriting internal ones
254
+ for (const [key, value] of Object.entries(attributes)) {
255
+ if (!(key in otelAttributes) && value !== undefined && value !== null) {
256
+ otelAttributes[key] = typeof value === 'string' ? value : _serialize(value);
257
+ }
258
+ }
259
+
173
260
  // Filter out null/undefined values
174
261
  return Object.fromEntries(
175
262
  Object.entries(otelAttributes).filter(([_, v]) => v != null),
@@ -198,10 +285,10 @@ function _serialize(obj: unknown): string | undefined {
198
285
  *
199
286
  * Converts nested metadata objects into dot-notation attribute keys.
200
287
  * For example, `{ database: { host: 'localhost' } }` becomes
201
- * `{ 'metadata.database.host': 'localhost' }` (or 'agkit.observation.metadata.database.host').
288
+ * `{ 'metadata.database.host': 'localhost' }` (or 'observation.metadata.database.host').
202
289
  *
203
290
  * @param metadata - Metadata object to flatten
204
- * @param prefix - Attribute prefix (e.g., 'metadata' or 'agkit.observation.metadata')
291
+ * @param prefix - Attribute prefix (e.g., 'metadata' or 'observation.metadata')
205
292
  * @returns Flattened metadata attributes
206
293
  * @internal
207
294
  */
@@ -231,3 +318,161 @@ function _flattenAndSerializeMetadata(
231
318
 
232
319
  return metadataAttributes;
233
320
  }
321
+
322
+ /**
323
+ * Flattens LLM messages into OpenTelemetry attributes.
324
+ *
325
+ * Follows OpenInference convention:
326
+ * - llm.input_messages.{i}.message.role
327
+ * - llm.input_messages.{i}.message.content
328
+ * - llm.input_messages.{i}.message.tool_calls
329
+ *
330
+ * @param messages - Array of LLM messages
331
+ * @param prefix - Attribute prefix ('llm.input_messages' or 'llm.output_messages')
332
+ * @returns Flattened message attributes
333
+ * @internal
334
+ */
335
+ function _flattenLLMMessages(
336
+ messages: LLMMessage[] | undefined,
337
+ prefix: string,
338
+ ): Record<string, string> {
339
+ const attributes: Record<string, string> = {};
340
+
341
+ if (!messages || !Array.isArray(messages)) {
342
+ return attributes;
343
+ }
344
+
345
+ try {
346
+ messages.forEach((msg, index) => {
347
+ if (!msg || typeof msg !== "object") return;
348
+
349
+ const baseKey = `${prefix}.${index}.message`;
350
+
351
+ if (msg.role !== undefined) {
352
+ attributes[`${baseKey}.role`] = String(msg.role);
353
+ }
354
+ if (msg.content !== undefined) {
355
+ attributes[`${baseKey}.content`] = String(msg.content);
356
+ }
357
+ if (msg.toolCallId !== undefined) {
358
+ attributes[`${baseKey}.tool_call_id`] = String(msg.toolCallId);
359
+ }
360
+
361
+ // Flatten tool calls if present
362
+ if (msg.toolCalls && Array.isArray(msg.toolCalls)) {
363
+ msg.toolCalls.forEach((toolCall, tcIndex) => {
364
+ if (!toolCall || typeof toolCall !== "object") return;
365
+
366
+ const tcKey = `${baseKey}.tool_calls.${tcIndex}`;
367
+
368
+ if (toolCall.id !== undefined) {
369
+ attributes[`${tcKey}.id`] = String(toolCall.id);
370
+ }
371
+ if (toolCall.function !== undefined) {
372
+ if (toolCall.function.name !== undefined) {
373
+ attributes[`${tcKey}.function.name`] = String(toolCall.function.name);
374
+ }
375
+ if (toolCall.function.arguments !== undefined) {
376
+ attributes[`${tcKey}.function.arguments`] = String(toolCall.function.arguments);
377
+ }
378
+ }
379
+ });
380
+ }
381
+ });
382
+ } catch (e) {
383
+ // Silent fail - don't block business logic for observability
384
+ }
385
+
386
+ return attributes;
387
+ }
388
+
389
+ /**
390
+ * Flattens documents into OpenTelemetry attributes.
391
+ *
392
+ * Follows OpenInference convention:
393
+ * - retrieval.documents.{i}.document.id
394
+ * - retrieval.documents.{i}.document.content
395
+ * - retrieval.documents.{i}.document.score
396
+ * - retrieval.documents.{i}.document.metadata
397
+ *
398
+ * @param documents - Array of documents
399
+ * @returns Flattened document attributes
400
+ * @internal
401
+ */
402
+ function _flattenDocuments(
403
+ documents: Document[] | undefined,
404
+ ): Record<string, string> {
405
+ const attributes: Record<string, string> = {};
406
+
407
+ if (!documents || !Array.isArray(documents)) {
408
+ return attributes;
409
+ }
410
+
411
+ try {
412
+ documents.forEach((doc, index) => {
413
+ if (!doc || typeof doc !== "object") return;
414
+
415
+ const baseKey = `retrieval.documents.${index}.document`;
416
+
417
+ if (doc.id !== undefined) {
418
+ attributes[`${baseKey}.id`] = String(doc.id);
419
+ }
420
+ if (doc.content !== undefined) {
421
+ attributes[`${baseKey}.content`] = String(doc.content);
422
+ }
423
+ if (doc.score !== undefined) {
424
+ attributes[`${baseKey}.score`] = String(doc.score);
425
+ }
426
+ if (doc.metadata !== undefined && typeof doc.metadata === "object") {
427
+ const metadataSerialized = _serialize(doc.metadata);
428
+ if (metadataSerialized) {
429
+ attributes[`${baseKey}.metadata`] = metadataSerialized;
430
+ }
431
+ }
432
+ });
433
+ } catch (e) {
434
+ // Silent fail - don't block business logic for observability
435
+ }
436
+
437
+ return attributes;
438
+ }
439
+
440
+ /**
441
+ * Flattens tool call into OpenTelemetry attributes.
442
+ *
443
+ * Follows OpenInference convention:
444
+ * - tool_call.id
445
+ * - tool_call.function.name
446
+ * - tool_call.function.arguments
447
+ *
448
+ * @param toolCall - Tool call object
449
+ * @returns Flattened tool call attributes
450
+ * @internal
451
+ */
452
+ function _flattenToolCall(
453
+ toolCall: ToolCall | undefined,
454
+ ): Record<string, string> {
455
+ const attributes: Record<string, string> = {};
456
+
457
+ if (!toolCall || typeof toolCall !== "object") {
458
+ return attributes;
459
+ }
460
+
461
+ try {
462
+ if (toolCall.id !== undefined) {
463
+ attributes["tool_call.id"] = String(toolCall.id);
464
+ }
465
+ if (toolCall.function !== undefined) {
466
+ if (toolCall.function.name !== undefined) {
467
+ attributes["tool_call.function.name"] = String(toolCall.function.name);
468
+ }
469
+ if (toolCall.function.arguments !== undefined) {
470
+ attributes["tool_call.function.arguments"] = String(toolCall.function.arguments);
471
+ }
472
+ }
473
+ } catch (e) {
474
+ // Silent fail - don't block business logic for observability
475
+ }
476
+
477
+ return attributes;
478
+ }