@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/LICENSE +11 -0
- package/README.md +24 -0
- package/dist/index.d.mts +968 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2170 -0
- package/dist/index.mjs.map +1 -0
- package/dist/stage-span.d.mts +139 -0
- package/dist/stage-span.d.mts.map +1 -0
- package/dist/stage-span.mjs +218 -0
- package/dist/stage-span.mjs.map +1 -0
- package/package.json +69 -0
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
|