@tangle-network/sdk-telemetry 0.1.8

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.mjs ADDED
@@ -0,0 +1,2170 @@
1
+ import { PROVISION_TRACE_ID_HEADER, PTID_PREFIX, SpanRecorder, isAdoptablePtid, mintPtid, monotonicNs, parseServerTiming, readOrMintPtid, spanDurationMs } from "./stage-span.mjs";
2
+ import { GEN_AI_CONVERSATION_ID, GEN_AI_CONVERSATION_ID as GEN_AI_CONVERSATION_ID$1, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OPERATION_NAME, GEN_AI_OPERATION_NAME as GEN_AI_OPERATION_NAME$1, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_REQUEST_MODEL as GEN_AI_REQUEST_MODEL$1, GEN_AI_RESPONSE_MODEL, GEN_AI_RESPONSE_MODEL as GEN_AI_RESPONSE_MODEL$1, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS as GEN_AI_USAGE_INPUT_TOKENS$1, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS as GEN_AI_USAGE_OUTPUT_TOKENS$1, SSEChunkParser, StreamUsageExtractor, TOKEN_USAGE_COST_KEYS, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, addTokenUsage, consumeSSEStream, createStreamUsageExtractor, createUsageCallback, firstTokenCount, firstUsageCostUsd, genAiUsageAttributes, isFilePart, isReasoningPart, isTextPart, isToolPart, parseSSEData as parseSSEData$1, parseSSEStream, readTokenCostUsd, readTokenUsage, tokenCount, tokenUsageSource } from "@tangle-network/agent-core";
3
+ //#region src/credits.ts
4
+ const DEFAULT_CONFIG$2 = {
5
+ refreshIntervalMs: 6e4,
6
+ warningThreshold: .2,
7
+ criticalThreshold: .05
8
+ };
9
+ var CreditTracker = class {
10
+ config;
11
+ provider;
12
+ storage;
13
+ cachedBalance;
14
+ lastRefresh;
15
+ refreshTimer;
16
+ reservations = /* @__PURE__ */ new Map();
17
+ constructor(config, provider, storage) {
18
+ this.config = {
19
+ ...DEFAULT_CONFIG$2,
20
+ ...config
21
+ };
22
+ this.provider = provider;
23
+ this.storage = storage;
24
+ if (this.config.refreshIntervalMs > 0) this.startRefreshTimer();
25
+ }
26
+ async getBalance(forceRefresh = false) {
27
+ if (forceRefresh || !this.cachedBalance || this.isStale()) {
28
+ this.cachedBalance = await this.provider.getBalance();
29
+ this.lastRefresh = /* @__PURE__ */ new Date();
30
+ this.checkThresholds(this.cachedBalance);
31
+ if (this.storage) await this.storage.set(`credits:${this.config.projectRef}:balance`, this.cachedBalance);
32
+ this.config.onBalanceUpdate?.(this.cachedBalance);
33
+ }
34
+ return this.cachedBalance;
35
+ }
36
+ async getTransactions(options) {
37
+ return this.provider.getTransactions(options);
38
+ }
39
+ async hasCredits(amount) {
40
+ const balance = await this.getBalance();
41
+ const reservedTotal = this.getReservedTotal();
42
+ return balance.available - reservedTotal >= amount;
43
+ }
44
+ async reserve(amount, options) {
45
+ if (!await this.hasCredits(amount)) return null;
46
+ const reservationId = await this.provider.reserveCredit(amount);
47
+ if (!reservationId) return null;
48
+ const now = /* @__PURE__ */ new Date();
49
+ this.reservations.set(reservationId, {
50
+ amount,
51
+ createdAt: now,
52
+ expiresAt: options?.ttlMs ? new Date(now.getTime() + options.ttlMs) : void 0
53
+ });
54
+ return reservationId;
55
+ }
56
+ async release(reservationId) {
57
+ await this.provider.releaseReservation(reservationId);
58
+ this.reservations.delete(reservationId);
59
+ }
60
+ async charge(reservationId, actualAmount) {
61
+ const transaction = await this.provider.chargeReservation(reservationId, actualAmount);
62
+ this.reservations.delete(reservationId);
63
+ await this.getBalance(true);
64
+ return transaction;
65
+ }
66
+ async chargeImmediate(amount, _description) {
67
+ const reservationId = await this.reserve(amount);
68
+ if (!reservationId) return null;
69
+ return this.charge(reservationId, amount);
70
+ }
71
+ getReservedTotal() {
72
+ this.cleanupExpiredReservations();
73
+ let total = 0;
74
+ for (const reservation of this.reservations.values()) total += reservation.amount;
75
+ return total;
76
+ }
77
+ getEffectiveBalance() {
78
+ const balance = this.cachedBalance;
79
+ if (!balance) return 0;
80
+ return balance.available - this.getReservedTotal();
81
+ }
82
+ getUsagePercent() {
83
+ const balance = this.cachedBalance;
84
+ if (!balance || balance.limit === 0) return 0;
85
+ return balance.used / balance.limit * 100;
86
+ }
87
+ getRemainingPercent() {
88
+ const balance = this.cachedBalance;
89
+ if (!balance || balance.limit === 0) return 100;
90
+ return balance.available / balance.limit * 100;
91
+ }
92
+ isLow() {
93
+ return this.getRemainingPercent() / 100 <= this.config.warningThreshold;
94
+ }
95
+ isCritical() {
96
+ return this.getRemainingPercent() / 100 <= this.config.criticalThreshold;
97
+ }
98
+ async close() {
99
+ this.stopRefreshTimer();
100
+ for (const [id] of this.reservations) try {
101
+ await this.release(id);
102
+ } catch {}
103
+ }
104
+ isStale() {
105
+ if (!this.lastRefresh) return true;
106
+ return Date.now() - this.lastRefresh.getTime() > this.config.refreshIntervalMs;
107
+ }
108
+ checkThresholds(balance) {
109
+ if (!balance.limit || balance.limit <= 0) return;
110
+ const remainingPercent = balance.available / balance.limit;
111
+ if (remainingPercent <= this.config.criticalThreshold) this.config.onCriticalBalance?.(balance);
112
+ else if (remainingPercent <= this.config.warningThreshold) this.config.onLowBalance?.(balance);
113
+ }
114
+ cleanupExpiredReservations() {
115
+ const now = Date.now();
116
+ for (const [id, reservation] of this.reservations) if (reservation.expiresAt && reservation.expiresAt.getTime() < now) this.reservations.delete(id);
117
+ }
118
+ startRefreshTimer() {
119
+ if (this.refreshTimer) return;
120
+ this.refreshTimer = setInterval(() => this.getBalance(true), this.config.refreshIntervalMs);
121
+ }
122
+ stopRefreshTimer() {
123
+ if (this.refreshTimer) {
124
+ clearInterval(this.refreshTimer);
125
+ this.refreshTimer = void 0;
126
+ }
127
+ }
128
+ };
129
+ var MemoryCreditProvider = class {
130
+ balance;
131
+ transactions = [];
132
+ reservations = /* @__PURE__ */ new Map();
133
+ constructor(initialBalance = {}) {
134
+ this.balance = {
135
+ available: initialBalance.available ?? 1e3,
136
+ used: initialBalance.used ?? 0,
137
+ limit: initialBalance.limit ?? 1e3,
138
+ currency: initialBalance.currency ?? "credits"
139
+ };
140
+ }
141
+ async getBalance() {
142
+ return { ...this.balance };
143
+ }
144
+ async getTransactions(options) {
145
+ let result = [...this.transactions];
146
+ if (options?.since) {
147
+ const since = options.since;
148
+ result = result.filter((t) => t.timestamp >= since);
149
+ }
150
+ result = result.slice(options?.offset ?? 0, (options?.offset ?? 0) + (options?.limit ?? 100));
151
+ return result;
152
+ }
153
+ async checkCredit(amount) {
154
+ const reservedTotal = Array.from(this.reservations.values()).reduce((sum, amt) => sum + amt, 0);
155
+ return this.balance.available - reservedTotal >= amount;
156
+ }
157
+ async reserveCredit(amount) {
158
+ if (!await this.checkCredit(amount)) return null;
159
+ const id = `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
160
+ this.reservations.set(id, amount);
161
+ return id;
162
+ }
163
+ async releaseReservation(reservationId) {
164
+ this.reservations.delete(reservationId);
165
+ }
166
+ async chargeReservation(reservationId, actualAmount) {
167
+ const reserved = this.reservations.get(reservationId);
168
+ if (reserved === void 0) throw new Error(`Reservation not found: ${reservationId}`);
169
+ const chargeAmount = actualAmount ?? reserved;
170
+ this.reservations.delete(reservationId);
171
+ this.balance.available -= chargeAmount;
172
+ this.balance.used += chargeAmount;
173
+ const transaction = {
174
+ id: `txn-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
175
+ type: "charge",
176
+ amount: chargeAmount,
177
+ balance: this.balance.available,
178
+ description: `Charged from reservation ${reservationId}`,
179
+ timestamp: /* @__PURE__ */ new Date()
180
+ };
181
+ this.transactions.unshift(transaction);
182
+ return transaction;
183
+ }
184
+ addCredits(amount, description) {
185
+ this.balance.available += amount;
186
+ this.balance.limit += amount;
187
+ const transaction = {
188
+ id: `txn-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
189
+ type: "grant",
190
+ amount,
191
+ balance: this.balance.available,
192
+ description,
193
+ timestamp: /* @__PURE__ */ new Date()
194
+ };
195
+ this.transactions.unshift(transaction);
196
+ return transaction;
197
+ }
198
+ };
199
+ function createCreditTracker(config, provider, storage) {
200
+ return new CreditTracker(config, provider, storage);
201
+ }
202
+ //#endregion
203
+ //#region src/internal/best-effort.ts
204
+ /**
205
+ * Local best-effort helpers. Mirrors `@repo/shared/best-effort` but kept
206
+ * inline so this published SDK package has no private workspace deps.
207
+ */
208
+ function bestEffort(fn) {
209
+ try {
210
+ return fn();
211
+ } catch {
212
+ return;
213
+ }
214
+ }
215
+ //#endregion
216
+ //#region src/enrichment/types.ts
217
+ /** Create an empty enrichment context with defaults */
218
+ function createEmptyContext() {
219
+ return { customLabels: {} };
220
+ }
221
+ //#endregion
222
+ //#region src/enrichment/adapter-registry.ts
223
+ /**
224
+ * Adapter Registry
225
+ *
226
+ * Registry for metadata blob adapters. Adapters are tried in order of registration
227
+ * until one can parse the blob. Default JSON adapter is always available.
228
+ */
229
+ /** Registry for metadata blob adapters */
230
+ var AdapterRegistry = class {
231
+ adapters = [];
232
+ /** Register an adapter (added to end of chain) */
233
+ register(adapter) {
234
+ if (this.adapters.find((a) => a.name === adapter.name)) throw new Error(`Adapter "${adapter.name}" already registered`);
235
+ this.adapters.push(adapter);
236
+ }
237
+ /** Unregister an adapter by name */
238
+ unregister(name) {
239
+ const idx = this.adapters.findIndex((a) => a.name === name);
240
+ if (idx >= 0) {
241
+ this.adapters.splice(idx, 1);
242
+ return true;
243
+ }
244
+ return false;
245
+ }
246
+ /** Get all registered adapter names */
247
+ getAdapterNames() {
248
+ return this.adapters.map((a) => a.name);
249
+ }
250
+ /**
251
+ * Parse a metadata blob using registered adapters.
252
+ * Returns enrichment context with parsed fields.
253
+ * Stores original blob in rawBlob for audit.
254
+ */
255
+ parse(blob) {
256
+ const context = createEmptyContext();
257
+ context.rawBlob = blob;
258
+ for (const adapter of this.adapters) if (adapter.canParse(blob)) {
259
+ const parsed = bestEffort(() => adapter.parse(blob));
260
+ if (parsed) return {
261
+ ...context,
262
+ ...parsed,
263
+ customLabels: {
264
+ ...context.customLabels,
265
+ ...parsed.customLabels || {}
266
+ },
267
+ rawBlob: blob
268
+ };
269
+ }
270
+ return context;
271
+ }
272
+ /** Check if any adapter can parse the given blob */
273
+ canParse(blob) {
274
+ return this.adapters.some((a) => a.canParse(blob));
275
+ }
276
+ };
277
+ /** Default global registry instance */
278
+ let defaultRegistry = null;
279
+ /** Get or create the default adapter registry */
280
+ function getDefaultRegistry() {
281
+ if (!defaultRegistry) defaultRegistry = new AdapterRegistry();
282
+ return defaultRegistry;
283
+ }
284
+ /** Reset the default registry (for testing) */
285
+ function resetDefaultRegistry() {
286
+ defaultRegistry = null;
287
+ }
288
+ //#endregion
289
+ //#region src/enrichment/json-adapter.ts
290
+ const VALID_PRODUCTS = [
291
+ "vibe_coding",
292
+ "simulation",
293
+ "background_agent"
294
+ ];
295
+ const VALID_ENVIRONMENTS = [
296
+ "development",
297
+ "staging",
298
+ "production"
299
+ ];
300
+ const VALID_TIERS = [
301
+ "free",
302
+ "pro",
303
+ "enterprise"
304
+ ];
305
+ function normalizeProduct(value) {
306
+ if (typeof value !== "string") return void 0;
307
+ const normalized = value.toLowerCase().replace(/-/g, "_");
308
+ return VALID_PRODUCTS.includes(normalized) ? normalized : void 0;
309
+ }
310
+ function normalizeEnvironment(value) {
311
+ if (typeof value !== "string") return void 0;
312
+ const normalized = value.toLowerCase();
313
+ if (normalized === "dev") return "development";
314
+ if (normalized === "prod") return "production";
315
+ return VALID_ENVIRONMENTS.includes(normalized) ? normalized : void 0;
316
+ }
317
+ function normalizeTier(value) {
318
+ if (typeof value !== "string") return void 0;
319
+ const normalized = value.toLowerCase();
320
+ return VALID_TIERS.includes(normalized) ? normalized : void 0;
321
+ }
322
+ /** JSON adapter for parsing metadata blobs */
323
+ const jsonAdapter = {
324
+ name: "json",
325
+ canParse(blob) {
326
+ return blob.format === "json";
327
+ },
328
+ parse(blob) {
329
+ const dataStr = typeof blob.data === "string" ? blob.data : blob.data.toString("utf-8");
330
+ const payload = JSON.parse(dataStr);
331
+ const context = { customLabels: {} };
332
+ const partnerId = payload.partner_id || payload.partnerId;
333
+ if (typeof partnerId === "string" && partnerId.length > 0) context.partnerId = partnerId;
334
+ context.subscriptionTier = normalizeTier(payload.subscription_tier || payload.subscriptionTier);
335
+ const userId = payload.end_user_id || payload.endUserId || payload.user_id || payload.userId;
336
+ if (typeof userId === "string" && userId.length > 0) context.endUserId = userId;
337
+ context.product = normalizeProduct(payload.product);
338
+ context.environment = normalizeEnvironment(payload.environment || payload.env);
339
+ const customLabels = {};
340
+ const addLabels = (obj) => {
341
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
342
+ for (const [key, value] of Object.entries(obj)) if (typeof value === "string") customLabels[key] = value;
343
+ else if (typeof value === "number" || typeof value === "boolean") customLabels[key] = String(value);
344
+ }
345
+ };
346
+ addLabels(payload.custom);
347
+ addLabels(payload.labels);
348
+ if (Object.keys(customLabels).length > 0) context.customLabels = customLabels;
349
+ return context;
350
+ }
351
+ };
352
+ /** Create a JSON adapter instance */
353
+ function createJsonAdapter() {
354
+ return jsonAdapter;
355
+ }
356
+ //#endregion
357
+ //#region src/enrichment/index.ts
358
+ /**
359
+ * Initialize the default registry with built-in adapters.
360
+ * Call this at startup to ensure JSON adapter is available.
361
+ */
362
+ function initializeDefaultAdapters() {
363
+ const registry = getDefaultRegistry();
364
+ if (!registry.getAdapterNames().includes("json")) registry.register(jsonAdapter);
365
+ }
366
+ /**
367
+ * Parse a metadata blob using the default registry.
368
+ * Convenience function for common use case.
369
+ */
370
+ function parseMetadataBlob(blob) {
371
+ initializeDefaultAdapters();
372
+ return getDefaultRegistry().parse(blob);
373
+ }
374
+ //#endregion
375
+ //#region src/trace-types.ts
376
+ /** Type guard for MessagePartUpdatedEvent */
377
+ function isMessagePartUpdatedEvent(event) {
378
+ return event.type === "message.part.updated";
379
+ }
380
+ function isMessageUpdatedEvent(event) {
381
+ return event.type === "message.updated";
382
+ }
383
+ function isErrorEvent(event) {
384
+ return event.type === "error";
385
+ }
386
+ function isDiagnosticEvent(event) {
387
+ return event.type === "diagnostic";
388
+ }
389
+ function isFileChangeEvent(event) {
390
+ return event.type === "file_change";
391
+ }
392
+ function isBuildResultEvent(event) {
393
+ return event.type === "build_result";
394
+ }
395
+ function isTestResultEvent(event) {
396
+ return event.type === "test_result";
397
+ }
398
+ function isEnvironmentEvent(event) {
399
+ return event.type === "environment";
400
+ }
401
+ function isCustomEvent(event) {
402
+ return event.type === "custom";
403
+ }
404
+ //#endregion
405
+ //#region src/langfuse-sink.ts
406
+ let langfuseClient = null;
407
+ let langfuseConfig = null;
408
+ /**
409
+ * Get or create the Langfuse client
410
+ */
411
+ async function getLangfuseClient(config) {
412
+ if (langfuseClient && langfuseConfig === config) return langfuseClient;
413
+ try {
414
+ const { Langfuse } = await import("langfuse-node");
415
+ langfuseClient = new Langfuse({
416
+ publicKey: config.publicKey,
417
+ secretKey: config.secretKey,
418
+ baseUrl: config.baseUrl || "https://cloud.langfuse.com"
419
+ });
420
+ langfuseConfig = config;
421
+ return langfuseClient;
422
+ } catch (err) {
423
+ console.warn("[langfuse-sink] Failed to initialize Langfuse:", err);
424
+ return null;
425
+ }
426
+ }
427
+ /**
428
+ * Langfuse trace exporter using the native SDK.
429
+ *
430
+ * Creates proper Langfuse traces with spans and generations
431
+ * for rich observability in the Langfuse dashboard.
432
+ *
433
+ * Note: This is a trace-level exporter (ExecutionTrace), not a
434
+ * TelemetrySink (TelemetryEvent). Use for exporting complete traces.
435
+ */
436
+ var LangfuseSink = class {
437
+ name = "langfuse";
438
+ config;
439
+ constructor(config) {
440
+ this.config = config;
441
+ }
442
+ /**
443
+ * Export an execution trace to Langfuse.
444
+ */
445
+ async exportTrace(trace) {
446
+ const client = await getLangfuseClient(this.config);
447
+ if (!client) return;
448
+ try {
449
+ const langfuseTrace = client.trace({
450
+ id: trace.id,
451
+ name: trace.task,
452
+ sessionId: trace.scope.sessionId,
453
+ userId: trace.scope.userId,
454
+ metadata: {
455
+ outcome: trace.outcome,
456
+ projectRef: trace.scope.projectRef,
457
+ ...trace.metadata
458
+ },
459
+ tags: trace.outcome === "failure" ? ["failed"] : void 0
460
+ });
461
+ for (const event of trace.events) {
462
+ if (!event) continue;
463
+ this.exportTraceEvent(langfuseTrace, event);
464
+ }
465
+ for (const signal of trace.signals) langfuseTrace.event({
466
+ name: `signal.${signal.signal}`,
467
+ startTime: signal.timestamp,
468
+ metadata: signal.metadata
469
+ });
470
+ langfuseTrace.update({ output: trace.outcome });
471
+ await client.flushAsync();
472
+ } catch (err) {
473
+ console.warn("[langfuse-sink] Failed to export trace:", err);
474
+ }
475
+ }
476
+ /**
477
+ * Export a trace event as a Langfuse span or generation.
478
+ */
479
+ exportTraceEvent(trace, event) {
480
+ switch (event.type) {
481
+ case "message.part.updated": {
482
+ const partEvent = event;
483
+ const part = partEvent.part;
484
+ if (isToolPart(part)) this.exportToolPart(trace, partEvent, part);
485
+ else if (isTextPart(part) || isReasoningPart(part)) trace.generation({
486
+ name: part.type === "reasoning" ? "reasoning" : "text",
487
+ startTime: partEvent.timestamp,
488
+ output: part.text,
489
+ metadata: {
490
+ partId: part.id,
491
+ sessionID: part.sessionID,
492
+ messageID: part.messageID,
493
+ delta: partEvent.delta
494
+ }
495
+ });
496
+ else if (isFilePart(part)) trace.event({
497
+ name: `file.${part.mediaType || "unknown"}`,
498
+ startTime: partEvent.timestamp,
499
+ metadata: {
500
+ partId: part.id,
501
+ filename: part.filename,
502
+ mediaType: part.mediaType,
503
+ url: part.url
504
+ }
505
+ });
506
+ break;
507
+ }
508
+ case "message.updated":
509
+ trace.event({
510
+ name: "message.updated",
511
+ startTime: event.timestamp,
512
+ metadata: {
513
+ text: event.text ?? event.finalText,
514
+ finalText: event.finalText,
515
+ tokenUsage: event.tokenUsage,
516
+ timing: event.timing,
517
+ toolInvocations: event.toolInvocations,
518
+ metadata: event.metadata
519
+ }
520
+ });
521
+ break;
522
+ case "error":
523
+ trace.event({
524
+ name: `error.${event.category}`,
525
+ startTime: event.timestamp,
526
+ level: "ERROR",
527
+ metadata: {
528
+ message: event.message,
529
+ code: event.code,
530
+ source: event.source
531
+ }
532
+ });
533
+ break;
534
+ case "file_change":
535
+ trace.event({
536
+ name: `file.${event.changeType}`,
537
+ startTime: event.timestamp,
538
+ metadata: {
539
+ file: event.file,
540
+ linesAdded: event.linesAdded,
541
+ linesRemoved: event.linesRemoved
542
+ }
543
+ });
544
+ break;
545
+ case "build_result":
546
+ trace.span({
547
+ name: `build.${event.success ? "success" : "failure"}`,
548
+ startTime: event.timestamp,
549
+ endTime: event.durationMs ? new Date(event.timestamp.getTime() + event.durationMs) : void 0,
550
+ output: event.success ? "success" : "failed",
551
+ metadata: {
552
+ command: event.command,
553
+ errorCount: event.errorCount,
554
+ warningCount: event.warningCount
555
+ },
556
+ level: event.success ? void 0 : "ERROR"
557
+ });
558
+ break;
559
+ case "test_result":
560
+ trace.span({
561
+ name: `test.${event.success ? "success" : "failure"}`,
562
+ startTime: event.timestamp,
563
+ endTime: event.durationMs ? new Date(event.timestamp.getTime() + event.durationMs) : void 0,
564
+ output: event.success ? "success" : "failed",
565
+ metadata: {
566
+ command: event.command,
567
+ passed: event.passed,
568
+ failed: event.failed,
569
+ skipped: event.skipped
570
+ },
571
+ level: event.success ? void 0 : "ERROR"
572
+ });
573
+ break;
574
+ case "diagnostic":
575
+ trace.event({
576
+ name: `diagnostic.${event.language || "unknown"}`,
577
+ startTime: event.timestamp,
578
+ metadata: {
579
+ file: event.file,
580
+ errorDelta: event.errorDelta,
581
+ errorsAfter: event.afterSummary.errors,
582
+ warningsAfter: event.afterSummary.warnings
583
+ }
584
+ });
585
+ break;
586
+ case "environment":
587
+ trace.event({
588
+ name: "environment.snapshot",
589
+ startTime: event.timestamp,
590
+ metadata: {
591
+ workingDir: event.workingDir,
592
+ platform: event.platform
593
+ }
594
+ });
595
+ break;
596
+ case "custom":
597
+ if (event.name === "startup.phase") {
598
+ const durationMs = typeof event.data.durationMs === "number" ? event.data.durationMs : 0;
599
+ const phase = typeof event.data.phase === "string" ? event.data.phase : "unknown";
600
+ trace.span({
601
+ name: `startup.${phase}`,
602
+ startTime: new Date(event.timestamp.getTime() - durationMs),
603
+ endTime: event.timestamp,
604
+ metadata: event.data,
605
+ level: event.data.status === "error" ? "ERROR" : void 0,
606
+ statusMessage: typeof event.data.error === "string" ? event.data.error : void 0
607
+ });
608
+ } else trace.event({
609
+ name: `custom.${event.name}`,
610
+ startTime: event.timestamp,
611
+ metadata: event.data
612
+ });
613
+ break;
614
+ }
615
+ }
616
+ /**
617
+ * Export a tool part from message.part.updated event.
618
+ * Tool parts have state progression: pending → running → completed/error
619
+ */
620
+ exportToolPart(trace, event, part) {
621
+ const status = part.state.status;
622
+ const toolName = part.tool;
623
+ if (status === "running") trace.span({
624
+ name: `tool.${toolName}`,
625
+ startTime: event.timestamp,
626
+ input: part.state.input ?? { callID: part.callID },
627
+ metadata: {
628
+ partId: part.id,
629
+ sessionID: part.sessionID,
630
+ messageID: part.messageID,
631
+ status
632
+ }
633
+ });
634
+ else if (status === "completed") {
635
+ const endTime = part.state.time?.end ? new Date(part.state.time.end) : event.timestamp;
636
+ const startTime = part.state.time?.start ? new Date(part.state.time.start) : event.timestamp;
637
+ trace.span({
638
+ name: `tool.${toolName}.result`,
639
+ startTime,
640
+ endTime,
641
+ input: part.state.input,
642
+ output: part.state.output ?? "success",
643
+ metadata: {
644
+ partId: part.id,
645
+ sessionID: part.sessionID,
646
+ messageID: part.messageID,
647
+ callID: part.callID,
648
+ success: true
649
+ }
650
+ });
651
+ } else if (status === "failed" || status === "error") {
652
+ const endTime = part.state.time?.end ? new Date(part.state.time.end) : event.timestamp;
653
+ const startTime = part.state.time?.start ? new Date(part.state.time.start) : event.timestamp;
654
+ trace.span({
655
+ name: `tool.${toolName}.result`,
656
+ startTime,
657
+ endTime,
658
+ input: part.state.input,
659
+ output: part.state.output ?? part.state.error,
660
+ metadata: {
661
+ partId: part.id,
662
+ sessionID: part.sessionID,
663
+ messageID: part.messageID,
664
+ callID: part.callID,
665
+ success: false
666
+ },
667
+ level: "ERROR",
668
+ statusMessage: part.state.error
669
+ });
670
+ }
671
+ }
672
+ async flush() {
673
+ const client = await getLangfuseClient(this.config);
674
+ if (client) await client.flushAsync();
675
+ }
676
+ async close() {
677
+ const client = await getLangfuseClient(this.config);
678
+ if (client) {
679
+ await client.shutdownAsync();
680
+ langfuseClient = null;
681
+ }
682
+ }
683
+ };
684
+ /**
685
+ * Create a Langfuse sink from environment variables.
686
+ */
687
+ function createLangfuseSinkFromEnv() {
688
+ const publicKey = process.env.LANGFUSE_PUBLIC_KEY;
689
+ const secretKey = process.env.LANGFUSE_SECRET_KEY;
690
+ if (!publicKey || !secretKey) return null;
691
+ return new LangfuseSink({
692
+ publicKey,
693
+ secretKey,
694
+ baseUrl: process.env.LANGFUSE_HOST || "https://cloud.langfuse.com",
695
+ debug: process.env.LANGFUSE_DEBUG === "true"
696
+ });
697
+ }
698
+ //#endregion
699
+ //#region src/otel-provider.ts
700
+ let provider = null;
701
+ /** Parse `OTEL_EXPORTER_OTLP_HEADERS` ("k=v,k2=v2") into a header map. */
702
+ function parseHeaders(raw) {
703
+ const out = {};
704
+ if (!raw) return out;
705
+ for (const pair of raw.split(",")) {
706
+ const eq = pair.indexOf("=");
707
+ if (eq <= 0) continue;
708
+ const key = pair.slice(0, eq).trim();
709
+ const value = pair.slice(eq + 1).trim();
710
+ if (key) out[key] = value;
711
+ }
712
+ return out;
713
+ }
714
+ async function initOtelProvider(serviceName) {
715
+ if (provider) return { active: true };
716
+ if (process.env.TELEMETRY_ENABLED !== "true") return {
717
+ active: false,
718
+ reason: "TELEMETRY_ENABLED!=true"
719
+ };
720
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
721
+ if (!endpoint) return {
722
+ active: false,
723
+ reason: "OTEL_EXPORTER_OTLP_ENDPOINT unset"
724
+ };
725
+ try {
726
+ const [{ NodeTracerProvider, BatchSpanProcessor }, { OTLPTraceExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME }] = await Promise.all([
727
+ import("@opentelemetry/sdk-trace-node"),
728
+ import("@opentelemetry/exporter-trace-otlp-http"),
729
+ import("@opentelemetry/resources"),
730
+ import("@opentelemetry/semantic-conventions")
731
+ ]);
732
+ const exporter = new OTLPTraceExporter({
733
+ url: endpoint,
734
+ headers: parseHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS)
735
+ });
736
+ const tracerProvider = new NodeTracerProvider({
737
+ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: serviceName }),
738
+ spanProcessors: [new BatchSpanProcessor(exporter)]
739
+ });
740
+ tracerProvider.register();
741
+ provider = tracerProvider;
742
+ return { active: true };
743
+ } catch (error) {
744
+ return {
745
+ active: false,
746
+ reason: error instanceof Error ? error.message : String(error)
747
+ };
748
+ }
749
+ }
750
+ async function shutdownOtelProvider() {
751
+ if (!provider) return;
752
+ const current = provider;
753
+ provider = null;
754
+ try {
755
+ await current.forceFlush();
756
+ } catch {}
757
+ await current.shutdown();
758
+ }
759
+ //#endregion
760
+ //#region src/otel-sink.ts
761
+ /**
762
+ * OpenTelemetry Sink
763
+ *
764
+ * Exports telemetry data to OpenTelemetry-compatible backends including
765
+ * Langfuse, Jaeger, Honeycomb, and other OTLP collectors.
766
+ *
767
+ * IMPORTANT: This sink uses the GLOBAL tracer from @opentelemetry/api.
768
+ * The OTEL SDK must be initialized separately (e.g., via NodeSDK with
769
+ * LangfuseSpanProcessor or OTLPTraceExporter) before using this sink.
770
+ *
771
+ * This sink converts ExecutionTrace data into OTEL spans using the
772
+ * global tracer, which ensures spans flow through whatever processor
773
+ * was configured at SDK initialization.
774
+ */
775
+ /**
776
+ * GenAI semantic-convention keys promoted from a `message.updated` event's
777
+ * metadata bag to DISCRETE span attributes. The intelligence ingest derives
778
+ * model/token/cost and conversation linkage by reading these exact keys off the
779
+ * flat span attribute bag (`parseGenAiAttribution` / `parseConversationLinkage`
780
+ * in the intelligence API); leaving them inside the serialized `message.metadata`
781
+ * blob would attribute every auto-instrumented LLM call as model/tokens/cost
782
+ * unknown. Promotion is unconditional (not gated on `includePayloads`): these
783
+ * are attribution signal, not prompt/completion content. `genAiUsageAttributes`
784
+ * already guarantees the writer never emits a fabricated zero, so a key absent
785
+ * here stays absent on the span — "cost not computed", never "free".
786
+ */
787
+ const GEN_AI_PROMOTED_KEYS = Object.freeze([
788
+ GEN_AI_REQUEST_MODEL$1,
789
+ GEN_AI_RESPONSE_MODEL$1,
790
+ GEN_AI_USAGE_INPUT_TOKENS$1,
791
+ GEN_AI_USAGE_OUTPUT_TOKENS$1,
792
+ GEN_AI_OPERATION_NAME$1,
793
+ GEN_AI_CONVERSATION_ID$1
794
+ ]);
795
+ /**
796
+ * Build the parent-span attributes for a trace, with a deliberate precedence:
797
+ * partner-supplied `resourceAttributes` are the LOWEST layer, so the trusted
798
+ * identity attributes (agent.trace.id, session.id, user.id) and metadata always
799
+ * override a colliding key. A partner cannot spoof span identity by passing a
800
+ * `resourceAttributes` key like `session.id`.
801
+ */
802
+ function buildTraceSpanAttributes(trace, flattenedMetadata) {
803
+ return {
804
+ ...trace.scope.resourceAttributes ?? {},
805
+ "agent.trace.id": trace.id,
806
+ "agent.task": trace.task,
807
+ "agent.outcome": trace.outcome,
808
+ "langfuse.trace.id": trace.id,
809
+ "langfuse.trace.name": trace.task,
810
+ ...trace.scope.projectRef && { "langfuse.project": trace.scope.projectRef },
811
+ ...trace.scope.sessionId && {
812
+ "langfuse.session.id": trace.scope.sessionId,
813
+ "session.id": trace.scope.sessionId
814
+ },
815
+ ...trace.scope.userId && {
816
+ "langfuse.user.id": trace.scope.userId,
817
+ "user.id": trace.scope.userId
818
+ },
819
+ ...flattenedMetadata ?? {}
820
+ };
821
+ }
822
+ let otelApi = null;
823
+ /**
824
+ * Load the OTEL API dynamically
825
+ */
826
+ async function getOtelApi() {
827
+ if (!otelApi) try {
828
+ otelApi = await import("@opentelemetry/api");
829
+ } catch {
830
+ return null;
831
+ }
832
+ return otelApi;
833
+ }
834
+ /**
835
+ * OpenTelemetry sink — exports via whatever OTEL SDK config was set at
836
+ * application startup (Langfuse, Jaeger, any OTLP backend).
837
+ */
838
+ var OtelTelemetrySink = class {
839
+ name = "otel";
840
+ config;
841
+ constructor(config = {}) {
842
+ this.config = {
843
+ serviceName: config.serviceName ?? "agent-sdk",
844
+ includePayloads: config.includePayloads ?? false
845
+ };
846
+ }
847
+ async send(events) {
848
+ const api = await getOtelApi();
849
+ if (!api) return;
850
+ const tracer = api.trace.getTracer(this.config.serviceName, "1.0.0");
851
+ for (const event of events) {
852
+ if (event.type === "agent_execution" && this.isExecutionTrace(event.data.trace)) {
853
+ await this.exportTrace(event.data.trace);
854
+ continue;
855
+ }
856
+ const span = tracer.startSpan(`telemetry.${event.type}.${event.name}`, {
857
+ attributes: {
858
+ "telemetry.event.type": event.type,
859
+ "telemetry.event.name": event.name
860
+ },
861
+ startTime: event.timestamp
862
+ });
863
+ if (event.data && typeof event.data === "object") {
864
+ for (const [key, value] of Object.entries(event.data)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") span.setAttribute(`telemetry.data.${key}`, value);
865
+ }
866
+ span.setStatus({ code: api.SpanStatusCode.OK });
867
+ span.end();
868
+ }
869
+ }
870
+ /**
871
+ * Export an execution trace as OTEL spans.
872
+ *
873
+ * Creates a parent span for the trace and child spans for each event.
874
+ * Uses the global tracer, so spans flow through the configured processor.
875
+ */
876
+ async exportTrace(trace) {
877
+ const api = await getOtelApi();
878
+ if (!api) return;
879
+ const tracer = api.trace.getTracer(this.config.serviceName, "1.0.0");
880
+ const traceSpan = tracer.startSpan("agent.task", {
881
+ attributes: buildTraceSpanAttributes(trace, trace.metadata ? this.flattenMetadata(trace.metadata) : void 0),
882
+ startTime: trace.startedAt
883
+ });
884
+ const parentContext = api.trace.setSpan(api.context.active(), traceSpan);
885
+ for (const event of trace.events) {
886
+ if (!event?.type) continue;
887
+ api.context.with(parentContext, () => {
888
+ this.exportTraceEvent(tracer, api, event);
889
+ });
890
+ }
891
+ for (const signal of trace.signals) traceSpan.addEvent(`signal.${signal.signal}`, {}, signal.timestamp);
892
+ if (trace.outcome === "failure") traceSpan.setStatus({
893
+ code: api.SpanStatusCode.ERROR,
894
+ message: "Task failed"
895
+ });
896
+ else traceSpan.setStatus({ code: api.SpanStatusCode.OK });
897
+ traceSpan.end(trace.completedAt || /* @__PURE__ */ new Date());
898
+ }
899
+ /**
900
+ * Export a single trace event as an OTEL span.
901
+ */
902
+ exportTraceEvent(tracer, api, event) {
903
+ const spanName = this.getSpanName(event);
904
+ const attributes = this.getEventAttributes(event);
905
+ const startTime = this.getEventStartTime(event);
906
+ const endTime = this.getEventEndTime(event);
907
+ const span = tracer.startSpan(spanName, {
908
+ attributes,
909
+ startTime
910
+ });
911
+ if (event.type === "error") {
912
+ span.setStatus({
913
+ code: api.SpanStatusCode.ERROR,
914
+ message: event.message
915
+ });
916
+ span.recordException(new Error(event.message));
917
+ } else if (event.type === "message.part.updated" && isToolPart(event.part)) {
918
+ const status = event.part.state.status;
919
+ if (status === "failed" || status === "error") {
920
+ const err = typeof event.part.state.error === "string" ? event.part.state.error : "Tool execution failed";
921
+ span.setStatus({
922
+ code: api.SpanStatusCode.ERROR,
923
+ message: err
924
+ });
925
+ span.recordException(new Error(err));
926
+ } else span.setStatus({ code: api.SpanStatusCode.OK });
927
+ } else if (event.type === "build_result" && !event.success) span.setStatus({
928
+ code: api.SpanStatusCode.ERROR,
929
+ message: "Build failed"
930
+ });
931
+ else if (event.type === "test_result" && !event.success) span.setStatus({
932
+ code: api.SpanStatusCode.ERROR,
933
+ message: "Tests failed"
934
+ });
935
+ else if (event.type === "custom" && event.name === "startup.phase" && event.data.status === "error") span.setStatus({
936
+ code: api.SpanStatusCode.ERROR,
937
+ message: typeof event.data.error === "string" ? event.data.error : "Startup phase failed"
938
+ });
939
+ else span.setStatus({ code: api.SpanStatusCode.OK });
940
+ span.end(endTime);
941
+ }
942
+ getEventStartTime(event) {
943
+ if (event.type === "custom" && event.name === "startup.phase") {
944
+ const durationMs = this.getStartupPhaseDurationMs(event);
945
+ if (typeof durationMs === "number") return new Date(event.timestamp.getTime() - durationMs);
946
+ }
947
+ return event.timestamp;
948
+ }
949
+ getEventEndTime(event) {
950
+ if (event.type === "custom" && event.name === "startup.phase") return event.timestamp;
951
+ if ("durationMs" in event && typeof event.durationMs === "number") return new Date(event.timestamp.getTime() + event.durationMs);
952
+ return event.timestamp;
953
+ }
954
+ getStartupPhaseDurationMs(event) {
955
+ return typeof event.data.durationMs === "number" ? event.data.durationMs : void 0;
956
+ }
957
+ /**
958
+ * Get span name for a trace event.
959
+ */
960
+ getSpanName(event) {
961
+ switch (event.type) {
962
+ case "message.part.updated":
963
+ if (isToolPart(event.part)) return `tool.${event.part.tool}.${event.part.state.status}`;
964
+ if (isReasoningPart(event.part)) return "llm.reasoning";
965
+ if (isTextPart(event.part)) return "llm.text";
966
+ return "message.file";
967
+ case "message.updated": return "message.updated";
968
+ case "error": return `error.${event.category}`;
969
+ case "diagnostic": return `diagnostic.${event.language || "unknown"}`;
970
+ case "file_change": return `file.${event.changeType}`;
971
+ case "build_result": return `build.${event.success ? "success" : "failure"}`;
972
+ case "test_result": return `test.${event.success ? "success" : "failure"}`;
973
+ case "environment": return "environment.snapshot";
974
+ case "custom":
975
+ if (event.name === "startup.phase") return `startup.${typeof event.data.phase === "string" ? event.data.phase : "unknown"}`;
976
+ return `custom.${event.name}`;
977
+ default: return "unknown";
978
+ }
979
+ }
980
+ /**
981
+ * Get OTEL attributes for a trace event.
982
+ */
983
+ getEventAttributes(event) {
984
+ const attrs = { "event.type": event.type };
985
+ switch (event.type) {
986
+ case "message.part.updated":
987
+ attrs["message.part.id"] = event.part.id;
988
+ attrs["message.part.type"] = event.part.type;
989
+ if (isToolPart(event.part)) {
990
+ attrs["tool.name"] = event.part.tool;
991
+ const status = event.part.state.status;
992
+ attrs["tool.status"] = status;
993
+ if (this.config.includePayloads && event.part.state.input) attrs["tool.input"] = JSON.stringify(event.part.state.input).slice(0, 4096);
994
+ if (this.config.includePayloads && status === "completed" && event.part.state.output !== void 0) attrs["tool.output"] = JSON.stringify(event.part.state.output).slice(0, 4096);
995
+ if (this.config.includePayloads && (status === "error" || status === "failed") && typeof event.part.state.error === "string") attrs["tool.error"] = event.part.state.error.slice(0, 4096);
996
+ } else if (isTextPart(event.part) || isReasoningPart(event.part)) {
997
+ attrs["message.part.text_length"] = event.part.text.length;
998
+ if (this.config.includePayloads) attrs["message.part.text"] = event.part.text.slice(0, 4096);
999
+ } else if (isFilePart(event.part)) {
1000
+ attrs["file.name"] = event.part.filename;
1001
+ if (event.part.mediaType) attrs["file.media_type"] = event.part.mediaType;
1002
+ } else {
1003
+ attrs["subtask.id"] = event.part.id;
1004
+ attrs["subtask.session_id"] = event.part.sessionID;
1005
+ }
1006
+ break;
1007
+ case "message.updated": {
1008
+ const text = event.text ?? event.finalText;
1009
+ if (typeof text === "string") {
1010
+ attrs["message.text_length"] = text.length;
1011
+ if (this.config.includePayloads) attrs["message.text"] = text.slice(0, 4096);
1012
+ }
1013
+ if (this.config.includePayloads && event.tokenUsage !== void 0) attrs["message.token_usage"] = JSON.stringify(event.tokenUsage).slice(0, 4096);
1014
+ if (this.config.includePayloads && event.timing !== void 0) attrs["message.timing"] = JSON.stringify(event.timing).slice(0, 4096);
1015
+ if (this.config.includePayloads && event.toolInvocations !== void 0) attrs["message.tool_invocations"] = JSON.stringify(event.toolInvocations).slice(0, 4096);
1016
+ if (event.metadata) for (const key of GEN_AI_PROMOTED_KEYS) {
1017
+ const value = event.metadata[key];
1018
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") attrs[key] = value;
1019
+ }
1020
+ if (this.config.includePayloads && event.metadata !== void 0) attrs["message.metadata"] = JSON.stringify(event.metadata).slice(0, 4096);
1021
+ break;
1022
+ }
1023
+ case "error":
1024
+ attrs["error.category"] = event.category;
1025
+ attrs["error.message"] = event.message;
1026
+ if (event.code) attrs["error.code"] = event.code;
1027
+ if (event.source) attrs["error.source"] = event.source;
1028
+ break;
1029
+ case "diagnostic":
1030
+ attrs["diagnostic.file"] = event.file;
1031
+ if (event.language) attrs["diagnostic.language"] = event.language;
1032
+ attrs["diagnostic.error_delta"] = event.errorDelta;
1033
+ attrs["diagnostic.errors_after"] = event.afterSummary.errors;
1034
+ attrs["diagnostic.warnings_after"] = event.afterSummary.warnings;
1035
+ break;
1036
+ case "file_change":
1037
+ attrs["file.path"] = event.file;
1038
+ attrs["file.change_type"] = event.changeType;
1039
+ if (event.linesAdded) attrs["file.lines_added"] = event.linesAdded;
1040
+ if (event.linesRemoved) attrs["file.lines_removed"] = event.linesRemoved;
1041
+ break;
1042
+ case "build_result":
1043
+ attrs["build.success"] = event.success;
1044
+ if (event.command) attrs["build.command"] = event.command;
1045
+ if (event.durationMs) attrs["build.duration_ms"] = event.durationMs;
1046
+ if (event.errorCount) attrs["build.error_count"] = event.errorCount;
1047
+ if (event.warningCount) attrs["build.warning_count"] = event.warningCount;
1048
+ break;
1049
+ case "test_result":
1050
+ attrs["test.success"] = event.success;
1051
+ if (event.command) attrs["test.command"] = event.command;
1052
+ if (event.durationMs) attrs["test.duration_ms"] = event.durationMs;
1053
+ if (event.passed) attrs["test.passed"] = event.passed;
1054
+ if (event.failed) attrs["test.failed"] = event.failed;
1055
+ if (event.skipped) attrs["test.skipped"] = event.skipped;
1056
+ break;
1057
+ case "environment":
1058
+ if (event.workingDir) attrs["env.working_dir"] = event.workingDir;
1059
+ if (event.platform) attrs["env.platform"] = event.platform;
1060
+ break;
1061
+ case "custom":
1062
+ attrs["custom.name"] = event.name;
1063
+ if (event.name === "startup.phase") {
1064
+ attrs["startup.id"] = typeof event.data.startupId === "string" ? event.data.startupId : void 0;
1065
+ attrs["startup.phase"] = typeof event.data.phase === "string" ? event.data.phase : void 0;
1066
+ attrs["startup.service"] = typeof event.data.service === "string" ? event.data.service : void 0;
1067
+ attrs["startup.status"] = typeof event.data.status === "string" ? event.data.status : void 0;
1068
+ }
1069
+ for (const [key, value] of Object.entries(event.data)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") attrs[`custom.data.${key}`] = value;
1070
+ break;
1071
+ }
1072
+ return attrs;
1073
+ }
1074
+ /**
1075
+ * Flatten metadata object into span attributes
1076
+ */
1077
+ flattenMetadata(metadata, prefix = "metadata") {
1078
+ const attrs = {};
1079
+ for (const [key, value] of Object.entries(metadata)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") attrs[`${prefix}.${key}`] = value;
1080
+ return attrs;
1081
+ }
1082
+ /** Narrow an opaque telemetry `data.trace` to an ExecutionTrace. */
1083
+ isExecutionTrace(value) {
1084
+ return typeof value === "object" && value !== null && "scope" in value && "events" in value && Array.isArray(value.events);
1085
+ }
1086
+ async flush() {}
1087
+ async close() {}
1088
+ };
1089
+ /**
1090
+ * Create an OTEL sink that uses the global tracer.
1091
+ *
1092
+ * The OTEL SDK must be initialized separately before using this sink.
1093
+ */
1094
+ function createOtelSink(config) {
1095
+ return new OtelTelemetrySink(config);
1096
+ }
1097
+ /**
1098
+ * Create an OTEL sink from environment variables.
1099
+ *
1100
+ * Returns a sink that uses the global tracer (which should be initialized
1101
+ * separately via NodeSDK with LangfuseSpanProcessor or OTLPTraceExporter).
1102
+ */
1103
+ function createOtelSinkFromEnv() {
1104
+ const enabled = process.env.TELEMETRY_ENABLED === "true";
1105
+ const hasLangfuse = process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY;
1106
+ const hasOtlp = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
1107
+ if (!enabled || !hasLangfuse && !hasOtlp) return null;
1108
+ return new OtelTelemetrySink({
1109
+ serviceName: process.env.OTEL_SERVICE_NAME || "orchestrator",
1110
+ includePayloads: process.env.OTEL_INCLUDE_PAYLOADS === "true"
1111
+ });
1112
+ }
1113
+ //#endregion
1114
+ //#region src/sinks.ts
1115
+ var HttpTelemetrySink = class {
1116
+ name = "http";
1117
+ endpoint;
1118
+ headers;
1119
+ fetchFn;
1120
+ timeout;
1121
+ pendingRequests = [];
1122
+ constructor(config) {
1123
+ this.endpoint = config.endpoint;
1124
+ this.headers = {
1125
+ "Content-Type": "application/json",
1126
+ ...config.headers
1127
+ };
1128
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
1129
+ this.timeout = config.timeout ?? 3e4;
1130
+ }
1131
+ async send(events) {
1132
+ const request = this.fetchFn(this.endpoint, {
1133
+ method: "POST",
1134
+ headers: this.headers,
1135
+ body: JSON.stringify({ events }),
1136
+ signal: AbortSignal.timeout(this.timeout)
1137
+ }).then((res) => {
1138
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
1139
+ });
1140
+ this.pendingRequests.push(request);
1141
+ try {
1142
+ await request;
1143
+ } finally {
1144
+ this.pendingRequests = this.pendingRequests.filter((r) => r !== request);
1145
+ }
1146
+ }
1147
+ async flush() {
1148
+ await Promise.all(this.pendingRequests);
1149
+ }
1150
+ async close() {
1151
+ await this.flush();
1152
+ }
1153
+ };
1154
+ var ConsoleTelemetrySink = class {
1155
+ name = "console";
1156
+ prefix;
1157
+ logLevel;
1158
+ formatJson;
1159
+ constructor(config) {
1160
+ this.prefix = config?.prefix ?? "[telemetry]";
1161
+ this.logLevel = config?.logLevel ?? "debug";
1162
+ this.formatJson = config?.formatJson ?? true;
1163
+ }
1164
+ async send(events) {
1165
+ for (const event of events) {
1166
+ const message = this.formatJson ? JSON.stringify(event, null, 2) : `${event.type}:${event.name} ${JSON.stringify(event.data)}`;
1167
+ console[this.logLevel](`${this.prefix} ${message}`);
1168
+ }
1169
+ }
1170
+ async flush() {}
1171
+ async close() {}
1172
+ };
1173
+ var TestTelemetrySink = class {
1174
+ name = "test";
1175
+ events = [];
1176
+ async send(events) {
1177
+ this.events.push(...events);
1178
+ }
1179
+ async flush() {}
1180
+ async close() {}
1181
+ clear() {
1182
+ this.events.length = 0;
1183
+ }
1184
+ getEvents() {
1185
+ return [...this.events];
1186
+ }
1187
+ getEventsByType(type) {
1188
+ return this.events.filter((e) => e.type === type);
1189
+ }
1190
+ getEventsByName(name) {
1191
+ return this.events.filter((e) => e.name === name);
1192
+ }
1193
+ };
1194
+ var MultiTelemetrySink = class {
1195
+ name = "multi";
1196
+ sinks;
1197
+ constructor(sinks) {
1198
+ this.sinks = sinks;
1199
+ }
1200
+ async send(events) {
1201
+ await Promise.all(this.sinks.map((sink) => sink.send(events)));
1202
+ }
1203
+ async flush() {
1204
+ await Promise.all(this.sinks.map((sink) => sink.flush()));
1205
+ }
1206
+ async close() {
1207
+ await Promise.all(this.sinks.map((sink) => sink.close()));
1208
+ }
1209
+ };
1210
+ function createHttpSink(config) {
1211
+ return new HttpTelemetrySink(config);
1212
+ }
1213
+ function createConsoleSink(config) {
1214
+ return new ConsoleTelemetrySink(config);
1215
+ }
1216
+ function createTestSink() {
1217
+ return new TestTelemetrySink();
1218
+ }
1219
+ //#endregion
1220
+ //#region src/sse-parser.ts
1221
+ /**
1222
+ * SSE Event Parser
1223
+ *
1224
+ * Converts SSE stream events to typed TraceEvents.
1225
+ * Used by both orchestrator and simulation to ensure consistent event capture.
1226
+ *
1227
+ * message.part.updated events are passed through directly without transformation.
1228
+ * This preserves full fidelity to what OpenCode outputs.
1229
+ */
1230
+ const parseSSEData = parseSSEData$1;
1231
+ /** Valid signals from SSE status events */
1232
+ const VALID_SIGNALS = [
1233
+ "build_passed",
1234
+ "build_failed",
1235
+ "tests_passed",
1236
+ "tests_failed",
1237
+ "lint_passed",
1238
+ "lint_failed",
1239
+ "user_approved",
1240
+ "user_rejected",
1241
+ "task_completed",
1242
+ "task_abandoned",
1243
+ "runtime_error",
1244
+ "timeout"
1245
+ ];
1246
+ /**
1247
+ * Provider event shapes that carry a tool-invocation payload either at
1248
+ * the top level or under one of the well-known wrappers. Recognized here
1249
+ * (and not in the consumer) so trace events look identical regardless of
1250
+ * which provider emitted the call.
1251
+ */
1252
+ const TOOL_INVOCATION_TYPES = new Set([
1253
+ "tool-invocation",
1254
+ `tool_call`,
1255
+ "computer-use",
1256
+ "computer_call"
1257
+ ]);
1258
+ /**
1259
+ * Best-effort: lift a raw tool-invocation event into a canonical
1260
+ * `message.part.updated` event with a `ToolPart`. Returns null for
1261
+ * payloads we cannot recognize, which lets the caller fall through to
1262
+ * a `custom` trace event rather than silently misclassifying.
1263
+ *
1264
+ * Wire contract — what shapes are expected here:
1265
+ *
1266
+ * This normalizer runs at the trace-event boundary, AFTER provider
1267
+ * adapters have done their own normalization. Adapters
1268
+ * (sdk-provider-opencode, sdk-provider-amp, sdk-provider-claude-code,
1269
+ * etc.) translate provider-native events into our internal
1270
+ * AgentStreamEvent shape — typically wrapping tool invocations
1271
+ * under `toolInvocation`/`tool_invocation`/`computerUse`/
1272
+ * `computer_use`, with `type` set to one of `TOOL_INVOCATION_TYPES`.
1273
+ *
1274
+ * Provider-native shapes that adapters are expected to unwrap:
1275
+ *
1276
+ * OpenAI Responses / Chat Completions:
1277
+ * `{id, type: "function", function: {name, arguments}}` →
1278
+ * adapter wraps into an internal tool-invocation event with `toolInvocation: {
1279
+ * toolCallId: id, toolName: function.name, args: function.arguments}}`
1280
+ *
1281
+ * Anthropic tool_use blocks:
1282
+ * `{type: "tool_use", id, name, input}` →
1283
+ * adapter wraps into an internal tool-invocation event with `toolInvocation`.
1284
+ *
1285
+ * The flat-with-`id` fallback (`source.id` in the callId chain) is
1286
+ * a defensive safety net for cases where an adapter forwards the
1287
+ * provider's flat shape verbatim. It is NOT a substitute for
1288
+ * adapter-side normalization — the literal OpenAI
1289
+ * `{type: "function", function: {...}}` wrapper is NOT recognized
1290
+ * here because `"function"` is intentionally not in
1291
+ * TOOL_INVOCATION_TYPES (the word collides with too many
1292
+ * non-tool-call uses of `function` as a top-level key).
1293
+ */
1294
+ function normalizeRawToolInvocation(event, timestamp) {
1295
+ const top = event;
1296
+ const wrapped = top.toolInvocation ?? top.tool_invocation ?? top.computerUse ?? top.computer_use;
1297
+ const innerType = typeof top.type === "string" ? top.type : void 0;
1298
+ const sourceType = typeof top.event === "object" && top.event !== null ? top.event.type : void 0;
1299
+ const isToolType = innerType && TOOL_INVOCATION_TYPES.has(innerType) || sourceType && TOOL_INVOCATION_TYPES.has(sourceType);
1300
+ if (!wrapped && !isToolType) return null;
1301
+ const source = wrapped ?? top;
1302
+ const isComputerUse = innerType === "computer-use" || innerType === "computer_call" || sourceType === "computer-use" || sourceType === "computer_call";
1303
+ const callId = String(source.toolCallId ?? source.tool_call_id ?? source.callId ?? source.callID ?? source.id ?? "");
1304
+ const baseName = String(source.toolName ?? source.tool_name ?? source.name ?? "");
1305
+ const toolName = isComputerUse ? `computer-use:${source.action?.type || "action"}` : baseName;
1306
+ if (!callId || !toolName) return null;
1307
+ const stateObj = source.state ?? void 0;
1308
+ const input = source.args ?? source.arguments ?? source.input ?? stateObj?.input ?? {};
1309
+ const output = source.result ?? source.output ?? stateObj?.output;
1310
+ const errorMsg = typeof source.error === "string" ? source.error : typeof stateObj?.error === "string" ? stateObj.error : void 0;
1311
+ const stateStatus = typeof stateObj?.status === "string" ? stateObj.status : void 0;
1312
+ const explicitStatus = typeof source.status === "string" ? source.status : void 0;
1313
+ const status = stateStatus ?? explicitStatus ?? (output !== void 0 ? "completed" : "running");
1314
+ let toolState;
1315
+ switch (status) {
1316
+ case "completed":
1317
+ toolState = {
1318
+ status: "completed",
1319
+ input: input ?? {},
1320
+ output: output ?? null
1321
+ };
1322
+ break;
1323
+ case "queued":
1324
+ case "pending":
1325
+ toolState = {
1326
+ status: "pending",
1327
+ input: input ?? {}
1328
+ };
1329
+ break;
1330
+ case "running":
1331
+ case "in_progress":
1332
+ toolState = {
1333
+ status: "running",
1334
+ input: input ?? {}
1335
+ };
1336
+ break;
1337
+ case "error":
1338
+ case "failed":
1339
+ toolState = {
1340
+ status,
1341
+ input: input ?? {},
1342
+ output,
1343
+ error: errorMsg
1344
+ };
1345
+ break;
1346
+ default: toolState = {
1347
+ status: "running",
1348
+ input: input ?? {}
1349
+ };
1350
+ }
1351
+ return {
1352
+ type: "message.part.updated",
1353
+ part: {
1354
+ id: callId,
1355
+ sessionID: "",
1356
+ messageID: "",
1357
+ type: "tool",
1358
+ callID: callId,
1359
+ tool: toolName,
1360
+ state: toolState
1361
+ },
1362
+ timestamp
1363
+ };
1364
+ }
1365
+ /**
1366
+ * Convert SSE event data to a typed TraceEvent.
1367
+ * Returns null for events that should be skipped (heartbeats, status-only, etc.)
1368
+ *
1369
+ * message.part.updated events are passed through directly - no transformation.
1370
+ * This preserves full fidelity to what OpenCode outputs.
1371
+ *
1372
+ * `raw` events that carry a recognized tool-invocation payload are
1373
+ * normalized into the same `message.part.updated` shape (with a
1374
+ * `ToolPart`) so consumers don't need to reach into provider-specific
1375
+ * raw shapes to render tool calls. Unrecognized raw events fall
1376
+ * through to a `custom` trace event.
1377
+ */
1378
+ function sseToTraceEvent(event) {
1379
+ const timestamp = /* @__PURE__ */ new Date();
1380
+ if (event.type === "raw" || typeof event.type === "string" && TOOL_INVOCATION_TYPES.has(event.type)) {
1381
+ const normalized = normalizeRawToolInvocation(event, timestamp);
1382
+ if (normalized) {
1383
+ if (normalized.part.state?.status === "pending") return null;
1384
+ return normalized;
1385
+ }
1386
+ const customName = typeof event.type === "string" ? event.type : "raw";
1387
+ return {
1388
+ type: "custom",
1389
+ timestamp,
1390
+ name: customName,
1391
+ data: {
1392
+ eventType: customName,
1393
+ ...event
1394
+ }
1395
+ };
1396
+ }
1397
+ switch (event.type) {
1398
+ case "message.part.updated": {
1399
+ const part = event.part;
1400
+ if (!part) return null;
1401
+ if (part.type === "tool") {
1402
+ const status = part.state?.status;
1403
+ if (status === "pending" || status === "queued") return null;
1404
+ }
1405
+ return {
1406
+ type: "message.part.updated",
1407
+ part,
1408
+ timestamp,
1409
+ delta: event.delta,
1410
+ _meta: event._meta
1411
+ };
1412
+ }
1413
+ case "error": return {
1414
+ type: "error",
1415
+ timestamp,
1416
+ category: "runtime",
1417
+ message: String(event.message || event.error || "Unknown error"),
1418
+ code: typeof event.code === "string" ? event.code : void 0
1419
+ };
1420
+ case "status": return null;
1421
+ case "result": {
1422
+ const finalText = typeof event.finalText === "string" ? event.finalText : typeof event.text === "string" ? event.text : void 0;
1423
+ return {
1424
+ type: "message.updated",
1425
+ timestamp,
1426
+ text: finalText,
1427
+ finalText,
1428
+ tokenUsage: event.tokenUsage,
1429
+ timing: event.timing,
1430
+ toolInvocations: event.toolInvocations,
1431
+ metadata: event.metadata
1432
+ };
1433
+ }
1434
+ case "done": return null;
1435
+ default:
1436
+ if (event.type && event.type !== "heartbeat") return {
1437
+ type: "custom",
1438
+ timestamp,
1439
+ name: event.type,
1440
+ data: {
1441
+ eventType: event.type,
1442
+ ...event
1443
+ }
1444
+ };
1445
+ return null;
1446
+ }
1447
+ }
1448
+ /**
1449
+ * Extract a signal from an SSE event if present.
1450
+ */
1451
+ function sseToSignal(event) {
1452
+ if (event.type === "status" && typeof event.status === "string") {
1453
+ if (VALID_SIGNALS.includes(event.status)) return {
1454
+ signal: event.status,
1455
+ timestamp: /* @__PURE__ */ new Date()
1456
+ };
1457
+ }
1458
+ return null;
1459
+ }
1460
+ /**
1461
+ * Check if an SSE event indicates completion (success or failure).
1462
+ */
1463
+ function isCompletionEvent(event) {
1464
+ if (event.type === "done" || event.type === "result") return {
1465
+ complete: true,
1466
+ success: !("success" in event && event.success === false)
1467
+ };
1468
+ if (event.type === "error") return {
1469
+ complete: true,
1470
+ success: false
1471
+ };
1472
+ return {
1473
+ complete: false,
1474
+ success: false
1475
+ };
1476
+ }
1477
+ //#endregion
1478
+ //#region src/trace-collector.ts
1479
+ const randomUUID = () => {
1480
+ const webCrypto = globalThis.crypto;
1481
+ if (webCrypto?.randomUUID) return webCrypto.randomUUID();
1482
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
1483
+ const r = Math.random() * 16 | 0;
1484
+ return (ch === "x" ? r : r & 3 | 8).toString(16);
1485
+ });
1486
+ };
1487
+ var TraceCollector = class {
1488
+ sink;
1489
+ config;
1490
+ activeTraces = /* @__PURE__ */ new Map();
1491
+ completedTraces = [];
1492
+ flushTimer;
1493
+ sequence = 0;
1494
+ constructor(config) {
1495
+ this.sink = config.sink;
1496
+ this.config = {
1497
+ maxBufferSize: config.maxBufferSize ?? 100,
1498
+ flushIntervalMs: config.flushIntervalMs ?? 0,
1499
+ defaultScope: config.defaultScope
1500
+ };
1501
+ if (this.config.flushIntervalMs > 0) this.flushTimer = setInterval(() => {
1502
+ this.flush().catch(() => {});
1503
+ }, this.config.flushIntervalMs);
1504
+ }
1505
+ /** Start tracking a new execution */
1506
+ startTrace(task, scope, metadata) {
1507
+ const traceId = `trace_${randomUUID()}`;
1508
+ const startedAt = /* @__PURE__ */ new Date();
1509
+ const activeTrace = {
1510
+ id: traceId,
1511
+ scope: scope ?? this.config.defaultScope ?? {},
1512
+ task,
1513
+ events: [],
1514
+ signals: [],
1515
+ startedAt,
1516
+ metadata
1517
+ };
1518
+ this.activeTraces.set(traceId, activeTrace);
1519
+ return {
1520
+ traceId,
1521
+ startedAt,
1522
+ addEvent: (event) => {
1523
+ const trace = this.activeTraces.get(traceId);
1524
+ if (trace) trace.events.push({
1525
+ ...event,
1526
+ timestamp: /* @__PURE__ */ new Date()
1527
+ });
1528
+ },
1529
+ addSignal: (signal, signalMeta) => {
1530
+ const trace = this.activeTraces.get(traceId);
1531
+ if (trace) trace.signals.push({
1532
+ signal,
1533
+ timestamp: /* @__PURE__ */ new Date(),
1534
+ metadata: signalMeta
1535
+ });
1536
+ },
1537
+ complete: (completeMeta) => {
1538
+ return this.completeTrace(traceId, "success", completeMeta);
1539
+ },
1540
+ fail: (error, failMeta) => {
1541
+ const trace = this.activeTraces.get(traceId);
1542
+ if (trace) {
1543
+ const errorMsg = error instanceof Error ? error.message : error;
1544
+ trace.events.push({
1545
+ type: "error",
1546
+ timestamp: /* @__PURE__ */ new Date(),
1547
+ category: "runtime",
1548
+ message: errorMsg,
1549
+ stack: error instanceof Error ? error.stack : void 0
1550
+ });
1551
+ trace.signals.push({
1552
+ signal: "runtime_error",
1553
+ timestamp: /* @__PURE__ */ new Date()
1554
+ });
1555
+ }
1556
+ return this.completeTrace(traceId, "failure", failMeta);
1557
+ },
1558
+ abandon: () => {
1559
+ return this.completeTrace(traceId, "unknown", { abandoned: true });
1560
+ }
1561
+ };
1562
+ }
1563
+ /** Convenience: track a complete execution with a wrapper function */
1564
+ async track(task, fn, options) {
1565
+ const ctx = this.startTrace(task, options?.scope, options?.metadata);
1566
+ try {
1567
+ return {
1568
+ result: await fn(ctx),
1569
+ trace: ctx.complete()
1570
+ };
1571
+ } catch (error) {
1572
+ ctx.fail(error instanceof Error ? error : String(error));
1573
+ throw error;
1574
+ }
1575
+ }
1576
+ /** Flush completed traces to sink */
1577
+ async flush() {
1578
+ if (this.completedTraces.length === 0) return;
1579
+ const events = this.completedTraces.map((trace) => ({
1580
+ id: `evt_${randomUUID()}`,
1581
+ type: "agent_execution",
1582
+ name: "execution_trace",
1583
+ projectRef: trace.scope.projectRef,
1584
+ sessionId: trace.scope.sessionId,
1585
+ userId: trace.scope.userId,
1586
+ data: {
1587
+ traceId: trace.id,
1588
+ task: trace.task,
1589
+ outcome: trace.outcome,
1590
+ eventCount: trace.events.length,
1591
+ signalCount: trace.signals.length,
1592
+ durationMs: trace.completedAt ? trace.completedAt.getTime() - trace.startedAt.getTime() : void 0,
1593
+ trace
1594
+ },
1595
+ timestamp: trace.completedAt ?? /* @__PURE__ */ new Date(),
1596
+ sequence: this.sequence++
1597
+ }));
1598
+ await this.sink.send(events);
1599
+ this.completedTraces = [];
1600
+ }
1601
+ /** Get count of pending traces */
1602
+ getPendingCount() {
1603
+ return this.completedTraces.length;
1604
+ }
1605
+ /** Get active trace count */
1606
+ getActiveCount() {
1607
+ return this.activeTraces.size;
1608
+ }
1609
+ /** Shutdown collector */
1610
+ async shutdown() {
1611
+ if (this.flushTimer) clearInterval(this.flushTimer);
1612
+ await this.flush();
1613
+ await this.sink.flush();
1614
+ await this.sink.close();
1615
+ }
1616
+ completeTrace(traceId, outcome, metadata) {
1617
+ const active = this.activeTraces.get(traceId);
1618
+ if (!active) throw new Error(`Trace not found: ${traceId}`);
1619
+ this.activeTraces.delete(traceId);
1620
+ const completedTrace = {
1621
+ id: active.id,
1622
+ scope: active.scope,
1623
+ task: active.task,
1624
+ events: active.events,
1625
+ signals: active.signals,
1626
+ outcome: this.classifyOutcome(active.signals, outcome),
1627
+ startedAt: active.startedAt,
1628
+ completedAt: /* @__PURE__ */ new Date(),
1629
+ metadata: {
1630
+ ...active.metadata,
1631
+ ...metadata
1632
+ }
1633
+ };
1634
+ this.completedTraces.push(completedTrace);
1635
+ if (this.completedTraces.length >= this.config.maxBufferSize) this.flush().catch(() => {});
1636
+ return completedTrace;
1637
+ }
1638
+ classifyOutcome(signals, defaultOutcome) {
1639
+ const signalSet = new Set(signals.map((s) => s.signal));
1640
+ if (signalSet.has("task_completed")) {
1641
+ if (signalSet.has("tests_passed") || signalSet.has("build_passed") || signalSet.has("user_approved")) return "success";
1642
+ return "partial";
1643
+ }
1644
+ if (signalSet.has("runtime_error") || signalSet.has("timeout") || signalSet.has("tests_failed") || signalSet.has("build_failed") || signalSet.has("user_rejected")) return "failure";
1645
+ if (signalSet.has("task_abandoned")) return "unknown";
1646
+ return defaultOutcome;
1647
+ }
1648
+ };
1649
+ function createTraceCollector(config) {
1650
+ return new TraceCollector(config);
1651
+ }
1652
+ //#endregion
1653
+ //#region src/tracker.ts
1654
+ const DEFAULT_CONFIG$1 = {
1655
+ enabled: true,
1656
+ flushIntervalMs: 3e4,
1657
+ maxBatchSize: 100,
1658
+ maxQueueSize: 1e3,
1659
+ sampleRate: 1,
1660
+ excludePatterns: [],
1661
+ includeMetadata: true,
1662
+ persistOffline: true
1663
+ };
1664
+ var TelemetryTracker = class {
1665
+ config;
1666
+ sinks = [];
1667
+ queue = [];
1668
+ storage;
1669
+ sequence = 0;
1670
+ flushTimer;
1671
+ sessionStartTime;
1672
+ isFlushing = false;
1673
+ constructor(config, storage) {
1674
+ this.config = {
1675
+ ...DEFAULT_CONFIG$1,
1676
+ ...config
1677
+ };
1678
+ this.storage = storage;
1679
+ if (this.config.enabled && this.config.flushIntervalMs > 0) this.startFlushTimer();
1680
+ }
1681
+ addSink(sink) {
1682
+ this.sinks.push(sink);
1683
+ return this;
1684
+ }
1685
+ removeSink(name) {
1686
+ const index = this.sinks.findIndex((s) => s.name === name);
1687
+ if (index !== -1) this.sinks.splice(index, 1);
1688
+ return this;
1689
+ }
1690
+ startSession(metadata) {
1691
+ const sessionId = this.config.sessionId ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1692
+ this.sessionStartTime = /* @__PURE__ */ new Date();
1693
+ this.track("session_start", "session.started", {
1694
+ ...metadata,
1695
+ sessionId,
1696
+ startTime: this.sessionStartTime.toISOString()
1697
+ });
1698
+ return sessionId;
1699
+ }
1700
+ endSession(metadata) {
1701
+ const duration = this.sessionStartTime ? Date.now() - this.sessionStartTime.getTime() : 0;
1702
+ this.track("session_end", "session.ended", {
1703
+ ...metadata,
1704
+ durationMs: duration,
1705
+ eventCount: this.sequence
1706
+ });
1707
+ this.flush();
1708
+ }
1709
+ track(type, name, data = {}) {
1710
+ if (!this.config.enabled) return;
1711
+ if (!this.shouldSample()) return;
1712
+ if (this.isExcluded(name)) return;
1713
+ const event = {
1714
+ id: `evt-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
1715
+ type,
1716
+ name,
1717
+ projectRef: this.config.projectRef,
1718
+ sessionId: this.config.sessionId,
1719
+ userId: this.config.userId,
1720
+ data: this.config.includeMetadata ? this.enrichData(data) : data,
1721
+ timestamp: /* @__PURE__ */ new Date(),
1722
+ sequence: ++this.sequence
1723
+ };
1724
+ this.enqueue(event);
1725
+ }
1726
+ trackPerformance(metric) {
1727
+ this.track("performance", `perf.${metric.name}`, {
1728
+ value: metric.value,
1729
+ unit: metric.unit,
1730
+ tags: metric.tags
1731
+ });
1732
+ }
1733
+ trackError(error, context) {
1734
+ const errorData = error instanceof Error ? {
1735
+ message: error.message,
1736
+ name: error.name,
1737
+ stack: error.stack
1738
+ } : { message: error };
1739
+ this.track("error", "error.occurred", {
1740
+ ...errorData,
1741
+ ...context
1742
+ });
1743
+ }
1744
+ trackApiRequest(method, path, status, durationMs, metadata) {
1745
+ this.track("api_request", `api.${method.toLowerCase()}.${path}`, {
1746
+ method,
1747
+ path,
1748
+ status,
1749
+ durationMs,
1750
+ ...metadata
1751
+ });
1752
+ }
1753
+ trackAgentExecution(identifier, executionId, durationMs, success, metadata) {
1754
+ this.track("agent_execution", `agent.${identifier}`, {
1755
+ identifier,
1756
+ executionId,
1757
+ durationMs,
1758
+ success,
1759
+ ...metadata
1760
+ });
1761
+ }
1762
+ async flush() {
1763
+ if (this.isFlushing || this.queue.length === 0) return;
1764
+ this.isFlushing = true;
1765
+ try {
1766
+ const events = this.queue.splice(0, this.config.maxBatchSize);
1767
+ await Promise.all(this.sinks.map((sink) => sink.send(events)));
1768
+ if (this.config.persistOffline && this.storage) await this.clearPersistedEvents(events.map((e) => e.id));
1769
+ } catch (_error) {
1770
+ if (this.config.persistOffline && this.storage) await this.persistEvents(this.queue);
1771
+ } finally {
1772
+ this.isFlushing = false;
1773
+ }
1774
+ }
1775
+ async close() {
1776
+ this.stopFlushTimer();
1777
+ await this.flush();
1778
+ await Promise.all(this.sinks.map((sink) => sink.close()));
1779
+ }
1780
+ getQueueSize() {
1781
+ return this.queue.length;
1782
+ }
1783
+ isEnabled() {
1784
+ return this.config.enabled;
1785
+ }
1786
+ enable() {
1787
+ this.config.enabled = true;
1788
+ this.startFlushTimer();
1789
+ }
1790
+ disable() {
1791
+ this.config.enabled = false;
1792
+ this.stopFlushTimer();
1793
+ }
1794
+ enqueue(event) {
1795
+ if (this.queue.length >= this.config.maxQueueSize) this.queue.shift();
1796
+ this.queue.push(event);
1797
+ if (this.queue.length >= this.config.maxBatchSize) this.flush();
1798
+ if (this.config.persistOffline && this.storage) this.persistEvent(event);
1799
+ }
1800
+ shouldSample() {
1801
+ return Math.random() < this.config.sampleRate;
1802
+ }
1803
+ isExcluded(name) {
1804
+ return this.config.excludePatterns.some((pattern) => {
1805
+ if (pattern.includes("*")) return new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(name);
1806
+ return name === pattern;
1807
+ });
1808
+ }
1809
+ enrichData(data) {
1810
+ return {
1811
+ ...data,
1812
+ _meta: {
1813
+ timestamp: Date.now(),
1814
+ platform: typeof navigator !== "undefined" ? navigator.userAgent : "node",
1815
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
1816
+ }
1817
+ };
1818
+ }
1819
+ startFlushTimer() {
1820
+ if (this.flushTimer) return;
1821
+ this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
1822
+ }
1823
+ stopFlushTimer() {
1824
+ if (this.flushTimer) {
1825
+ clearInterval(this.flushTimer);
1826
+ this.flushTimer = void 0;
1827
+ }
1828
+ }
1829
+ async persistEvent(event) {
1830
+ if (!this.storage) return;
1831
+ const key = `telemetry:${event.id}`;
1832
+ await this.storage.set(key, event);
1833
+ }
1834
+ async persistEvents(events) {
1835
+ if (!this.storage) return;
1836
+ await Promise.all(events.map((e) => this.persistEvent(e)));
1837
+ }
1838
+ async clearPersistedEvents(ids) {
1839
+ if (!this.storage) return;
1840
+ await Promise.all(ids.map((id) => this.storage?.delete(`telemetry:${id}`)));
1841
+ }
1842
+ async loadPersistedEvents() {
1843
+ if (!this.storage) return [];
1844
+ const telemetryKeys = (await this.storage.keys()).filter((k) => k.startsWith("telemetry:"));
1845
+ const events = [];
1846
+ for (const key of telemetryKeys) {
1847
+ const event = await this.storage.get(key);
1848
+ if (event) events.push(event);
1849
+ }
1850
+ return events.sort((a, b) => a.sequence - b.sequence);
1851
+ }
1852
+ };
1853
+ function createTelemetryTracker(config, storage) {
1854
+ return new TelemetryTracker(config, storage);
1855
+ }
1856
+ //#endregion
1857
+ //#region src/usage.ts
1858
+ const DEFAULT_CONFIG = {
1859
+ autoTrack: true,
1860
+ trackApiCalls: true,
1861
+ trackAgentExecutions: true,
1862
+ trackErrors: true,
1863
+ batchSize: 50,
1864
+ flushIntervalMs: 3e4
1865
+ };
1866
+ const OPERATION_COSTS = {
1867
+ "api.get": .001,
1868
+ "api.post": .002,
1869
+ "api.put": .002,
1870
+ "api.delete": .001,
1871
+ "agent.execution": .01,
1872
+ "agent.streaming": .015,
1873
+ "token.input": 1e-5,
1874
+ "token.output": 3e-5,
1875
+ default: .001
1876
+ };
1877
+ var UsageTracker = class {
1878
+ config;
1879
+ provider;
1880
+ storage;
1881
+ queue = [];
1882
+ flushTimer;
1883
+ isFlushing = false;
1884
+ sessionMetrics = {
1885
+ totalCredits: 0,
1886
+ totalOperations: 0,
1887
+ totalDurationMs: 0,
1888
+ totalInputTokens: 0,
1889
+ totalOutputTokens: 0,
1890
+ operationCounts: /* @__PURE__ */ new Map(),
1891
+ startTime: Date.now()
1892
+ };
1893
+ constructor(config, provider, storage) {
1894
+ this.config = {
1895
+ ...DEFAULT_CONFIG,
1896
+ ...config
1897
+ };
1898
+ this.provider = provider;
1899
+ this.storage = storage;
1900
+ if (this.config.flushIntervalMs > 0) this.startFlushTimer();
1901
+ }
1902
+ recordApiCall(metric) {
1903
+ if (!this.config.trackApiCalls) return;
1904
+ const operation = `api.${metric.method.toLowerCase()}`;
1905
+ const credits = this.calculateCost(operation, {
1906
+ durationMs: metric.durationMs,
1907
+ requestSize: metric.requestSize,
1908
+ responseSize: metric.responseSize
1909
+ });
1910
+ this.record({
1911
+ projectRef: this.config.projectRef,
1912
+ userId: this.config.userId,
1913
+ operation,
1914
+ credits,
1915
+ durationMs: metric.durationMs,
1916
+ metadata: {
1917
+ path: metric.path,
1918
+ status: metric.status,
1919
+ requestSize: metric.requestSize,
1920
+ responseSize: metric.responseSize,
1921
+ error: metric.error
1922
+ }
1923
+ });
1924
+ }
1925
+ recordAgentExecution(metric) {
1926
+ if (!this.config.trackAgentExecutions) return;
1927
+ const operation = "agent.execution";
1928
+ const tokenCost = (metric.inputTokens ?? 0) * OPERATION_COSTS["token.input"] + (metric.outputTokens ?? 0) * OPERATION_COSTS["token.output"];
1929
+ const credits = this.calculateCost(operation, { durationMs: metric.durationMs }) + tokenCost;
1930
+ this.record({
1931
+ projectRef: metric.projectRef,
1932
+ userId: this.config.userId,
1933
+ operation,
1934
+ credits,
1935
+ durationMs: metric.durationMs,
1936
+ inputTokens: metric.inputTokens,
1937
+ outputTokens: metric.outputTokens,
1938
+ metadata: {
1939
+ identifier: metric.identifier,
1940
+ executionId: metric.executionId,
1941
+ toolCalls: metric.toolCalls,
1942
+ success: metric.success,
1943
+ error: metric.error
1944
+ }
1945
+ });
1946
+ }
1947
+ recordError(error, operation) {
1948
+ if (!this.config.trackErrors) return;
1949
+ this.record({
1950
+ projectRef: this.config.projectRef,
1951
+ userId: this.config.userId,
1952
+ operation: `error.${operation}`,
1953
+ credits: 0,
1954
+ metadata: {
1955
+ error: error instanceof Error ? error.message : error,
1956
+ stack: error instanceof Error ? error.stack : void 0
1957
+ }
1958
+ });
1959
+ }
1960
+ record(usage) {
1961
+ this.queue.push(usage);
1962
+ this.sessionMetrics.totalCredits += usage.credits;
1963
+ this.sessionMetrics.totalOperations++;
1964
+ this.sessionMetrics.totalDurationMs += usage.durationMs ?? 0;
1965
+ this.sessionMetrics.totalInputTokens += usage.inputTokens ?? 0;
1966
+ this.sessionMetrics.totalOutputTokens += usage.outputTokens ?? 0;
1967
+ const count = this.sessionMetrics.operationCounts.get(usage.operation) ?? 0;
1968
+ this.sessionMetrics.operationCounts.set(usage.operation, count + 1);
1969
+ if (this.queue.length >= this.config.batchSize) this.flush();
1970
+ }
1971
+ async flush() {
1972
+ if (this.isFlushing || this.queue.length === 0) return;
1973
+ this.isFlushing = true;
1974
+ const records = this.queue.splice(0, this.config.batchSize);
1975
+ const providerFailed = /* @__PURE__ */ new Set();
1976
+ const providerDone = /* @__PURE__ */ new Set();
1977
+ try {
1978
+ if (this.provider) {
1979
+ const results = await Promise.allSettled(records.map((r) => this.provider?.recordUsage(r)));
1980
+ for (let i = 0; i < records.length; i++) if (results[i].status === "rejected") providerFailed.add(records[i]);
1981
+ else providerDone.add(records[i]);
1982
+ if (providerFailed.size > 0) this.queue.unshift(...providerFailed);
1983
+ }
1984
+ if (this.storage) {
1985
+ const toPersist = providerFailed.size > 0 ? records.filter((r) => !providerFailed.has(r)) : records;
1986
+ if (toPersist.length > 0) await this.persistRecords(toPersist);
1987
+ }
1988
+ } catch (error) {
1989
+ const firstFailed = typeof error?.firstFailedIndex === "number" ? error.firstFailedIndex : 0;
1990
+ const unwritten = records.slice(firstFailed);
1991
+ if (providerDone.size > 0) console.warn(`[usage-tracker] storage persistence failed for ${providerDone.size} provider-accepted records — local audit trail incomplete`, error instanceof Error ? error.message : String(error));
1992
+ const unhandled = unwritten.filter((r) => !providerFailed.has(r) && !providerDone.has(r));
1993
+ if (unhandled.length > 0) this.queue.unshift(...unhandled);
1994
+ } finally {
1995
+ this.isFlushing = false;
1996
+ }
1997
+ }
1998
+ async getUsage(options) {
1999
+ if (this.provider) return this.provider.getUsage({
2000
+ projectRef: this.config.projectRef,
2001
+ ...options
2002
+ });
2003
+ return this.loadPersistedRecords(options);
2004
+ }
2005
+ async getSummary(period = "day") {
2006
+ if (this.provider) return this.provider.getSummary(this.config.projectRef, period);
2007
+ return this.generateLocalSummary(period);
2008
+ }
2009
+ getSessionMetrics() {
2010
+ return {
2011
+ totalCredits: this.sessionMetrics.totalCredits,
2012
+ totalOperations: this.sessionMetrics.totalOperations,
2013
+ totalDurationMs: this.sessionMetrics.totalDurationMs,
2014
+ totalInputTokens: this.sessionMetrics.totalInputTokens,
2015
+ totalOutputTokens: this.sessionMetrics.totalOutputTokens,
2016
+ operationBreakdown: Object.fromEntries(this.sessionMetrics.operationCounts),
2017
+ sessionDurationMs: Date.now() - this.sessionMetrics.startTime
2018
+ };
2019
+ }
2020
+ resetSessionMetrics() {
2021
+ this.sessionMetrics = {
2022
+ totalCredits: 0,
2023
+ totalOperations: 0,
2024
+ totalDurationMs: 0,
2025
+ totalInputTokens: 0,
2026
+ totalOutputTokens: 0,
2027
+ operationCounts: /* @__PURE__ */ new Map(),
2028
+ startTime: Date.now()
2029
+ };
2030
+ }
2031
+ async close() {
2032
+ this.stopFlushTimer();
2033
+ await this.flush();
2034
+ }
2035
+ calculateCost(operation, factors) {
2036
+ let cost = OPERATION_COSTS[operation] ?? OPERATION_COSTS.default;
2037
+ if (factors.durationMs) cost += factors.durationMs / 1e3 * .001;
2038
+ if (factors.requestSize) cost += factors.requestSize / 1024 * 1e-4;
2039
+ if (factors.responseSize) cost += factors.responseSize / 1024 * 1e-4;
2040
+ return Math.round(cost * 1e5) / 1e5;
2041
+ }
2042
+ async persistRecords(records) {
2043
+ if (!this.storage) return;
2044
+ const timestamp = Date.now();
2045
+ for (let i = 0; i < records.length; i++) {
2046
+ const record = records[i];
2047
+ const id = `usage-${timestamp}-${i}`;
2048
+ try {
2049
+ await this.storage.set(`usage:${id}`, {
2050
+ ...record,
2051
+ id,
2052
+ timestamp: /* @__PURE__ */ new Date()
2053
+ });
2054
+ } catch (err) {
2055
+ throw Object.assign(err instanceof Error ? err : new Error(String(err)), { firstFailedIndex: i });
2056
+ }
2057
+ }
2058
+ }
2059
+ async loadPersistedRecords(options) {
2060
+ if (!this.storage) return [];
2061
+ const usageKeys = (await this.storage.keys()).filter((k) => k.startsWith("usage:"));
2062
+ const records = [];
2063
+ for (const key of usageKeys) {
2064
+ const record = await this.storage.get(key);
2065
+ if (!record) continue;
2066
+ if (options?.since && record.timestamp < options.since) continue;
2067
+ if (options?.until && record.timestamp > options.until) continue;
2068
+ records.push(record);
2069
+ }
2070
+ records.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2071
+ return options?.limit ? records.slice(0, options.limit) : records;
2072
+ }
2073
+ async generateLocalSummary(period) {
2074
+ const now = /* @__PURE__ */ new Date();
2075
+ const periodStart = this.getPeriodStart(now, period);
2076
+ const records = await this.loadPersistedRecords({
2077
+ since: periodStart,
2078
+ until: now
2079
+ });
2080
+ const operationBreakdown = {};
2081
+ let totalCredits = 0;
2082
+ let totalDurationMs = 0;
2083
+ let totalInputTokens = 0;
2084
+ let totalOutputTokens = 0;
2085
+ for (const record of records) {
2086
+ totalCredits += record.credits;
2087
+ totalDurationMs += record.durationMs ?? 0;
2088
+ totalInputTokens += record.inputTokens ?? 0;
2089
+ totalOutputTokens += record.outputTokens ?? 0;
2090
+ operationBreakdown[record.operation] = (operationBreakdown[record.operation] ?? 0) + 1;
2091
+ }
2092
+ return {
2093
+ projectRef: this.config.projectRef,
2094
+ period,
2095
+ periodStart,
2096
+ periodEnd: now,
2097
+ totalCredits,
2098
+ totalOperations: records.length,
2099
+ totalDurationMs,
2100
+ totalInputTokens,
2101
+ totalOutputTokens,
2102
+ operationBreakdown
2103
+ };
2104
+ }
2105
+ getPeriodStart(now, period) {
2106
+ const start = new Date(now);
2107
+ switch (period) {
2108
+ case "hour":
2109
+ start.setMinutes(0, 0, 0);
2110
+ break;
2111
+ case "day":
2112
+ start.setHours(0, 0, 0, 0);
2113
+ break;
2114
+ case "week":
2115
+ start.setDate(start.getDate() - start.getDay());
2116
+ start.setHours(0, 0, 0, 0);
2117
+ break;
2118
+ case "month":
2119
+ start.setDate(1);
2120
+ start.setHours(0, 0, 0, 0);
2121
+ break;
2122
+ }
2123
+ return start;
2124
+ }
2125
+ startFlushTimer() {
2126
+ if (this.flushTimer) return;
2127
+ this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
2128
+ }
2129
+ stopFlushTimer() {
2130
+ if (this.flushTimer) {
2131
+ clearInterval(this.flushTimer);
2132
+ this.flushTimer = void 0;
2133
+ }
2134
+ }
2135
+ };
2136
+ function createUsageInterceptor(tracker) {
2137
+ return {
2138
+ onRequest(ctx) {
2139
+ return ctx;
2140
+ },
2141
+ onResponse(ctx) {
2142
+ const durationMs = Date.now() - ctx.startedAt;
2143
+ tracker.recordApiCall({
2144
+ path: new URL(ctx.request.url).pathname,
2145
+ method: ctx.request.method,
2146
+ status: ctx.response.status,
2147
+ durationMs,
2148
+ timestamp: /* @__PURE__ */ new Date()
2149
+ });
2150
+ },
2151
+ onError(ctx) {
2152
+ const durationMs = Date.now() - ctx.startedAt;
2153
+ tracker.recordApiCall({
2154
+ path: new URL(ctx.request.url).pathname,
2155
+ method: ctx.request.method,
2156
+ status: 0,
2157
+ durationMs,
2158
+ error: ctx.error.message,
2159
+ timestamp: /* @__PURE__ */ new Date()
2160
+ });
2161
+ }
2162
+ };
2163
+ }
2164
+ function createUsageTracker(config, provider, storage) {
2165
+ return new UsageTracker(config, provider, storage);
2166
+ }
2167
+ //#endregion
2168
+ export { AdapterRegistry, ConsoleTelemetrySink, CreditTracker, GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_TOKEN_KEYS, GEN_AI_MODEL_KEYS, GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_TOKEN_KEYS, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, HttpTelemetrySink, LangfuseSink, MemoryCreditProvider, MultiTelemetrySink, OtelTelemetrySink, PROVISION_TRACE_ID_HEADER, PTID_PREFIX, SSEChunkParser, SpanRecorder, StreamUsageExtractor, TOKEN_USAGE_COST_KEYS, TOKEN_USAGE_INPUT_KEYS, TOKEN_USAGE_OUTPUT_KEYS, TelemetryTracker, TestTelemetrySink, TraceCollector, UsageTracker, addTokenUsage, consumeSSEStream, createConsoleSink, createCreditTracker, createEmptyContext, createHttpSink, createJsonAdapter, createLangfuseSinkFromEnv, createOtelSink, createOtelSinkFromEnv, createStreamUsageExtractor, createTelemetryTracker, createTestSink, createTraceCollector, createUsageCallback, createUsageInterceptor, createUsageTracker, firstTokenCount, firstUsageCostUsd, genAiUsageAttributes, getDefaultRegistry, initOtelProvider, initializeDefaultAdapters, isAdoptablePtid, isBuildResultEvent, isCompletionEvent, isCustomEvent, isDiagnosticEvent, isEnvironmentEvent, isErrorEvent, isFileChangeEvent, isFilePart, isMessagePartUpdatedEvent, isMessageUpdatedEvent, isReasoningPart, isTestResultEvent, isTextPart, isToolPart, jsonAdapter, mintPtid, monotonicNs, parseMetadataBlob, parseSSEData, parseSSEStream, parseServerTiming, readOrMintPtid, readTokenCostUsd, readTokenUsage, resetDefaultRegistry, shutdownOtelProvider, spanDurationMs, sseToSignal, sseToTraceEvent, tokenCount, tokenUsageSource };
2169
+
2170
+ //# sourceMappingURL=index.mjs.map