@agentium/observability 1.0.0

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/index.js ADDED
@@ -0,0 +1,1333 @@
1
+ // src/exporters/callback.ts
2
+ var CallbackExporter = class {
3
+ name = "callback";
4
+ callback;
5
+ constructor(callback) {
6
+ this.callback = callback;
7
+ }
8
+ async export(trace) {
9
+ await this.callback(trace);
10
+ }
11
+ };
12
+
13
+ // src/exporters/console.ts
14
+ var C = {
15
+ reset: "\x1B[0m",
16
+ bold: "\x1B[1m",
17
+ dim: "\x1B[2m",
18
+ cyan: "\x1B[36m",
19
+ green: "\x1B[32m",
20
+ red: "\x1B[31m",
21
+ yellow: "\x1B[33m",
22
+ magenta: "\x1B[35m",
23
+ gray: "\x1B[90m",
24
+ brightCyan: "\x1B[96m",
25
+ brightGreen: "\x1B[92m"
26
+ };
27
+ function c(code, text) {
28
+ return `${code}${text}${C.reset}`;
29
+ }
30
+ function statusIcon(status) {
31
+ if (status === "ok") return c(C.green, "\u2713");
32
+ if (status === "error") return c(C.red, "\u2717");
33
+ return c(C.yellow, "\u22EF");
34
+ }
35
+ function kindColor(kind) {
36
+ switch (kind) {
37
+ case "agent":
38
+ return C.brightCyan;
39
+ case "llm":
40
+ return C.yellow;
41
+ case "tool":
42
+ return C.magenta;
43
+ case "handoff":
44
+ return C.brightGreen;
45
+ case "team":
46
+ return C.cyan;
47
+ default:
48
+ return C.gray;
49
+ }
50
+ }
51
+ var ConsoleExporter = class {
52
+ name = "console";
53
+ async export(trace) {
54
+ const line = c(C.dim, "\u2500".repeat(80));
55
+ console.log(`
56
+ ${line}`);
57
+ console.log(
58
+ ` ${c(C.bold + C.brightCyan, "Trace")} ${c(C.dim, trace.traceId)} ${c(C.dim, "duration=")}${c(C.brightGreen, `${trace.durationMs ?? "?"}ms`)}`
59
+ );
60
+ if (trace.metadata.agentName) {
61
+ console.log(` ${c(C.dim, "agent=")}${trace.metadata.agentName}`);
62
+ }
63
+ console.log(line);
64
+ const spanMap = /* @__PURE__ */ new Map();
65
+ for (const span of trace.spans) {
66
+ const parentId = span.parentSpanId ?? "__root__";
67
+ if (!spanMap.has(parentId)) spanMap.set(parentId, []);
68
+ spanMap.get(parentId).push(span);
69
+ }
70
+ const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
71
+ if (root) {
72
+ this.printSpan(root, spanMap, 0, trace.startTime);
73
+ }
74
+ console.log(line);
75
+ console.log("");
76
+ }
77
+ printSpan(span, childMap, depth, traceStart) {
78
+ const indent = ` ${"\u2502 ".repeat(depth)}`;
79
+ const connector = depth > 0 ? "\u251C\u2500 " : "";
80
+ const offset = span.startTime - traceStart;
81
+ const dur = span.durationMs ?? "?";
82
+ const kc = kindColor(span.kind);
83
+ const icon = statusIcon(span.status);
84
+ let line = `${indent}${connector}${icon} ${c(kc, span.name)}`;
85
+ line += ` ${c(C.dim, `[${offset}ms \u2192 +${dur}ms]`)}`;
86
+ if (span.attributes.tokens) {
87
+ line += ` ${c(C.brightGreen, `${span.attributes.tokens} tok`)}`;
88
+ }
89
+ if (span.attributes.toolName) {
90
+ line += ` ${c(C.dim, `(${span.attributes.toolName})`)}`;
91
+ }
92
+ if (span.attributes.cached) {
93
+ line += ` ${c(C.yellow, "[cached]")}`;
94
+ }
95
+ if (span.status === "error" && span.attributes.error) {
96
+ line += ` ${c(C.red, String(span.attributes.error))}`;
97
+ }
98
+ console.log(line);
99
+ for (const evt of span.events) {
100
+ console.log(
101
+ `${indent}\u2502 ${c(C.dim, `\u2937 ${evt.name}`)}${evt.attributes ? c(C.gray, ` ${JSON.stringify(evt.attributes)}`) : ""}`
102
+ );
103
+ }
104
+ const children = childMap.get(span.spanId) ?? [];
105
+ for (const child of children) {
106
+ this.printSpan(child, childMap, depth + 1, traceStart);
107
+ }
108
+ }
109
+ };
110
+
111
+ // src/exporters/json-file.ts
112
+ import { appendFile, writeFile } from "fs/promises";
113
+ var JsonFileExporter = class {
114
+ name = "json-file";
115
+ path;
116
+ mode;
117
+ pretty;
118
+ constructor(config) {
119
+ this.mode = config?.mode ?? "append";
120
+ this.path = config?.path ?? `traces-${Date.now()}.${this.mode === "append" ? "jsonl" : "json"}`;
121
+ this.pretty = config?.pretty ?? true;
122
+ }
123
+ async export(trace) {
124
+ const json = this.pretty ? JSON.stringify(trace, null, 2) : JSON.stringify(trace);
125
+ try {
126
+ if (this.mode === "append") {
127
+ await appendFile(this.path, `${json}
128
+ `);
129
+ } else {
130
+ await writeFile(this.path, json);
131
+ }
132
+ } catch (err) {
133
+ console.warn(
134
+ `[agentium/observability] Failed to write trace to ${this.path}:`,
135
+ err instanceof Error ? err.message : err
136
+ );
137
+ }
138
+ }
139
+ };
140
+
141
+ // src/exporters/langfuse.ts
142
+ var eventCounter = 0;
143
+ function eventId() {
144
+ return `evt_${Date.now().toString(36)}_${(eventCounter++).toString(36)}`;
145
+ }
146
+ function extractIO(span) {
147
+ const input = span.attributes.input ?? null;
148
+ const output = span.attributes.output ?? null;
149
+ return { input, output };
150
+ }
151
+ var LangfuseExporter = class {
152
+ name = "langfuse";
153
+ publicKey;
154
+ secretKey;
155
+ baseUrl;
156
+ constructor(config) {
157
+ this.publicKey = config?.publicKey ?? process.env.LANGFUSE_PUBLIC_KEY ?? "";
158
+ this.secretKey = config?.secretKey ?? process.env.LANGFUSE_SECRET_KEY ?? "";
159
+ this.baseUrl = (config?.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com").replace(
160
+ /\/$/,
161
+ ""
162
+ );
163
+ if (!this.publicKey || !this.secretKey) {
164
+ throw new Error(
165
+ "LangfuseExporter: missing credentials. Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY env vars, or pass them in config."
166
+ );
167
+ }
168
+ }
169
+ async fetchWithRetry(url, init, retries = 2) {
170
+ for (let attempt = 0; attempt <= retries; attempt++) {
171
+ try {
172
+ const controller = new AbortController();
173
+ const timeout = setTimeout(() => controller.abort(), 1e4);
174
+ const res = await fetch(url, { ...init, signal: controller.signal });
175
+ clearTimeout(timeout);
176
+ if (res.status >= 500 && attempt < retries) {
177
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
178
+ continue;
179
+ }
180
+ return res;
181
+ } catch (err) {
182
+ if (attempt === retries) throw err;
183
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
184
+ }
185
+ }
186
+ throw new Error("Unreachable");
187
+ }
188
+ async export(trace) {
189
+ const events = [];
190
+ const now = (/* @__PURE__ */ new Date()).toISOString();
191
+ const rootSpan = trace.spans.find((s) => s.spanId === trace.rootSpanId);
192
+ events.push({
193
+ id: eventId(),
194
+ type: "trace-create",
195
+ timestamp: now,
196
+ body: {
197
+ id: trace.traceId,
198
+ name: String(trace.metadata.agentName ?? "agent.run"),
199
+ input: trace.metadata.input ?? rootSpan?.attributes.input ?? null,
200
+ output: trace.metadata.output ?? rootSpan?.attributes.output ?? null,
201
+ metadata: { ...trace.metadata, input: void 0, output: void 0 },
202
+ timestamp: new Date(trace.startTime).toISOString()
203
+ }
204
+ });
205
+ for (const span of trace.spans) {
206
+ const { input, output } = extractIO(span);
207
+ const { input: _i, output: _o, ...restAttrs } = span.attributes;
208
+ if (span.kind === "llm" || span.name.startsWith("llm.")) {
209
+ events.push({
210
+ id: eventId(),
211
+ type: "generation-create",
212
+ timestamp: now,
213
+ body: {
214
+ id: span.spanId,
215
+ traceId: trace.traceId,
216
+ parentObservationId: span.parentSpanId,
217
+ name: span.name,
218
+ model: span.attributes.modelId ?? void 0,
219
+ input,
220
+ output,
221
+ startTime: new Date(span.startTime).toISOString(),
222
+ endTime: span.endTime ? new Date(span.endTime).toISOString() : void 0,
223
+ usage: {
224
+ promptTokens: span.attributes.promptTokens,
225
+ completionTokens: span.attributes.completionTokens,
226
+ totalTokens: span.attributes.tokens
227
+ },
228
+ metadata: {
229
+ ...restAttrs,
230
+ ...span.attributes.providerMetrics ? { providerMetrics: span.attributes.providerMetrics } : {}
231
+ }
232
+ }
233
+ });
234
+ } else {
235
+ events.push({
236
+ id: eventId(),
237
+ type: "span-create",
238
+ timestamp: now,
239
+ body: {
240
+ id: span.spanId,
241
+ traceId: trace.traceId,
242
+ parentObservationId: span.parentSpanId,
243
+ name: span.name,
244
+ input,
245
+ output,
246
+ startTime: new Date(span.startTime).toISOString(),
247
+ endTime: span.endTime ? new Date(span.endTime).toISOString() : void 0,
248
+ metadata: restAttrs,
249
+ level: span.status === "error" ? "ERROR" : "DEFAULT"
250
+ }
251
+ });
252
+ }
253
+ }
254
+ const auth = Buffer.from(`${this.publicKey}:${this.secretKey}`).toString("base64");
255
+ const res = await this.fetchWithRetry(`${this.baseUrl}/api/public/ingestion`, {
256
+ method: "POST",
257
+ headers: {
258
+ "Content-Type": "application/json",
259
+ Authorization: `Basic ${auth}`
260
+ },
261
+ body: JSON.stringify({ batch: events })
262
+ });
263
+ if (!res.ok && res.status !== 207) {
264
+ throw new Error(`Langfuse export failed: ${res.status} ${res.statusText}`);
265
+ }
266
+ if (res.status === 207) {
267
+ const body = await res.json();
268
+ if (body.errors && body.errors.length > 0) {
269
+ const realErrors = body.errors.filter((e) => e.status >= 400);
270
+ if (realErrors.length > 0) {
271
+ throw new Error(`Langfuse partial failure: ${JSON.stringify(realErrors[0])}`);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ };
277
+
278
+ // src/exporters/otel.ts
279
+ function parseEnvHeaders(raw) {
280
+ if (!raw) return {};
281
+ const result = {};
282
+ for (const pair of raw.split(",")) {
283
+ const [k, ...v] = pair.split("=");
284
+ if (k) result[k.trim()] = v.join("=").trim();
285
+ }
286
+ return result;
287
+ }
288
+ var OTelExporter = class {
289
+ name = "otel";
290
+ endpoint;
291
+ headers;
292
+ serviceName;
293
+ constructor(config) {
294
+ const ep = config?.endpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
295
+ if (!ep) {
296
+ throw new Error("OTelExporter: missing endpoint. Set OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass it in config.");
297
+ }
298
+ this.endpoint = ep.replace(/\/$/, "");
299
+ this.headers = config?.headers ?? parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);
300
+ this.serviceName = config?.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "agentium";
301
+ }
302
+ async fetchWithRetry(url, init, retries = 2) {
303
+ for (let attempt = 0; attempt <= retries; attempt++) {
304
+ try {
305
+ const controller = new AbortController();
306
+ const timeout = setTimeout(() => controller.abort(), 1e4);
307
+ const res = await fetch(url, { ...init, signal: controller.signal });
308
+ clearTimeout(timeout);
309
+ if (res.status >= 500 && attempt < retries) {
310
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
311
+ continue;
312
+ }
313
+ return res;
314
+ } catch (err) {
315
+ if (attempt === retries) throw err;
316
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
317
+ }
318
+ }
319
+ throw new Error("Unreachable");
320
+ }
321
+ async export(trace) {
322
+ const otlpSpans = trace.spans.map((span) => ({
323
+ traceId: this.padHex(span.traceId, 32),
324
+ spanId: this.padHex(span.spanId, 16),
325
+ parentSpanId: span.parentSpanId ? this.padHex(span.parentSpanId, 16) : void 0,
326
+ name: span.name,
327
+ kind: this.mapKind(span.kind),
328
+ startTimeUnixNano: String(span.startTime * 1e6),
329
+ endTimeUnixNano: span.endTime ? String(span.endTime * 1e6) : void 0,
330
+ status: {
331
+ code: span.status === "error" ? 2 : 1,
332
+ message: span.status === "error" ? String(span.attributes.error ?? "") : void 0
333
+ },
334
+ attributes: Object.entries(span.attributes).map(([key, value]) => ({
335
+ key,
336
+ value: this.toOtlpValue(value)
337
+ })),
338
+ events: span.events.map((evt) => ({
339
+ name: evt.name,
340
+ timeUnixNano: String(evt.timestamp * 1e6),
341
+ attributes: evt.attributes ? Object.entries(evt.attributes).map(([k, v]) => ({ key: k, value: this.toOtlpValue(v) })) : []
342
+ }))
343
+ }));
344
+ const payload = {
345
+ resourceSpans: [
346
+ {
347
+ resource: {
348
+ attributes: [{ key: "service.name", value: { stringValue: this.serviceName } }]
349
+ },
350
+ scopeSpans: [
351
+ {
352
+ scope: { name: "@agentium/observability" },
353
+ spans: otlpSpans
354
+ }
355
+ ]
356
+ }
357
+ ]
358
+ };
359
+ const url = `${this.endpoint}/v1/traces`;
360
+ const res = await this.fetchWithRetry(url, {
361
+ method: "POST",
362
+ headers: {
363
+ "Content-Type": "application/json",
364
+ ...this.headers
365
+ },
366
+ body: JSON.stringify(payload)
367
+ });
368
+ if (!res.ok) {
369
+ throw new Error(`OTLP export failed: ${res.status} ${res.statusText}`);
370
+ }
371
+ }
372
+ padHex(id, length) {
373
+ let hex = "";
374
+ for (let i = 0; i < id.length; i++) {
375
+ hex += id.charCodeAt(i).toString(16).padStart(2, "0");
376
+ }
377
+ return hex.padStart(length, "0").slice(-length);
378
+ }
379
+ mapKind(kind) {
380
+ switch (kind) {
381
+ case "agent":
382
+ return 1;
383
+ case "llm":
384
+ return 3;
385
+ case "tool":
386
+ return 3;
387
+ default:
388
+ return 0;
389
+ }
390
+ }
391
+ toOtlpValue(value) {
392
+ if (typeof value === "string") return { stringValue: value };
393
+ if (typeof value === "number")
394
+ return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
395
+ if (typeof value === "boolean") return { boolValue: value };
396
+ if (Array.isArray(value)) return { stringValue: JSON.stringify(value) };
397
+ return { stringValue: String(value) };
398
+ }
399
+ };
400
+
401
+ // src/metrics.ts
402
+ var MetricsCollector = class {
403
+ counters = {
404
+ runs_total: 0,
405
+ runs_success: 0,
406
+ runs_error: 0,
407
+ tool_calls_total: 0,
408
+ handoffs_total: 0,
409
+ cache_hits: 0,
410
+ cache_misses: 0
411
+ };
412
+ histograms = {
413
+ run_duration_ms: [],
414
+ tool_latency_ms: []
415
+ };
416
+ gauges = {
417
+ total_cost_usd: 0,
418
+ total_tokens: 0,
419
+ prompt_tokens: 0,
420
+ completion_tokens: 0,
421
+ reasoning_tokens: 0,
422
+ cached_tokens: 0,
423
+ audio_input_tokens: 0,
424
+ audio_output_tokens: 0
425
+ };
426
+ MAX_HISTOGRAM_SIZE = 1e4;
427
+ toolStartTimes = /* @__PURE__ */ new Map();
428
+ runStartTimes = /* @__PURE__ */ new Map();
429
+ listeners = [];
430
+ attach(eventBus) {
431
+ const on = (event, handler) => {
432
+ eventBus.on(event, handler);
433
+ this.listeners.push({ event, handler });
434
+ };
435
+ on("run.start", (data) => {
436
+ this.counters.runs_total++;
437
+ this.runStartTimes.set(data.runId, Date.now());
438
+ });
439
+ on("run.complete", (data) => {
440
+ this.counters.runs_success++;
441
+ const startTime = this.runStartTimes.get(data.runId);
442
+ if (startTime) {
443
+ const duration = Date.now() - startTime;
444
+ this.histograms.run_duration_ms.push(duration);
445
+ if (this.histograms.run_duration_ms.length > this.MAX_HISTOGRAM_SIZE) {
446
+ this.histograms.run_duration_ms = this.histograms.run_duration_ms.slice(-this.MAX_HISTOGRAM_SIZE);
447
+ }
448
+ this.runStartTimes.delete(data.runId);
449
+ }
450
+ if (data.output?.usage) {
451
+ this.gauges.total_tokens += data.output.usage.totalTokens ?? 0;
452
+ this.gauges.prompt_tokens += data.output.usage.promptTokens ?? 0;
453
+ this.gauges.completion_tokens += data.output.usage.completionTokens ?? 0;
454
+ this.gauges.reasoning_tokens += data.output.usage.reasoningTokens ?? 0;
455
+ this.gauges.cached_tokens += data.output.usage.cachedTokens ?? 0;
456
+ this.gauges.audio_input_tokens += data.output.usage.audioInputTokens ?? 0;
457
+ this.gauges.audio_output_tokens += data.output.usage.audioOutputTokens ?? 0;
458
+ }
459
+ for (const key of this.toolStartTimes.keys()) {
460
+ if (key.startsWith(`${data.runId}:`)) this.toolStartTimes.delete(key);
461
+ }
462
+ });
463
+ on("run.error", (data) => {
464
+ this.counters.runs_error++;
465
+ const startTime = this.runStartTimes.get(data.runId);
466
+ if (startTime) {
467
+ const duration = Date.now() - startTime;
468
+ this.histograms.run_duration_ms.push(duration);
469
+ if (this.histograms.run_duration_ms.length > this.MAX_HISTOGRAM_SIZE) {
470
+ this.histograms.run_duration_ms = this.histograms.run_duration_ms.slice(-this.MAX_HISTOGRAM_SIZE);
471
+ }
472
+ this.runStartTimes.delete(data.runId);
473
+ }
474
+ for (const key of this.toolStartTimes.keys()) {
475
+ if (key.startsWith(`${data.runId}:`)) this.toolStartTimes.delete(key);
476
+ }
477
+ });
478
+ on("tool.call", (data) => {
479
+ this.counters.tool_calls_total++;
480
+ this.toolStartTimes.set(`${data.runId}:${data.toolName}`, Date.now());
481
+ });
482
+ on("tool.result", (data) => {
483
+ const key = `${data.runId}:${data.toolName}`;
484
+ const start = this.toolStartTimes.get(key);
485
+ if (start) {
486
+ this.histograms.tool_latency_ms.push(Date.now() - start);
487
+ if (this.histograms.tool_latency_ms.length > this.MAX_HISTOGRAM_SIZE) {
488
+ this.histograms.tool_latency_ms = this.histograms.tool_latency_ms.slice(-this.MAX_HISTOGRAM_SIZE);
489
+ }
490
+ this.toolStartTimes.delete(key);
491
+ }
492
+ });
493
+ on("handoff.transfer", () => {
494
+ this.counters.handoffs_total++;
495
+ });
496
+ on("cache.hit", () => {
497
+ this.counters.cache_hits++;
498
+ });
499
+ on("cache.miss", () => {
500
+ this.counters.cache_misses++;
501
+ });
502
+ on("cost.tracked", (data) => {
503
+ const cost = data.cost ?? data.usage?.cost ?? 0;
504
+ if (cost) {
505
+ this.gauges.total_cost_usd += cost;
506
+ }
507
+ });
508
+ }
509
+ detach(eventBus) {
510
+ for (const { event, handler } of this.listeners) {
511
+ eventBus.off(event, handler);
512
+ }
513
+ this.listeners = [];
514
+ }
515
+ getMetrics() {
516
+ const totalCacheAttempts = this.counters.cache_hits + this.counters.cache_misses;
517
+ return {
518
+ counters: { ...this.counters },
519
+ histograms: {
520
+ run_duration_ms: [...this.histograms.run_duration_ms],
521
+ tool_latency_ms: [...this.histograms.tool_latency_ms]
522
+ },
523
+ gauges: { ...this.gauges },
524
+ rates: {
525
+ cache_hit_ratio: totalCacheAttempts > 0 ? this.counters.cache_hits / totalCacheAttempts : 0,
526
+ error_rate: this.counters.runs_total > 0 ? this.counters.runs_error / this.counters.runs_total : 0
527
+ },
528
+ timestamp: Date.now()
529
+ };
530
+ }
531
+ reset() {
532
+ this.counters = {
533
+ runs_total: 0,
534
+ runs_success: 0,
535
+ runs_error: 0,
536
+ tool_calls_total: 0,
537
+ handoffs_total: 0,
538
+ cache_hits: 0,
539
+ cache_misses: 0
540
+ };
541
+ this.histograms = {
542
+ run_duration_ms: [],
543
+ tool_latency_ms: []
544
+ };
545
+ this.gauges = {
546
+ total_cost_usd: 0,
547
+ total_tokens: 0,
548
+ prompt_tokens: 0,
549
+ completion_tokens: 0,
550
+ reasoning_tokens: 0,
551
+ cached_tokens: 0,
552
+ audio_input_tokens: 0,
553
+ audio_output_tokens: 0
554
+ };
555
+ this.toolStartTimes.clear();
556
+ this.runStartTimes.clear();
557
+ }
558
+ };
559
+
560
+ // src/structured-logger.ts
561
+ var StructuredLogger = class {
562
+ drain;
563
+ tracer;
564
+ listeners = [];
565
+ constructor(drain = "json", tracer) {
566
+ this.drain = drain;
567
+ this.tracer = tracer ?? null;
568
+ }
569
+ attach(eventBus) {
570
+ const on = (event, handler) => {
571
+ eventBus.on(event, handler);
572
+ this.listeners.push({ event, handler });
573
+ };
574
+ on("run.start", (data) => {
575
+ this.log(
576
+ "info",
577
+ "Run started",
578
+ data.agentName,
579
+ {
580
+ runId: data.runId,
581
+ input: data.input?.slice(0, 200) ?? ""
582
+ },
583
+ data.runId
584
+ );
585
+ });
586
+ on("run.complete", (data) => {
587
+ this.log(
588
+ "info",
589
+ "Run completed",
590
+ data.output?.agentName ?? void 0,
591
+ {
592
+ runId: data.runId,
593
+ tokens: data.output?.usage?.totalTokens,
594
+ promptTokens: data.output?.usage?.promptTokens,
595
+ completionTokens: data.output?.usage?.completionTokens,
596
+ reasoningTokens: data.output?.usage?.reasoningTokens,
597
+ durationMs: data.output?.durationMs,
598
+ providerMetrics: data.output?.usage?.providerMetrics
599
+ },
600
+ data.runId
601
+ );
602
+ });
603
+ on("run.error", (data) => {
604
+ this.log(
605
+ "error",
606
+ `Run failed: ${data.error?.message ?? "unknown error"}`,
607
+ void 0,
608
+ {
609
+ runId: data.runId,
610
+ error: data.error?.message ?? "unknown error",
611
+ stack: data.error?.stack?.split("\n").slice(0, 3).join("\n")
612
+ },
613
+ data.runId
614
+ );
615
+ });
616
+ on("tool.call", (data) => {
617
+ this.log(
618
+ "debug",
619
+ `Tool call: ${data.toolName}`,
620
+ void 0,
621
+ {
622
+ runId: data.runId,
623
+ toolName: data.toolName
624
+ },
625
+ data.runId
626
+ );
627
+ });
628
+ on("tool.result", (data) => {
629
+ this.log(
630
+ "debug",
631
+ `Tool result: ${data.toolName}`,
632
+ void 0,
633
+ {
634
+ runId: data.runId,
635
+ toolName: data.toolName
636
+ },
637
+ data.runId
638
+ );
639
+ });
640
+ on("handoff.transfer", (data) => {
641
+ this.log(
642
+ "info",
643
+ `Handoff: ${data.fromAgent} -> ${data.toAgent}`,
644
+ data.fromAgent,
645
+ {
646
+ runId: data.runId,
647
+ toAgent: data.toAgent,
648
+ reason: data.reason
649
+ },
650
+ data.runId
651
+ );
652
+ });
653
+ on("cache.hit", (data) => {
654
+ this.log("debug", "Cache hit", data.agentName, {
655
+ input: data.input?.slice(0, 100) ?? ""
656
+ });
657
+ });
658
+ on("cache.miss", (data) => {
659
+ this.log("debug", "Cache miss", data.agentName, {
660
+ input: data.input?.slice(0, 100) ?? ""
661
+ });
662
+ });
663
+ on("cost.tracked", (data) => {
664
+ this.log(
665
+ "info",
666
+ `Cost tracked: ${data.modelId}`,
667
+ data.agentName,
668
+ {
669
+ runId: data.runId,
670
+ modelId: data.modelId,
671
+ tokens: data.usage?.totalTokens,
672
+ promptTokens: data.usage?.promptTokens,
673
+ completionTokens: data.usage?.completionTokens,
674
+ reasoningTokens: data.usage?.reasoningTokens,
675
+ cachedTokens: data.usage?.cachedTokens,
676
+ audioInputTokens: data.usage?.audioInputTokens,
677
+ audioOutputTokens: data.usage?.audioOutputTokens
678
+ },
679
+ data.runId
680
+ );
681
+ });
682
+ }
683
+ detach(eventBus) {
684
+ for (const { event, handler } of this.listeners) {
685
+ eventBus.off(event, handler);
686
+ }
687
+ this.listeners = [];
688
+ }
689
+ log(level, message, agentName, attributes, runId) {
690
+ const traceId = runId && this.tracer ? this.tracer.getTraceByRunId(runId)?.traceId : void 0;
691
+ const entry = {
692
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
693
+ level,
694
+ message,
695
+ traceId,
696
+ agentName,
697
+ attributes
698
+ };
699
+ if (typeof this.drain === "function") {
700
+ this.drain(entry);
701
+ } else if (this.drain === "json") {
702
+ console.log(JSON.stringify(entry));
703
+ } else {
704
+ console.log(`[${entry.timestamp}] ${level.toUpperCase()} ${message}${traceId ? ` trace=${traceId}` : ""}`);
705
+ }
706
+ }
707
+ };
708
+
709
+ // src/tracer.ts
710
+ var idCounter = 0;
711
+ function genId() {
712
+ return `${Date.now().toString(36)}_${(idCounter++).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
713
+ }
714
+ var Tracer = class {
715
+ traces = /* @__PURE__ */ new Map();
716
+ runToTrace = /* @__PURE__ */ new Map();
717
+ runToRootSpan = /* @__PURE__ */ new Map();
718
+ activeSpans = /* @__PURE__ */ new Map();
719
+ exporters;
720
+ listeners = [];
721
+ pendingExports = [];
722
+ maxTraces = 1e3;
723
+ constructor(exporters = []) {
724
+ this.exporters = exporters;
725
+ }
726
+ attach(eventBus) {
727
+ const on = (event, handler) => {
728
+ eventBus.on(event, handler);
729
+ this.listeners.push({ event, handler });
730
+ };
731
+ on("run.start", (data) => {
732
+ const traceId = genId();
733
+ const inputText = data.input?.slice(0, 1e3) ?? "";
734
+ const span = this.startSpan(traceId, "agent.run", "agent", {
735
+ agentName: data.agentName,
736
+ input: inputText,
737
+ runId: data.runId
738
+ });
739
+ this.runToTrace.set(data.runId, traceId);
740
+ this.runToRootSpan.set(data.runId, span.spanId);
741
+ const trace = {
742
+ traceId,
743
+ spans: [span],
744
+ rootSpanId: span.spanId,
745
+ startTime: span.startTime,
746
+ metadata: { agentName: data.agentName, runId: data.runId, input: inputText }
747
+ };
748
+ this.traces.set(traceId, trace);
749
+ this.activeSpans.set(`${data.runId}:root`, span);
750
+ });
751
+ on("run.complete", (data) => {
752
+ const span = this.activeSpans.get(`${data.runId}:root`);
753
+ if (span) {
754
+ const outputText = data.output?.text?.slice(0, 2e3) ?? "";
755
+ span.attributes.output = outputText;
756
+ span.attributes.outputLength = data.output?.text?.length ?? 0;
757
+ span.attributes.tokens = data.output?.usage?.totalTokens ?? 0;
758
+ span.attributes.promptTokens = data.output?.usage?.promptTokens ?? 0;
759
+ span.attributes.completionTokens = data.output?.usage?.completionTokens ?? 0;
760
+ span.attributes.reasoningTokens = data.output?.usage?.reasoningTokens ?? 0;
761
+ span.attributes.cachedTokens = data.output?.usage?.cachedTokens ?? 0;
762
+ span.attributes.audioInputTokens = data.output?.usage?.audioInputTokens ?? 0;
763
+ span.attributes.audioOutputTokens = data.output?.usage?.audioOutputTokens ?? 0;
764
+ if (data.output?.usage?.providerMetrics) {
765
+ span.attributes.providerMetrics = data.output.usage.providerMetrics;
766
+ }
767
+ this.endSpan(span, "ok");
768
+ this.activeSpans.delete(`${data.runId}:root`);
769
+ const traceId = this.runToTrace.get(data.runId);
770
+ const trace = traceId ? this.traces.get(traceId) : void 0;
771
+ if (trace) {
772
+ trace.metadata.output = outputText;
773
+ }
774
+ this.finalizeTrace(data.runId);
775
+ }
776
+ });
777
+ on("run.error", (data) => {
778
+ const span = this.activeSpans.get(`${data.runId}:root`);
779
+ if (span) {
780
+ span.attributes.error = data.error.message;
781
+ this.endSpan(span, "error");
782
+ this.activeSpans.delete(`${data.runId}:root`);
783
+ this.finalizeTrace(data.runId);
784
+ }
785
+ });
786
+ on("tool.call", (data) => {
787
+ const traceId = this.runToTrace.get(data.runId);
788
+ const parentId = this.runToRootSpan.get(data.runId);
789
+ if (!traceId) return;
790
+ const argsStr = typeof data.args === "string" ? data.args : JSON.stringify(data.args ?? {});
791
+ const span = this.startSpan(
792
+ traceId,
793
+ `tool.${data.toolName}`,
794
+ "tool",
795
+ {
796
+ toolName: data.toolName,
797
+ runId: data.runId,
798
+ input: argsStr.slice(0, 1e3)
799
+ },
800
+ parentId
801
+ );
802
+ const trace = this.traces.get(traceId);
803
+ trace?.spans.push(span);
804
+ this.activeSpans.set(`${data.runId}:tool:${data.toolName}:${span.spanId}`, span);
805
+ });
806
+ on("tool.result", (data) => {
807
+ const prefix = `${data.runId}:tool:${data.toolName}:`;
808
+ let matchKey;
809
+ for (const key of this.activeSpans.keys()) {
810
+ if (key.startsWith(prefix)) {
811
+ matchKey = key;
812
+ break;
813
+ }
814
+ }
815
+ if (!matchKey) return;
816
+ const span = this.activeSpans.get(matchKey);
817
+ const resultStr = typeof data.result === "string" ? data.result : JSON.stringify(data.result ?? "");
818
+ span.attributes.output = resultStr.slice(0, 2e3);
819
+ span.attributes.resultLength = resultStr.length;
820
+ span.attributes.cached = resultStr.startsWith("[cached]");
821
+ this.endSpan(span, "ok");
822
+ this.activeSpans.delete(matchKey);
823
+ });
824
+ on("handoff.transfer", (data) => {
825
+ const traceId = this.runToTrace.get(data.runId);
826
+ const parentId = this.runToRootSpan.get(data.runId);
827
+ if (!traceId) return;
828
+ const span = this.startSpan(
829
+ traceId,
830
+ `handoff.${data.fromAgent}->${data.toAgent}`,
831
+ "handoff",
832
+ {
833
+ fromAgent: data.fromAgent,
834
+ toAgent: data.toAgent,
835
+ reason: data.reason
836
+ },
837
+ parentId
838
+ );
839
+ this.endSpan(span, "ok");
840
+ const trace = this.traces.get(traceId);
841
+ trace?.spans.push(span);
842
+ });
843
+ on("handoff.complete", (data) => {
844
+ const span = this.activeSpans.get(`${data.runId}:root`);
845
+ if (span) {
846
+ span.attributes.handoffChain = data.chain;
847
+ span.attributes.finalAgent = data.finalAgent;
848
+ }
849
+ });
850
+ on("team.delegate", (data) => {
851
+ const traceId = this.runToTrace.get(data.runId);
852
+ const parentId = this.runToRootSpan.get(data.runId);
853
+ if (!traceId) return;
854
+ const span = this.startSpan(
855
+ traceId,
856
+ `team.delegate.${data.memberId}`,
857
+ "team",
858
+ {
859
+ memberId: data.memberId,
860
+ task: data.task.slice(0, 200)
861
+ },
862
+ parentId
863
+ );
864
+ this.endSpan(span, "ok");
865
+ const trace = this.traces.get(traceId);
866
+ trace?.spans.push(span);
867
+ });
868
+ on("cache.hit", (data) => {
869
+ for (const [key, span] of this.activeSpans) {
870
+ if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
871
+ span.events.push({ name: "cache.hit", timestamp: Date.now(), attributes: { cachedId: data.cachedId } });
872
+ break;
873
+ }
874
+ }
875
+ });
876
+ on("cache.miss", (data) => {
877
+ for (const [key, span] of this.activeSpans) {
878
+ if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
879
+ span.events.push({ name: "cache.miss", timestamp: Date.now(), attributes: {} });
880
+ break;
881
+ }
882
+ }
883
+ });
884
+ on("cost.tracked", (data) => {
885
+ const span = this.activeSpans.get(`${data.runId}:root`);
886
+ if (span) {
887
+ span.attributes.modelId = data.modelId;
888
+ }
889
+ });
890
+ on("memory.extract", (data) => {
891
+ for (const [key, span] of this.activeSpans) {
892
+ if (key.endsWith(":root") && span.attributes.agentName === data.agentName) {
893
+ span.events.push({
894
+ name: "memory.extract",
895
+ timestamp: Date.now(),
896
+ attributes: { sessionId: data.sessionId, agentName: data.agentName }
897
+ });
898
+ break;
899
+ }
900
+ }
901
+ });
902
+ }
903
+ detach(eventBus) {
904
+ for (const { event, handler } of this.listeners) {
905
+ eventBus.off(event, handler);
906
+ }
907
+ this.listeners = [];
908
+ }
909
+ getTrace(traceId) {
910
+ return this.traces.get(traceId);
911
+ }
912
+ getTraceByRunId(runId) {
913
+ const traceId = this.runToTrace.get(runId);
914
+ return traceId ? this.traces.get(traceId) : void 0;
915
+ }
916
+ getAllTraces() {
917
+ return [...this.traces.values()];
918
+ }
919
+ clear() {
920
+ this.traces.clear();
921
+ this.runToTrace.clear();
922
+ this.runToRootSpan.clear();
923
+ this.activeSpans.clear();
924
+ }
925
+ async flush() {
926
+ await Promise.allSettled(this.pendingExports);
927
+ this.pendingExports = [];
928
+ for (const exporter of this.exporters) {
929
+ if (exporter.flush) await exporter.flush();
930
+ }
931
+ }
932
+ async shutdown() {
933
+ await this.flush();
934
+ for (const exporter of this.exporters) {
935
+ if (exporter.shutdown) await exporter.shutdown();
936
+ }
937
+ }
938
+ startSpan(traceId, name, kind, attributes, parentSpanId) {
939
+ return {
940
+ traceId,
941
+ spanId: genId(),
942
+ parentSpanId,
943
+ name,
944
+ kind,
945
+ startTime: Date.now(),
946
+ status: "running",
947
+ attributes,
948
+ events: []
949
+ };
950
+ }
951
+ endSpan(span, status) {
952
+ span.endTime = Date.now();
953
+ span.durationMs = span.endTime - span.startTime;
954
+ span.status = status;
955
+ }
956
+ finalizeTrace(runId) {
957
+ const traceId = this.runToTrace.get(runId);
958
+ if (!traceId) return;
959
+ const trace = this.traces.get(traceId);
960
+ if (!trace) return;
961
+ const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
962
+ if (root) {
963
+ trace.endTime = root.endTime;
964
+ trace.durationMs = root.durationMs;
965
+ }
966
+ const exportPromise = this.runExporters(trace);
967
+ this.pendingExports.push(exportPromise);
968
+ exportPromise.finally(() => {
969
+ const idx = this.pendingExports.indexOf(exportPromise);
970
+ if (idx >= 0) this.pendingExports.splice(idx, 1);
971
+ });
972
+ this.runToTrace.delete(runId);
973
+ this.runToRootSpan.delete(runId);
974
+ if (this.traces.size > this.maxTraces) {
975
+ const oldest = this.traces.keys().next().value;
976
+ if (oldest) this.traces.delete(oldest);
977
+ }
978
+ }
979
+ async runExporters(trace) {
980
+ for (const exporter of this.exporters) {
981
+ try {
982
+ await exporter.export(trace);
983
+ } catch (err) {
984
+ console.warn(
985
+ `[agentium/observability] Export failed (${exporter.name}):`,
986
+ err instanceof Error ? err.message : err
987
+ );
988
+ }
989
+ }
990
+ }
991
+ };
992
+
993
+ // src/instrument.ts
994
+ function resolveExporters(raw) {
995
+ if (!raw || raw.length === 0) return [];
996
+ return raw.map((e) => {
997
+ if (typeof e !== "string") return e;
998
+ const shorthand = {
999
+ console: () => new ConsoleExporter(),
1000
+ langfuse: () => new LangfuseExporter(),
1001
+ "json-file": () => new JsonFileExporter(),
1002
+ otel: () => new OTelExporter()
1003
+ };
1004
+ const factory = shorthand[e];
1005
+ if (!factory) throw new Error(`Unknown exporter: "${e}". Use "console", "langfuse", "json-file", or "otel".`);
1006
+ return factory();
1007
+ });
1008
+ }
1009
+ function buildResult(eventBus, config) {
1010
+ const exporters = resolveExporters(config?.exporters);
1011
+ const tracer = new Tracer(exporters);
1012
+ tracer.attach(eventBus);
1013
+ let metrics = null;
1014
+ if (config?.metrics !== false) {
1015
+ metrics = new MetricsCollector();
1016
+ metrics.attach(eventBus);
1017
+ }
1018
+ let logger = null;
1019
+ if (config?.structuredLogs) {
1020
+ const drain = config.structuredLogs === true ? "json" : config.structuredLogs;
1021
+ logger = new StructuredLogger(drain, tracer);
1022
+ logger.attach(eventBus);
1023
+ }
1024
+ const detach = () => {
1025
+ tracer.detach(eventBus);
1026
+ metrics?.detach(eventBus);
1027
+ logger?.detach(eventBus);
1028
+ };
1029
+ return { tracer, metrics, logger, detach };
1030
+ }
1031
+ function instrument(agent, config) {
1032
+ return buildResult(agent.eventBus, config);
1033
+ }
1034
+ function instrumentBus(eventBus, config) {
1035
+ return buildResult(eventBus, config);
1036
+ }
1037
+
1038
+ // src/metrics-exporter.ts
1039
+ var MetricsExporter = class {
1040
+ runs = [];
1041
+ toolUsage = {};
1042
+ runStartTimes = /* @__PURE__ */ new Map();
1043
+ runToolCounts = /* @__PURE__ */ new Map();
1044
+ listeners = [];
1045
+ subscribers = /* @__PURE__ */ new Set();
1046
+ maxRecords = 5e4;
1047
+ sessionCategories = {};
1048
+ estimatedKvCacheGb;
1049
+ attach(eventBus) {
1050
+ const on = (event, handler) => {
1051
+ eventBus.on(event, handler);
1052
+ this.listeners.push({ event, handler });
1053
+ };
1054
+ on("run.start", (data) => {
1055
+ this.runStartTimes.set(data.runId, { agentName: data.agentName, start: Date.now() });
1056
+ this.runToolCounts.set(data.runId, 0);
1057
+ this.emit({ type: "run.start", agentName: data.agentName, timestamp: Date.now(), data: { runId: data.runId } });
1058
+ });
1059
+ on("run.complete", (data) => {
1060
+ const info = this.runStartTimes.get(data.runId);
1061
+ if (!info) return;
1062
+ const duration = Date.now() - info.start;
1063
+ const usage = data.output?.usage;
1064
+ const tokens = usage?.totalTokens ?? 0;
1065
+ this.addRecord({
1066
+ agentName: info.agentName,
1067
+ durationMs: duration,
1068
+ tokens,
1069
+ promptTokens: usage?.promptTokens ?? 0,
1070
+ completionTokens: usage?.completionTokens ?? 0,
1071
+ reasoningTokens: usage?.reasoningTokens ?? 0,
1072
+ cachedTokens: usage?.cachedTokens ?? 0,
1073
+ audioInputTokens: usage?.audioInputTokens ?? 0,
1074
+ audioOutputTokens: usage?.audioOutputTokens ?? 0,
1075
+ providerMetrics: usage?.providerMetrics,
1076
+ cost: 0,
1077
+ toolCalls: this.runToolCounts.get(data.runId) ?? 0,
1078
+ success: true,
1079
+ timestamp: Date.now()
1080
+ });
1081
+ this.runStartTimes.delete(data.runId);
1082
+ this.runToolCounts.delete(data.runId);
1083
+ this.emit({
1084
+ type: "run.complete",
1085
+ agentName: info.agentName,
1086
+ timestamp: Date.now(),
1087
+ data: { durationMs: duration, tokens }
1088
+ });
1089
+ });
1090
+ on("run.error", (data) => {
1091
+ const info = this.runStartTimes.get(data.runId);
1092
+ if (!info) return;
1093
+ const duration = Date.now() - info.start;
1094
+ this.addRecord({
1095
+ agentName: info.agentName,
1096
+ durationMs: duration,
1097
+ tokens: 0,
1098
+ promptTokens: 0,
1099
+ completionTokens: 0,
1100
+ reasoningTokens: 0,
1101
+ cachedTokens: 0,
1102
+ audioInputTokens: 0,
1103
+ audioOutputTokens: 0,
1104
+ cost: 0,
1105
+ toolCalls: this.runToolCounts.get(data.runId) ?? 0,
1106
+ success: false,
1107
+ timestamp: Date.now()
1108
+ });
1109
+ this.runStartTimes.delete(data.runId);
1110
+ this.runToolCounts.delete(data.runId);
1111
+ this.emit({
1112
+ type: "run.error",
1113
+ agentName: info.agentName,
1114
+ timestamp: Date.now(),
1115
+ data: { durationMs: duration }
1116
+ });
1117
+ });
1118
+ on("tool.call", (data) => {
1119
+ const info = this.runStartTimes.get(data.runId);
1120
+ const agentName = info?.agentName ?? "unknown";
1121
+ this.runToolCounts.set(data.runId, (this.runToolCounts.get(data.runId) ?? 0) + 1);
1122
+ if (!this.toolUsage[agentName]) this.toolUsage[agentName] = {};
1123
+ this.toolUsage[agentName][data.toolName] = (this.toolUsage[agentName][data.toolName] ?? 0) + 1;
1124
+ });
1125
+ on("cost.tracked", (data) => {
1126
+ for (let i = this.runs.length - 1; i >= 0; i--) {
1127
+ if (this.runs[i].agentName === data.agentName) {
1128
+ if (data.usage?.cost) this.runs[i].cost = data.usage.cost;
1129
+ break;
1130
+ }
1131
+ }
1132
+ });
1133
+ on("capacity.session.classified", (data) => {
1134
+ this.sessionCategories[data.category] = (this.sessionCategories[data.category] ?? 0) + 1;
1135
+ });
1136
+ on("capacity.warning", (data) => {
1137
+ if (data.estimatedKvGb !== void 0) {
1138
+ this.estimatedKvCacheGb = data.estimatedKvGb;
1139
+ }
1140
+ });
1141
+ }
1142
+ detach(eventBus) {
1143
+ for (const { event, handler } of this.listeners) {
1144
+ eventBus.off(event, handler);
1145
+ }
1146
+ this.listeners = [];
1147
+ }
1148
+ addRecord(record) {
1149
+ this.runs.push(record);
1150
+ if (this.runs.length > this.maxRecords) {
1151
+ this.runs = this.runs.slice(-this.maxRecords);
1152
+ }
1153
+ }
1154
+ emit(event) {
1155
+ for (const sub of this.subscribers) {
1156
+ try {
1157
+ sub(event);
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ }
1162
+ getMetrics(agentName) {
1163
+ const filtered = agentName ? this.runs.filter((r) => r.agentName === agentName) : this.runs;
1164
+ const successful = filtered.filter((r) => r.success);
1165
+ const durations = filtered.map((r) => r.durationMs).sort((a, b) => a - b);
1166
+ const totalRuns = filtered.length;
1167
+ const errors = filtered.filter((r) => !r.success).length;
1168
+ const totalTokens = filtered.reduce((s, r) => s + r.tokens, 0);
1169
+ const promptTokens = filtered.reduce((s, r) => s + r.promptTokens, 0);
1170
+ const completionTokens = filtered.reduce((s, r) => s + r.completionTokens, 0);
1171
+ const reasoningTokens = filtered.reduce((s, r) => s + r.reasoningTokens, 0);
1172
+ const cachedTokens = filtered.reduce((s, r) => s + r.cachedTokens, 0);
1173
+ const audioInputTokens = filtered.reduce((s, r) => s + r.audioInputTokens, 0);
1174
+ const audioOutputTokens = filtered.reduce((s, r) => s + r.audioOutputTokens, 0);
1175
+ const totalCost = filtered.reduce((s, r) => s + r.cost, 0);
1176
+ const totalToolCalls = filtered.reduce((s, r) => s + r.toolCalls, 0);
1177
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
1178
+ const p95Idx = Math.floor(durations.length * 0.95);
1179
+ const p95 = durations.length > 0 ? durations[Math.min(p95Idx, durations.length - 1)] : 0;
1180
+ const toolFreq = {};
1181
+ if (agentName && this.toolUsage[agentName]) {
1182
+ Object.assign(toolFreq, this.toolUsage[agentName]);
1183
+ } else {
1184
+ for (const usage of Object.values(this.toolUsage)) {
1185
+ for (const [tool, count] of Object.entries(usage)) {
1186
+ toolFreq[tool] = (toolFreq[tool] ?? 0) + count;
1187
+ }
1188
+ }
1189
+ }
1190
+ const avgContextLength = successful.length > 0 ? Math.round(promptTokens / successful.length) : 0;
1191
+ return {
1192
+ runs: totalRuns,
1193
+ errors,
1194
+ avgDurationMs: Math.round(avgDuration),
1195
+ p95DurationMs: Math.round(p95),
1196
+ totalCost,
1197
+ totalTokens,
1198
+ promptTokens,
1199
+ completionTokens,
1200
+ reasoningTokens,
1201
+ cachedTokens,
1202
+ audioInputTokens,
1203
+ audioOutputTokens,
1204
+ toolCallCount: totalToolCalls,
1205
+ toolUsageFrequency: toolFreq,
1206
+ errorRate: totalRuns > 0 ? errors / totalRuns : 0,
1207
+ tokensPerRun: successful.length > 0 ? Math.round(totalTokens / successful.length) : 0,
1208
+ avgContextLength,
1209
+ sessionCategories: this.sessionCategories,
1210
+ estimatedKvCacheGb: this.estimatedKvCacheGb
1211
+ };
1212
+ }
1213
+ toPrometheus() {
1214
+ const lines = [];
1215
+ const allAgents = new Set(this.runs.map((r) => r.agentName));
1216
+ lines.push("# HELP agentium_agent_runs_total Total agent runs");
1217
+ lines.push("# TYPE agentium_agent_runs_total counter");
1218
+ for (const agent of allAgents) {
1219
+ const m = this.getMetrics(agent);
1220
+ lines.push(`agentium_agent_runs_total{agent="${agent}"} ${m.runs}`);
1221
+ }
1222
+ lines.push("# HELP agentium_agent_errors_total Total agent errors");
1223
+ lines.push("# TYPE agentium_agent_errors_total counter");
1224
+ for (const agent of allAgents) {
1225
+ const m = this.getMetrics(agent);
1226
+ lines.push(`agentium_agent_errors_total{agent="${agent}"} ${m.errors}`);
1227
+ }
1228
+ lines.push("# HELP agentium_agent_duration_ms_avg Average run duration in ms");
1229
+ lines.push("# TYPE agentium_agent_duration_ms_avg gauge");
1230
+ for (const agent of allAgents) {
1231
+ const m = this.getMetrics(agent);
1232
+ lines.push(`agentium_agent_duration_ms_avg{agent="${agent}"} ${m.avgDurationMs}`);
1233
+ }
1234
+ lines.push("# HELP agentium_agent_duration_ms_p95 P95 run duration in ms");
1235
+ lines.push("# TYPE agentium_agent_duration_ms_p95 gauge");
1236
+ for (const agent of allAgents) {
1237
+ const m = this.getMetrics(agent);
1238
+ lines.push(`agentium_agent_duration_ms_p95{agent="${agent}"} ${m.p95DurationMs}`);
1239
+ }
1240
+ lines.push("# HELP agentium_agent_tokens_total Total tokens consumed");
1241
+ lines.push("# TYPE agentium_agent_tokens_total counter");
1242
+ for (const agent of allAgents) {
1243
+ const m = this.getMetrics(agent);
1244
+ lines.push(`agentium_agent_tokens_total{agent="${agent}"} ${m.totalTokens}`);
1245
+ }
1246
+ lines.push("# HELP agentium_agent_cost_usd_total Total cost in USD");
1247
+ lines.push("# TYPE agentium_agent_cost_usd_total counter");
1248
+ for (const agent of allAgents) {
1249
+ const m = this.getMetrics(agent);
1250
+ lines.push(`agentium_agent_cost_usd_total{agent="${agent}"} ${m.totalCost}`);
1251
+ }
1252
+ lines.push("# HELP agentium_agent_tool_calls_total Total tool calls");
1253
+ lines.push("# TYPE agentium_agent_tool_calls_total counter");
1254
+ for (const agent of allAgents) {
1255
+ const m = this.getMetrics(agent);
1256
+ lines.push(`agentium_agent_tool_calls_total{agent="${agent}"} ${m.toolCallCount}`);
1257
+ }
1258
+ if (this.estimatedKvCacheGb !== void 0) {
1259
+ lines.push("# HELP agentium_kv_cache_estimated_gb Estimated KV cache size in GB");
1260
+ lines.push("# TYPE agentium_kv_cache_estimated_gb gauge");
1261
+ lines.push(`agentium_kv_cache_estimated_gb ${this.estimatedKvCacheGb}`);
1262
+ }
1263
+ const categories = Object.entries(this.sessionCategories);
1264
+ if (categories.length > 0) {
1265
+ lines.push("# HELP agentium_session_category_total Sessions by category");
1266
+ lines.push("# TYPE agentium_session_category_total counter");
1267
+ for (const [category, count] of categories) {
1268
+ lines.push(`agentium_session_category_total{category="${category}"} ${count}`);
1269
+ }
1270
+ const totalSessions = categories.reduce((s, [, c2]) => s + c2, 0);
1271
+ lines.push("# HELP agentium_capacity_sessions_total Total tracked sessions");
1272
+ lines.push("# TYPE agentium_capacity_sessions_total counter");
1273
+ lines.push(`agentium_capacity_sessions_total ${totalSessions}`);
1274
+ }
1275
+ return `${lines.join("\n")}
1276
+ `;
1277
+ }
1278
+ toJSON() {
1279
+ const allAgents = new Set(this.runs.map((r) => r.agentName));
1280
+ const byAgent = {};
1281
+ for (const agent of allAgents) {
1282
+ byAgent[agent] = this.getMetrics(agent);
1283
+ }
1284
+ return {
1285
+ global: this.getMetrics(),
1286
+ byAgent,
1287
+ timestamp: Date.now()
1288
+ };
1289
+ }
1290
+ async *stream() {
1291
+ const queue = [];
1292
+ let resolve = null;
1293
+ const handler = (event) => {
1294
+ queue.push(event);
1295
+ if (resolve) {
1296
+ resolve();
1297
+ resolve = null;
1298
+ }
1299
+ };
1300
+ this.subscribers.add(handler);
1301
+ try {
1302
+ while (true) {
1303
+ while (queue.length > 0) {
1304
+ yield queue.shift();
1305
+ }
1306
+ await new Promise((r) => {
1307
+ resolve = r;
1308
+ });
1309
+ }
1310
+ } finally {
1311
+ this.subscribers.delete(handler);
1312
+ }
1313
+ }
1314
+ reset() {
1315
+ this.runs = [];
1316
+ this.toolUsage = {};
1317
+ this.runStartTimes.clear();
1318
+ this.runToolCounts.clear();
1319
+ }
1320
+ };
1321
+ export {
1322
+ CallbackExporter,
1323
+ ConsoleExporter,
1324
+ JsonFileExporter,
1325
+ LangfuseExporter,
1326
+ MetricsCollector,
1327
+ MetricsExporter,
1328
+ OTelExporter,
1329
+ StructuredLogger,
1330
+ Tracer,
1331
+ instrument,
1332
+ instrumentBus
1333
+ };