@bitfab/sdk 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -0
- package/dist/chunk-62NGOY7Q.js +2408 -0
- package/dist/chunk-62NGOY7Q.js.map +1 -0
- package/dist/chunk-QLVXAFGP.js +406 -0
- package/dist/chunk-QLVXAFGP.js.map +1 -0
- package/dist/index.cjs +3040 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +888 -0
- package/dist/index.d.ts +888 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +3058 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +40 -0
- package/dist/node.js.map +1 -0
- package/dist/replay-SIWSK66F.js +148 -0
- package/dist/replay-SIWSK66F.js.map +1 -0
- package/package.json +88 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3040 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/version.generated.ts
|
|
34
|
+
var __version__;
|
|
35
|
+
var init_version_generated = __esm({
|
|
36
|
+
"src/version.generated.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
__version__ = "0.13.0";
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// src/constants.ts
|
|
43
|
+
var DEFAULT_SERVICE_URL;
|
|
44
|
+
var init_constants = __esm({
|
|
45
|
+
"src/constants.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
init_version_generated();
|
|
48
|
+
DEFAULT_SERVICE_URL = "https://bitfab.ai";
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// src/http.ts
|
|
53
|
+
function awaitOnExit(promise) {
|
|
54
|
+
pendingTracePromises.add(promise);
|
|
55
|
+
void promise.finally(() => {
|
|
56
|
+
pendingTracePromises.delete(promise);
|
|
57
|
+
}).catch(() => {
|
|
58
|
+
});
|
|
59
|
+
return promise;
|
|
60
|
+
}
|
|
61
|
+
async function flushTraces(timeoutMs = 5e3) {
|
|
62
|
+
if (pendingTracePromises.size === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await Promise.race([
|
|
66
|
+
Promise.allSettled(Array.from(pendingTracePromises)),
|
|
67
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
var BitfabError, pendingTracePromises, HttpClient;
|
|
71
|
+
var init_http = __esm({
|
|
72
|
+
"src/http.ts"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
init_constants();
|
|
75
|
+
BitfabError = class extends Error {
|
|
76
|
+
constructor(message, url) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.url = url;
|
|
79
|
+
this.name = "BitfabError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
pendingTracePromises = /* @__PURE__ */ new Set();
|
|
83
|
+
if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
|
|
84
|
+
let isFlushing = false;
|
|
85
|
+
process.on("beforeExit", () => {
|
|
86
|
+
if (pendingTracePromises.size > 0 && !isFlushing) {
|
|
87
|
+
isFlushing = true;
|
|
88
|
+
Promise.allSettled(
|
|
89
|
+
Array.from(pendingTracePromises).map(
|
|
90
|
+
(p) => p.catch(() => {
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
).then(() => {
|
|
94
|
+
isFlushing = false;
|
|
95
|
+
}).catch(() => {
|
|
96
|
+
isFlushing = false;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
HttpClient = class {
|
|
102
|
+
constructor(config) {
|
|
103
|
+
this.apiKey = config.apiKey;
|
|
104
|
+
this.serviceUrl = config.serviceUrl;
|
|
105
|
+
this.timeout = config.timeout ?? 12e4;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Make an HTTP request to the Bitfab API. Defaults to POST; pass
|
|
109
|
+
* `options.method` to use a different verb (e.g. "PATCH").
|
|
110
|
+
*
|
|
111
|
+
* @param endpoint - The API endpoint (without base URL)
|
|
112
|
+
* @param payload - The request body
|
|
113
|
+
* @param options - Optional request options
|
|
114
|
+
* @returns The parsed JSON response
|
|
115
|
+
* @throws {BitfabError} If the request fails
|
|
116
|
+
*/
|
|
117
|
+
async request(endpoint, payload, options) {
|
|
118
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
119
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
120
|
+
const method = options?.method ?? "POST";
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
123
|
+
let body;
|
|
124
|
+
let serializationError;
|
|
125
|
+
try {
|
|
126
|
+
body = JSON.stringify(payload);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
serializationError = error instanceof Error ? error.message : String(error);
|
|
129
|
+
body = JSON.stringify({
|
|
130
|
+
...Object.fromEntries(
|
|
131
|
+
Object.entries(payload).filter(
|
|
132
|
+
([, v]) => typeof v === "string" || typeof v === "number"
|
|
133
|
+
)
|
|
134
|
+
),
|
|
135
|
+
rawSpan: {},
|
|
136
|
+
errors: [{ step: "json_serialize", error: serializationError }]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(url, {
|
|
141
|
+
method,
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
145
|
+
},
|
|
146
|
+
body,
|
|
147
|
+
signal: controller.signal
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const errorText = await response.text();
|
|
151
|
+
throw new BitfabError(
|
|
152
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const result = await response.json();
|
|
156
|
+
if (result.error) {
|
|
157
|
+
if (result.url) {
|
|
158
|
+
throw new BitfabError(
|
|
159
|
+
`${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
|
|
160
|
+
result.url
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
throw new BitfabError(result.error);
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof BitfabError) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (error instanceof Error) {
|
|
171
|
+
if (error.name === "AbortError") {
|
|
172
|
+
throw new BitfabError(`Request timed out after ${timeout}ms`);
|
|
173
|
+
}
|
|
174
|
+
throw new BitfabError(error.message);
|
|
175
|
+
}
|
|
176
|
+
throw new BitfabError("Unknown error occurred");
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Look up a function by name.
|
|
183
|
+
* Blocks until complete - needed for function execution.
|
|
184
|
+
*/
|
|
185
|
+
async lookupFunction(name) {
|
|
186
|
+
return this.request("/api/sdk/functions/lookup", { name });
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Send an internal trace (from BAML execution).
|
|
190
|
+
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
191
|
+
*/
|
|
192
|
+
sendInternalTrace(functionId, payload) {
|
|
193
|
+
void awaitOnExit(
|
|
194
|
+
this.request(`/api/sdk/functions/${functionId}/traces`, {
|
|
195
|
+
...payload,
|
|
196
|
+
sdkVersion: __version__
|
|
197
|
+
})
|
|
198
|
+
).catch((error) => {
|
|
199
|
+
try {
|
|
200
|
+
console.error("Bitfab: Failed to create trace:", error);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Send an external span (from withSpan wrapper or OpenAI tracing).
|
|
207
|
+
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
208
|
+
* Returns the tracked promise so callers can optionally await it.
|
|
209
|
+
*/
|
|
210
|
+
sendExternalSpan(payload) {
|
|
211
|
+
return awaitOnExit(
|
|
212
|
+
this.request("/api/sdk/externalSpans", {
|
|
213
|
+
...payload,
|
|
214
|
+
sdkVersion: __version__
|
|
215
|
+
})
|
|
216
|
+
).catch((error) => {
|
|
217
|
+
try {
|
|
218
|
+
console.error("Bitfab: Failed to create external span:", error);
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Send an external trace (from OpenAI tracing).
|
|
225
|
+
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
226
|
+
*/
|
|
227
|
+
sendExternalTrace(payload) {
|
|
228
|
+
void awaitOnExit(
|
|
229
|
+
this.request("/api/sdk/externalTraces", {
|
|
230
|
+
...payload,
|
|
231
|
+
sdkVersion: __version__
|
|
232
|
+
})
|
|
233
|
+
).catch((error) => {
|
|
234
|
+
try {
|
|
235
|
+
console.error("Bitfab: Failed to create external trace:", error);
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Partial update of an existing external trace identified by sourceTraceId.
|
|
242
|
+
* Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
|
|
243
|
+
* returns a tracked promise that callers may optionally await.
|
|
244
|
+
*/
|
|
245
|
+
patchTrace(sourceTraceId, payload) {
|
|
246
|
+
const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
|
|
247
|
+
return awaitOnExit(
|
|
248
|
+
this.request(endpoint, payload, { method: "PATCH" })
|
|
249
|
+
).catch((error) => {
|
|
250
|
+
try {
|
|
251
|
+
console.error("Bitfab: Failed to patch trace:", error);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Start a replay session by fetching historical traces.
|
|
258
|
+
* Blocking call — creates a test run and returns lightweight item references.
|
|
259
|
+
*/
|
|
260
|
+
async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles) {
|
|
261
|
+
const payload = { traceFunctionKey, limit };
|
|
262
|
+
if (traceIds) {
|
|
263
|
+
payload.traceIds = traceIds;
|
|
264
|
+
}
|
|
265
|
+
if (codeChangeDescription !== void 0) {
|
|
266
|
+
payload.codeChangeDescription = codeChangeDescription;
|
|
267
|
+
}
|
|
268
|
+
if (codeChangeFiles !== void 0) {
|
|
269
|
+
payload.codeChangeFiles = codeChangeFiles;
|
|
270
|
+
}
|
|
271
|
+
return this.request("/api/sdk/replay/start", payload, {
|
|
272
|
+
timeout: 3e4
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Fetch an external span by ID.
|
|
277
|
+
* Blocking GET request.
|
|
278
|
+
*/
|
|
279
|
+
async getExternalSpan(spanId) {
|
|
280
|
+
const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
283
|
+
try {
|
|
284
|
+
const response = await fetch(url, {
|
|
285
|
+
method: "GET",
|
|
286
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
287
|
+
signal: controller.signal
|
|
288
|
+
});
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
const errorText = await response.text();
|
|
291
|
+
throw new BitfabError(
|
|
292
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return await response.json();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof BitfabError) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
if (error instanceof Error) {
|
|
301
|
+
if (error.name === "AbortError") {
|
|
302
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
303
|
+
}
|
|
304
|
+
throw new BitfabError(error.message);
|
|
305
|
+
}
|
|
306
|
+
throw new BitfabError("Unknown error occurred");
|
|
307
|
+
} finally {
|
|
308
|
+
clearTimeout(timeoutId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Fetch the span tree for a root span.
|
|
313
|
+
* Blocking GET request.
|
|
314
|
+
*/
|
|
315
|
+
async getSpanTree(externalSpanId) {
|
|
316
|
+
const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
|
|
317
|
+
const controller = new AbortController();
|
|
318
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
319
|
+
try {
|
|
320
|
+
const response = await fetch(url, {
|
|
321
|
+
method: "GET",
|
|
322
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
323
|
+
signal: controller.signal
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const errorText = await response.text();
|
|
327
|
+
throw new BitfabError(
|
|
328
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return await response.json();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error instanceof BitfabError) {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
if (error instanceof Error) {
|
|
337
|
+
if (error.name === "AbortError") {
|
|
338
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
339
|
+
}
|
|
340
|
+
throw new BitfabError(error.message);
|
|
341
|
+
}
|
|
342
|
+
throw new BitfabError("Unknown error occurred");
|
|
343
|
+
} finally {
|
|
344
|
+
clearTimeout(timeoutId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Mark a replay test run as completed.
|
|
349
|
+
* Blocking call.
|
|
350
|
+
*/
|
|
351
|
+
async completeReplay(testRunId) {
|
|
352
|
+
return this.request(
|
|
353
|
+
"/api/sdk/replay/complete",
|
|
354
|
+
{ testRunId },
|
|
355
|
+
{ timeout: 3e4 }
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// src/asyncStorage.ts
|
|
363
|
+
function registerAsyncLocalStorageClass(cls) {
|
|
364
|
+
if (!AsyncLocalStorageClass) {
|
|
365
|
+
AsyncLocalStorageClass = cls;
|
|
366
|
+
}
|
|
367
|
+
initDone = true;
|
|
368
|
+
}
|
|
369
|
+
function isAsyncStorageInitDone() {
|
|
370
|
+
return initDone;
|
|
371
|
+
}
|
|
372
|
+
function createAsyncLocalStorage() {
|
|
373
|
+
return AsyncLocalStorageClass ? new AsyncLocalStorageClass() : null;
|
|
374
|
+
}
|
|
375
|
+
var AsyncLocalStorageClass, initDone, asyncStorageReady;
|
|
376
|
+
var init_asyncStorage = __esm({
|
|
377
|
+
"src/asyncStorage.ts"() {
|
|
378
|
+
"use strict";
|
|
379
|
+
AsyncLocalStorageClass = null;
|
|
380
|
+
initDone = false;
|
|
381
|
+
asyncStorageReady = (typeof process !== "undefined" && process.versions?.node ? (
|
|
382
|
+
// The join trick hides "node:async_hooks" from static analysis so
|
|
383
|
+
// bundlers that ban Node.js built-ins don't fail at build time.
|
|
384
|
+
// webpackIgnore tells webpack/turbopack to emit a native import()
|
|
385
|
+
// so Node.js can resolve the module at runtime.
|
|
386
|
+
import(
|
|
387
|
+
/* webpackIgnore: true */
|
|
388
|
+
["node", "async_hooks"].join(":")
|
|
389
|
+
).then(
|
|
390
|
+
(mod) => {
|
|
391
|
+
registerAsyncLocalStorageClass(mod.AsyncLocalStorage);
|
|
392
|
+
}
|
|
393
|
+
).catch(() => {
|
|
394
|
+
})
|
|
395
|
+
) : Promise.resolve()).then(() => {
|
|
396
|
+
initDone = true;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// src/replayContext.ts
|
|
402
|
+
function getReplayContext() {
|
|
403
|
+
return replayContextStorage?.getStore() ?? null;
|
|
404
|
+
}
|
|
405
|
+
function runWithReplayContext(ctx, fn) {
|
|
406
|
+
if (replayContextStorage) {
|
|
407
|
+
return replayContextStorage.run(ctx, fn);
|
|
408
|
+
}
|
|
409
|
+
return fn();
|
|
410
|
+
}
|
|
411
|
+
var replayContextStorage, replayContextReady;
|
|
412
|
+
var init_replayContext = __esm({
|
|
413
|
+
"src/replayContext.ts"() {
|
|
414
|
+
"use strict";
|
|
415
|
+
init_asyncStorage();
|
|
416
|
+
replayContextStorage = null;
|
|
417
|
+
replayContextReady = asyncStorageReady.then(() => {
|
|
418
|
+
replayContextStorage = createAsyncLocalStorage();
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// src/serialize.ts
|
|
424
|
+
function serializeValue(value) {
|
|
425
|
+
try {
|
|
426
|
+
const { json, meta } = import_superjson.default.serialize(value);
|
|
427
|
+
return meta ? { json, meta } : { json };
|
|
428
|
+
} catch {
|
|
429
|
+
try {
|
|
430
|
+
return { json: JSON.parse(JSON.stringify(value)) };
|
|
431
|
+
} catch {
|
|
432
|
+
return { json: String(value) };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function deserializeValue(serialized) {
|
|
437
|
+
if (serialized.meta === void 0) {
|
|
438
|
+
return serialized.json;
|
|
439
|
+
}
|
|
440
|
+
return import_superjson.default.deserialize({
|
|
441
|
+
json: serialized.json,
|
|
442
|
+
meta: serialized.meta
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
var import_superjson;
|
|
446
|
+
var init_serialize = __esm({
|
|
447
|
+
"src/serialize.ts"() {
|
|
448
|
+
"use strict";
|
|
449
|
+
import_superjson = __toESM(require("superjson"), 1);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// src/replay.ts
|
|
454
|
+
var replay_exports = {};
|
|
455
|
+
__export(replay_exports, {
|
|
456
|
+
replay: () => replay
|
|
457
|
+
});
|
|
458
|
+
function deserializeInputs(spanData) {
|
|
459
|
+
const inputMeta = spanData.input_meta;
|
|
460
|
+
const rawInput = spanData.input;
|
|
461
|
+
if (inputMeta !== void 0 && inputMeta !== null) {
|
|
462
|
+
const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
|
|
463
|
+
if (Array.isArray(deserialized)) {
|
|
464
|
+
return deserialized;
|
|
465
|
+
}
|
|
466
|
+
return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(rawInput)) {
|
|
469
|
+
return rawInput;
|
|
470
|
+
}
|
|
471
|
+
return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
|
|
472
|
+
}
|
|
473
|
+
function deserializeOutput(spanData) {
|
|
474
|
+
const outputMeta = spanData.output_meta;
|
|
475
|
+
const rawOutput = spanData.output;
|
|
476
|
+
if (outputMeta !== void 0 && outputMeta !== null) {
|
|
477
|
+
return deserializeValue({ json: rawOutput, meta: outputMeta });
|
|
478
|
+
}
|
|
479
|
+
return rawOutput;
|
|
480
|
+
}
|
|
481
|
+
function buildMockTree(rootNode) {
|
|
482
|
+
const spans = /* @__PURE__ */ new Map();
|
|
483
|
+
const counters = /* @__PURE__ */ new Map();
|
|
484
|
+
function walk(node) {
|
|
485
|
+
const key = node.traceFunctionKey;
|
|
486
|
+
if (key) {
|
|
487
|
+
const name = node.spanName;
|
|
488
|
+
const counterKey = `${key}:${name}`;
|
|
489
|
+
const index = counters.get(counterKey) ?? 0;
|
|
490
|
+
counters.set(counterKey, index + 1);
|
|
491
|
+
spans.set(`${counterKey}:${index}`, {
|
|
492
|
+
sourceSpanId: node.sourceSpanId,
|
|
493
|
+
output: node.output,
|
|
494
|
+
outputMeta: node.outputMeta
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
for (const child of node.children) {
|
|
498
|
+
walk(child);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const child of rootNode.children) {
|
|
502
|
+
walk(child);
|
|
503
|
+
}
|
|
504
|
+
return { spans };
|
|
505
|
+
}
|
|
506
|
+
async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy) {
|
|
507
|
+
const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
|
|
508
|
+
const spanData = span.rawData?.span_data ?? {};
|
|
509
|
+
const inputs = deserializeInputs(spanData);
|
|
510
|
+
const originalOutput = deserializeOutput(spanData);
|
|
511
|
+
let mockTree;
|
|
512
|
+
if (mockStrategy === "all" || mockStrategy === "marked") {
|
|
513
|
+
const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId);
|
|
514
|
+
mockTree = buildMockTree(treeResponse.root);
|
|
515
|
+
}
|
|
516
|
+
let result;
|
|
517
|
+
let error = null;
|
|
518
|
+
try {
|
|
519
|
+
const maybePromise = runWithReplayContext(
|
|
520
|
+
{
|
|
521
|
+
testRunId,
|
|
522
|
+
inputSourceSpanId: span.id,
|
|
523
|
+
inputSourceTraceId: span.externalTraceId,
|
|
524
|
+
mockTree,
|
|
525
|
+
callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
|
|
526
|
+
mockStrategy
|
|
527
|
+
},
|
|
528
|
+
() => fn(...inputs)
|
|
529
|
+
);
|
|
530
|
+
result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
error = e instanceof Error ? e.message : String(e);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
input: inputs,
|
|
536
|
+
result,
|
|
537
|
+
originalOutput,
|
|
538
|
+
error,
|
|
539
|
+
durationMs: serverItem.durationMs ?? null,
|
|
540
|
+
tokens: serverItem.tokens ?? null,
|
|
541
|
+
model: serverItem.model ?? null
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async function mapWithConcurrency(tasks, maxConcurrency) {
|
|
545
|
+
const results = new Array(tasks.length);
|
|
546
|
+
let nextIndex = 0;
|
|
547
|
+
async function worker() {
|
|
548
|
+
while (nextIndex < tasks.length) {
|
|
549
|
+
const index = nextIndex++;
|
|
550
|
+
results[index] = await tasks[index]();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const workers = Array.from(
|
|
554
|
+
{ length: Math.min(maxConcurrency, tasks.length) },
|
|
555
|
+
() => worker()
|
|
556
|
+
);
|
|
557
|
+
await Promise.all(workers);
|
|
558
|
+
return results;
|
|
559
|
+
}
|
|
560
|
+
async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
|
|
561
|
+
await replayContextReady;
|
|
562
|
+
const {
|
|
563
|
+
testRunId,
|
|
564
|
+
testRunUrl,
|
|
565
|
+
items: serverItems
|
|
566
|
+
} = await httpClient.startReplay(
|
|
567
|
+
traceFunctionKey,
|
|
568
|
+
options?.limit ?? 5,
|
|
569
|
+
options?.traceIds,
|
|
570
|
+
options?.codeChangeDescription,
|
|
571
|
+
options?.codeChangeFiles
|
|
572
|
+
);
|
|
573
|
+
const mockStrategy = options?.mock ?? "none";
|
|
574
|
+
const maxConcurrency = options?.maxConcurrency ?? 10;
|
|
575
|
+
const tasks = serverItems.map(
|
|
576
|
+
(serverItem) => () => processItem(httpClient, serverItem, fn, testRunId, mockStrategy)
|
|
577
|
+
);
|
|
578
|
+
const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
|
|
579
|
+
await flushTraces();
|
|
580
|
+
try {
|
|
581
|
+
await httpClient.completeReplay(testRunId);
|
|
582
|
+
} catch (e) {
|
|
583
|
+
try {
|
|
584
|
+
console.error("Bitfab: Failed to complete replay:", e);
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
items: resultItems,
|
|
590
|
+
testRunId,
|
|
591
|
+
testRunUrl: `${serviceUrl}${testRunUrl}`
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
var init_replay = __esm({
|
|
595
|
+
"src/replay.ts"() {
|
|
596
|
+
"use strict";
|
|
597
|
+
init_http();
|
|
598
|
+
init_replayContext();
|
|
599
|
+
init_serialize();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/index.ts
|
|
604
|
+
var index_exports = {};
|
|
605
|
+
__export(index_exports, {
|
|
606
|
+
Bitfab: () => Bitfab,
|
|
607
|
+
BitfabClaudeAgentHandler: () => BitfabClaudeAgentHandler,
|
|
608
|
+
BitfabError: () => BitfabError,
|
|
609
|
+
BitfabFunction: () => BitfabFunction,
|
|
610
|
+
BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
|
|
611
|
+
BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
|
|
612
|
+
DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
|
|
613
|
+
__version__: () => __version__,
|
|
614
|
+
flushTraces: () => flushTraces,
|
|
615
|
+
getCurrentSpan: () => getCurrentSpan,
|
|
616
|
+
getCurrentTrace: () => getCurrentTrace
|
|
617
|
+
});
|
|
618
|
+
module.exports = __toCommonJS(index_exports);
|
|
619
|
+
|
|
620
|
+
// src/claudeAgentSdk.ts
|
|
621
|
+
init_constants();
|
|
622
|
+
init_http();
|
|
623
|
+
function nowIso() {
|
|
624
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
625
|
+
}
|
|
626
|
+
function safeSerialize(value) {
|
|
627
|
+
if (value === null || value === void 0) {
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(value)) {
|
|
634
|
+
return value.map(safeSerialize);
|
|
635
|
+
}
|
|
636
|
+
if (typeof value === "object") {
|
|
637
|
+
if (typeof value.toJSON === "function") {
|
|
638
|
+
return value.toJSON();
|
|
639
|
+
}
|
|
640
|
+
const result = {};
|
|
641
|
+
for (const [k, v] of Object.entries(value)) {
|
|
642
|
+
if (!k.startsWith("_")) {
|
|
643
|
+
result[k] = safeSerialize(v);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
return String(value);
|
|
649
|
+
}
|
|
650
|
+
function extractContentBlocks(content) {
|
|
651
|
+
if (!Array.isArray(content)) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
return content.map((block) => safeSerialize(block));
|
|
655
|
+
}
|
|
656
|
+
function extractUsage(message) {
|
|
657
|
+
const usageInfo = {};
|
|
658
|
+
const usage = message.usage;
|
|
659
|
+
if (!usage) {
|
|
660
|
+
return usageInfo;
|
|
661
|
+
}
|
|
662
|
+
const mapping = {
|
|
663
|
+
input_tokens: "inputTokens",
|
|
664
|
+
output_tokens: "outputTokens",
|
|
665
|
+
cache_read_input_tokens: "cacheReadTokens",
|
|
666
|
+
cache_creation_input_tokens: "cacheCreationTokens"
|
|
667
|
+
};
|
|
668
|
+
for (const [srcKey, dstKey] of Object.entries(mapping)) {
|
|
669
|
+
const val = usage[srcKey];
|
|
670
|
+
if (val !== void 0 && val !== null) {
|
|
671
|
+
usageInfo[dstKey] = val;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return usageInfo;
|
|
675
|
+
}
|
|
676
|
+
var BitfabClaudeAgentHandler = class {
|
|
677
|
+
constructor(config) {
|
|
678
|
+
// Span tracking
|
|
679
|
+
this.runToSpan = /* @__PURE__ */ new Map();
|
|
680
|
+
this.traceId = null;
|
|
681
|
+
this.rootSpanId = null;
|
|
682
|
+
this.activeContext = null;
|
|
683
|
+
this.traceStartedAt = null;
|
|
684
|
+
// LLM turn tracking
|
|
685
|
+
this.conversationHistory = [];
|
|
686
|
+
this.pendingMessages = [];
|
|
687
|
+
this.currentLlmSpanId = null;
|
|
688
|
+
this.currentLlmMessageId = null;
|
|
689
|
+
this.currentLlmContent = [];
|
|
690
|
+
this.currentLlmModel = null;
|
|
691
|
+
this.currentLlmUsage = {};
|
|
692
|
+
this.currentLlmStartedAt = null;
|
|
693
|
+
this.currentLlmHistorySnapshot = [];
|
|
694
|
+
// Subagent tracking
|
|
695
|
+
this.activeSubagentSpans = /* @__PURE__ */ new Map();
|
|
696
|
+
this.httpClient = new HttpClient({
|
|
697
|
+
apiKey: config.apiKey,
|
|
698
|
+
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
699
|
+
timeout: config.timeout ?? 1e4
|
|
700
|
+
});
|
|
701
|
+
this.traceFunctionKey = config.traceFunctionKey;
|
|
702
|
+
this.getActiveSpanContext = config.getActiveSpanContext ?? null;
|
|
703
|
+
this.preToolUseHook = this.preToolUseHook.bind(this);
|
|
704
|
+
this.postToolUseHook = this.postToolUseHook.bind(this);
|
|
705
|
+
this.postToolUseFailureHook = this.postToolUseFailureHook.bind(this);
|
|
706
|
+
this.subagentStartHook = this.subagentStartHook.bind(this);
|
|
707
|
+
this.subagentStopHook = this.subagentStopHook.bind(this);
|
|
708
|
+
}
|
|
709
|
+
// ── trace lifecycle ──────────────────────────────────────────
|
|
710
|
+
ensureTrace() {
|
|
711
|
+
if (this.traceId !== null) {
|
|
712
|
+
return this.traceId;
|
|
713
|
+
}
|
|
714
|
+
this.activeContext = this.getActiveSpanContext?.() ?? null;
|
|
715
|
+
if (this.activeContext) {
|
|
716
|
+
this.traceId = this.activeContext.traceId;
|
|
717
|
+
} else {
|
|
718
|
+
this.traceId = crypto.randomUUID();
|
|
719
|
+
}
|
|
720
|
+
this.traceStartedAt = nowIso();
|
|
721
|
+
return this.traceId;
|
|
722
|
+
}
|
|
723
|
+
getParentId(agentId) {
|
|
724
|
+
if (agentId) {
|
|
725
|
+
const subagentSpanId = this.activeSubagentSpans.get(agentId);
|
|
726
|
+
if (subagentSpanId) {
|
|
727
|
+
return subagentSpanId;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return this.activeContext?.spanId ?? this.rootSpanId ?? null;
|
|
731
|
+
}
|
|
732
|
+
// ── span helpers ─────────────────────────────────────────────
|
|
733
|
+
startSpan(spanId, name, spanType, inputData, parentId) {
|
|
734
|
+
const traceId = this.ensureTrace();
|
|
735
|
+
const spanInfo = {
|
|
736
|
+
spanId,
|
|
737
|
+
traceId,
|
|
738
|
+
parentId: parentId ?? null,
|
|
739
|
+
startedAt: nowIso(),
|
|
740
|
+
name,
|
|
741
|
+
type: spanType,
|
|
742
|
+
input: safeSerialize(inputData),
|
|
743
|
+
contexts: []
|
|
744
|
+
};
|
|
745
|
+
this.runToSpan.set(spanId, spanInfo);
|
|
746
|
+
return spanInfo;
|
|
747
|
+
}
|
|
748
|
+
completeSpan(spanId, output, error, extraContexts) {
|
|
749
|
+
const spanInfo = this.runToSpan.get(spanId);
|
|
750
|
+
if (!spanInfo) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this.runToSpan.delete(spanId);
|
|
754
|
+
spanInfo.endedAt = nowIso();
|
|
755
|
+
spanInfo.output = safeSerialize(output);
|
|
756
|
+
if (error !== void 0) {
|
|
757
|
+
spanInfo.error = error;
|
|
758
|
+
}
|
|
759
|
+
if (extraContexts) {
|
|
760
|
+
spanInfo.contexts.push(extraContexts);
|
|
761
|
+
}
|
|
762
|
+
this.sendSpan(spanInfo);
|
|
763
|
+
}
|
|
764
|
+
sendSpan(spanInfo) {
|
|
765
|
+
const spanData = {
|
|
766
|
+
name: spanInfo.name,
|
|
767
|
+
type: spanInfo.type
|
|
768
|
+
};
|
|
769
|
+
if (spanInfo.input !== void 0) {
|
|
770
|
+
spanData.input = spanInfo.input;
|
|
771
|
+
}
|
|
772
|
+
if (spanInfo.output !== void 0) {
|
|
773
|
+
spanData.output = spanInfo.output;
|
|
774
|
+
}
|
|
775
|
+
if (spanInfo.error !== void 0) {
|
|
776
|
+
spanData.error = spanInfo.error;
|
|
777
|
+
}
|
|
778
|
+
if (spanInfo.contexts.length > 0) {
|
|
779
|
+
spanData.contexts = spanInfo.contexts;
|
|
780
|
+
}
|
|
781
|
+
const rawSpan = {
|
|
782
|
+
id: spanInfo.spanId,
|
|
783
|
+
trace_id: spanInfo.traceId,
|
|
784
|
+
started_at: spanInfo.startedAt,
|
|
785
|
+
ended_at: spanInfo.endedAt ?? nowIso(),
|
|
786
|
+
span_data: spanData
|
|
787
|
+
};
|
|
788
|
+
if (spanInfo.parentId !== null) {
|
|
789
|
+
rawSpan.parent_id = spanInfo.parentId;
|
|
790
|
+
}
|
|
791
|
+
const payload = {
|
|
792
|
+
type: "sdk-function",
|
|
793
|
+
source: "typescript-sdk-claude-agent-sdk",
|
|
794
|
+
traceFunctionKey: this.traceFunctionKey,
|
|
795
|
+
sourceTraceId: spanInfo.traceId,
|
|
796
|
+
rawSpan
|
|
797
|
+
};
|
|
798
|
+
try {
|
|
799
|
+
this.httpClient.sendExternalSpan(payload);
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
sendTraceCompletion(endedAt, metadata) {
|
|
804
|
+
if (this.traceId === null) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const completed = this.activeContext === null;
|
|
808
|
+
const traceId = this.traceId;
|
|
809
|
+
this.traceId = null;
|
|
810
|
+
const externalTrace = {
|
|
811
|
+
id: traceId,
|
|
812
|
+
started_at: this.traceStartedAt ?? nowIso(),
|
|
813
|
+
ended_at: endedAt ?? nowIso(),
|
|
814
|
+
workflow_name: this.traceFunctionKey
|
|
815
|
+
};
|
|
816
|
+
if (metadata) {
|
|
817
|
+
externalTrace.metadata = metadata;
|
|
818
|
+
}
|
|
819
|
+
const traceData = {
|
|
820
|
+
type: "sdk-function",
|
|
821
|
+
source: "typescript-sdk-claude-agent-sdk",
|
|
822
|
+
traceFunctionKey: this.traceFunctionKey,
|
|
823
|
+
externalTrace,
|
|
824
|
+
completed
|
|
825
|
+
};
|
|
826
|
+
try {
|
|
827
|
+
this.httpClient.sendExternalTrace(traceData);
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// ── hook callbacks ───────────────────────────────────────────
|
|
832
|
+
async preToolUseHook(inputData, toolUseId, _context) {
|
|
833
|
+
try {
|
|
834
|
+
const sid = inputData.tool_use_id ?? toolUseId ?? crypto.randomUUID();
|
|
835
|
+
const toolName = inputData.tool_name ?? "tool";
|
|
836
|
+
const toolInput = inputData.tool_input ?? {};
|
|
837
|
+
const agentId = inputData.agent_id;
|
|
838
|
+
const parentId = this.getParentId(agentId);
|
|
839
|
+
this.startSpan(sid, toolName, "function", toolInput, parentId);
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
return {};
|
|
843
|
+
}
|
|
844
|
+
async postToolUseHook(inputData, toolUseId, _context) {
|
|
845
|
+
try {
|
|
846
|
+
const sid = inputData.tool_use_id ?? toolUseId ?? "";
|
|
847
|
+
const toolResponse = inputData.tool_response;
|
|
848
|
+
this.completeSpan(sid, toolResponse);
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
return {};
|
|
852
|
+
}
|
|
853
|
+
async postToolUseFailureHook(inputData, toolUseId, _context) {
|
|
854
|
+
try {
|
|
855
|
+
const sid = inputData.tool_use_id ?? toolUseId ?? "";
|
|
856
|
+
const error = String(inputData.error ?? "Unknown error");
|
|
857
|
+
this.completeSpan(sid, void 0, error);
|
|
858
|
+
} catch {
|
|
859
|
+
}
|
|
860
|
+
return {};
|
|
861
|
+
}
|
|
862
|
+
async subagentStartHook(inputData, _toolUseId, _context) {
|
|
863
|
+
try {
|
|
864
|
+
const agentId = inputData.agent_id ?? crypto.randomUUID();
|
|
865
|
+
const agentType = inputData.agent_type ?? "subagent";
|
|
866
|
+
const parentId = this.getParentId();
|
|
867
|
+
const spanId = crypto.randomUUID();
|
|
868
|
+
this.activeSubagentSpans.set(agentId, spanId);
|
|
869
|
+
this.startSpan(
|
|
870
|
+
spanId,
|
|
871
|
+
`Agent: ${agentType}`,
|
|
872
|
+
"agent",
|
|
873
|
+
void 0,
|
|
874
|
+
parentId
|
|
875
|
+
);
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
return {};
|
|
879
|
+
}
|
|
880
|
+
async subagentStopHook(inputData, _toolUseId, _context) {
|
|
881
|
+
try {
|
|
882
|
+
const agentId = inputData.agent_id ?? "";
|
|
883
|
+
const spanId = this.activeSubagentSpans.get(agentId);
|
|
884
|
+
if (spanId) {
|
|
885
|
+
this.activeSubagentSpans.delete(agentId);
|
|
886
|
+
this.completeSpan(spanId);
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
return {};
|
|
891
|
+
}
|
|
892
|
+
// ── public API ───────────────────────────────────────────────
|
|
893
|
+
/**
|
|
894
|
+
* Inject Bitfab tracing hooks into Claude Agent SDK options.
|
|
895
|
+
*
|
|
896
|
+
* Modifies the options object and returns it for convenience.
|
|
897
|
+
* The SDK's `HookMatcher` is constructed as a plain object
|
|
898
|
+
* (`{ matcher: null, hooks: [callback] }`) to avoid requiring
|
|
899
|
+
* `@anthropic-ai/claude-agent-sdk` as a dependency.
|
|
900
|
+
*
|
|
901
|
+
* @param options - Options object with a `hooks` property
|
|
902
|
+
* @returns The modified options object with Bitfab hooks injected
|
|
903
|
+
*/
|
|
904
|
+
instrumentOptions(options) {
|
|
905
|
+
const hooks = options.hooks ?? {};
|
|
906
|
+
if (!options.hooks) {
|
|
907
|
+
;
|
|
908
|
+
options.hooks = hooks;
|
|
909
|
+
}
|
|
910
|
+
const hookConfig = [
|
|
911
|
+
["PreToolUse", this.preToolUseHook],
|
|
912
|
+
["PostToolUse", this.postToolUseHook],
|
|
913
|
+
["PostToolUseFailure", this.postToolUseFailureHook],
|
|
914
|
+
["SubagentStart", this.subagentStartHook],
|
|
915
|
+
["SubagentStop", this.subagentStopHook]
|
|
916
|
+
];
|
|
917
|
+
for (const [event, callback] of hookConfig) {
|
|
918
|
+
if (!hooks[event]) {
|
|
919
|
+
hooks[event] = [];
|
|
920
|
+
}
|
|
921
|
+
hooks[event].push({ matcher: null, hooks: [callback] });
|
|
922
|
+
}
|
|
923
|
+
return options;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Wrap a `ClaudeSDKClient.receiveResponse()` stream to capture LLM turns.
|
|
927
|
+
*
|
|
928
|
+
* Yields every message unchanged while capturing AssistantMessage
|
|
929
|
+
* content as LLM turn spans.
|
|
930
|
+
*/
|
|
931
|
+
async *wrapResponse(stream) {
|
|
932
|
+
yield* this.processStream(stream);
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Wrap a `query()` async iterator to capture LLM turns.
|
|
936
|
+
*
|
|
937
|
+
* Same as `wrapResponse` but for the simpler `query()` API
|
|
938
|
+
* which does not support hooks (no tool/subagent spans).
|
|
939
|
+
*/
|
|
940
|
+
async *wrapQuery(stream) {
|
|
941
|
+
yield* this.processStream(stream);
|
|
942
|
+
}
|
|
943
|
+
// ── stream processing ────────────────────────────────────────
|
|
944
|
+
async *processStream(stream) {
|
|
945
|
+
try {
|
|
946
|
+
for await (const message of stream) {
|
|
947
|
+
try {
|
|
948
|
+
this.processMessage(message);
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
yield message;
|
|
952
|
+
}
|
|
953
|
+
} finally {
|
|
954
|
+
try {
|
|
955
|
+
this.flushLlmTurn();
|
|
956
|
+
this.sendTraceCompletion();
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
this.resetState();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
processMessage(message) {
|
|
963
|
+
const typeName = message.constructor?.name ?? "";
|
|
964
|
+
if (typeName === "AssistantMessage") {
|
|
965
|
+
this.handleAssistantMessage(message);
|
|
966
|
+
} else if (typeName === "UserMessage") {
|
|
967
|
+
this.handleUserMessage(message);
|
|
968
|
+
} else if (typeName === "ResultMessage") {
|
|
969
|
+
this.handleResultMessage(message);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
handleAssistantMessage(message) {
|
|
973
|
+
this.ensureTrace();
|
|
974
|
+
const messageId = message.message_id;
|
|
975
|
+
if (messageId !== this.currentLlmMessageId) {
|
|
976
|
+
this.flushLlmTurn();
|
|
977
|
+
this.conversationHistory.push(...this.pendingMessages);
|
|
978
|
+
this.pendingMessages = [];
|
|
979
|
+
this.currentLlmSpanId = crypto.randomUUID();
|
|
980
|
+
this.currentLlmMessageId = messageId ?? null;
|
|
981
|
+
this.currentLlmContent = [];
|
|
982
|
+
this.currentLlmModel = message.model ?? null;
|
|
983
|
+
this.currentLlmUsage = {};
|
|
984
|
+
this.currentLlmStartedAt = nowIso();
|
|
985
|
+
this.currentLlmHistorySnapshot = [...this.conversationHistory];
|
|
986
|
+
}
|
|
987
|
+
const content = message.content;
|
|
988
|
+
if (Array.isArray(content)) {
|
|
989
|
+
this.currentLlmContent.push(...extractContentBlocks(content));
|
|
990
|
+
}
|
|
991
|
+
const usage = extractUsage(message);
|
|
992
|
+
if (Object.keys(usage).length > 0) {
|
|
993
|
+
Object.assign(this.currentLlmUsage, usage);
|
|
994
|
+
}
|
|
995
|
+
const model = message.model;
|
|
996
|
+
if (model) {
|
|
997
|
+
this.currentLlmModel = model;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
handleUserMessage(message) {
|
|
1001
|
+
const content = message.content;
|
|
1002
|
+
const toolUseResult = message.tool_use_result;
|
|
1003
|
+
if (toolUseResult !== void 0) {
|
|
1004
|
+
this.pendingMessages.push({
|
|
1005
|
+
role: "tool",
|
|
1006
|
+
content: safeSerialize(content),
|
|
1007
|
+
tool_result: safeSerialize(toolUseResult)
|
|
1008
|
+
});
|
|
1009
|
+
} else {
|
|
1010
|
+
this.pendingMessages.push({
|
|
1011
|
+
role: "user",
|
|
1012
|
+
content: safeSerialize(content)
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
handleResultMessage(message) {
|
|
1017
|
+
this.flushLlmTurn();
|
|
1018
|
+
const metadata = {};
|
|
1019
|
+
for (const attr of [
|
|
1020
|
+
"num_turns",
|
|
1021
|
+
"total_cost_usd",
|
|
1022
|
+
"duration_ms",
|
|
1023
|
+
"duration_api_ms",
|
|
1024
|
+
"session_id"
|
|
1025
|
+
]) {
|
|
1026
|
+
const val = message[attr];
|
|
1027
|
+
if (val !== void 0 && val !== null) {
|
|
1028
|
+
metadata[attr] = val;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const usage = message.usage;
|
|
1032
|
+
if (usage && typeof usage === "object") {
|
|
1033
|
+
metadata.usage = safeSerialize(usage);
|
|
1034
|
+
}
|
|
1035
|
+
this.sendTraceCompletion(
|
|
1036
|
+
void 0,
|
|
1037
|
+
Object.keys(metadata).length > 0 ? metadata : void 0
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
flushLlmTurn() {
|
|
1041
|
+
if (this.currentLlmSpanId === null) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const spanId = this.currentLlmSpanId;
|
|
1045
|
+
const traceId = this.ensureTrace();
|
|
1046
|
+
const parentId = this.getParentId();
|
|
1047
|
+
const llmContext = {};
|
|
1048
|
+
if (this.currentLlmModel) {
|
|
1049
|
+
llmContext.model = this.currentLlmModel;
|
|
1050
|
+
}
|
|
1051
|
+
Object.assign(llmContext, this.currentLlmUsage);
|
|
1052
|
+
const spanInfo = {
|
|
1053
|
+
spanId,
|
|
1054
|
+
traceId,
|
|
1055
|
+
parentId,
|
|
1056
|
+
startedAt: this.currentLlmStartedAt ?? nowIso(),
|
|
1057
|
+
endedAt: nowIso(),
|
|
1058
|
+
name: this.currentLlmModel ?? "llm",
|
|
1059
|
+
type: "llm",
|
|
1060
|
+
input: this.currentLlmHistorySnapshot,
|
|
1061
|
+
output: this.currentLlmContent,
|
|
1062
|
+
contexts: Object.keys(llmContext).length > 0 ? [llmContext] : []
|
|
1063
|
+
};
|
|
1064
|
+
this.sendSpan(spanInfo);
|
|
1065
|
+
this.conversationHistory.push({
|
|
1066
|
+
role: "assistant",
|
|
1067
|
+
content: this.currentLlmContent
|
|
1068
|
+
});
|
|
1069
|
+
this.currentLlmSpanId = null;
|
|
1070
|
+
this.currentLlmMessageId = null;
|
|
1071
|
+
this.currentLlmContent = [];
|
|
1072
|
+
this.currentLlmModel = null;
|
|
1073
|
+
this.currentLlmUsage = {};
|
|
1074
|
+
this.currentLlmStartedAt = null;
|
|
1075
|
+
this.currentLlmHistorySnapshot = [];
|
|
1076
|
+
}
|
|
1077
|
+
resetState() {
|
|
1078
|
+
this.runToSpan.clear();
|
|
1079
|
+
this.traceId = null;
|
|
1080
|
+
this.rootSpanId = null;
|
|
1081
|
+
this.activeContext = null;
|
|
1082
|
+
this.traceStartedAt = null;
|
|
1083
|
+
this.conversationHistory = [];
|
|
1084
|
+
this.pendingMessages = [];
|
|
1085
|
+
this.currentLlmSpanId = null;
|
|
1086
|
+
this.currentLlmMessageId = null;
|
|
1087
|
+
this.currentLlmContent = [];
|
|
1088
|
+
this.currentLlmModel = null;
|
|
1089
|
+
this.currentLlmUsage = {};
|
|
1090
|
+
this.currentLlmStartedAt = null;
|
|
1091
|
+
this.currentLlmHistorySnapshot = [];
|
|
1092
|
+
this.activeSubagentSpans.clear();
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// src/client.ts
|
|
1097
|
+
init_asyncStorage();
|
|
1098
|
+
|
|
1099
|
+
// src/baml.ts
|
|
1100
|
+
var cachedBaml = null;
|
|
1101
|
+
async function loadBaml() {
|
|
1102
|
+
if (cachedBaml) {
|
|
1103
|
+
return cachedBaml;
|
|
1104
|
+
}
|
|
1105
|
+
try {
|
|
1106
|
+
cachedBaml = await import("@boundaryml/baml");
|
|
1107
|
+
return cachedBaml;
|
|
1108
|
+
} catch {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
"@boundaryml/baml is required for Bitfab.call(). Install it with: npm install @boundaryml/baml"
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function capitalize(str) {
|
|
1115
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1116
|
+
}
|
|
1117
|
+
function formatProvider(provider) {
|
|
1118
|
+
const providerMap = {
|
|
1119
|
+
openai: "OpenAI",
|
|
1120
|
+
anthropic: "Anthropic",
|
|
1121
|
+
google: "Google"
|
|
1122
|
+
};
|
|
1123
|
+
return providerMap[provider] ?? capitalize(provider);
|
|
1124
|
+
}
|
|
1125
|
+
function formatModel(model) {
|
|
1126
|
+
return model.replace(/^gpt-/, "GPT").replace(/\./g, "_").replace(/-/g, "_");
|
|
1127
|
+
}
|
|
1128
|
+
function getClientName(provider, model) {
|
|
1129
|
+
return `${formatProvider(provider)}_${formatModel(model)}`;
|
|
1130
|
+
}
|
|
1131
|
+
function generateClientDefinitions(providers) {
|
|
1132
|
+
const definitions = [];
|
|
1133
|
+
for (const providerDef of providers) {
|
|
1134
|
+
for (const model of providerDef.models) {
|
|
1135
|
+
const clientName = getClientName(providerDef.provider, model.model);
|
|
1136
|
+
definitions.push(`client<llm> ${clientName} {
|
|
1137
|
+
provider ${providerDef.provider}
|
|
1138
|
+
options {
|
|
1139
|
+
model "${model.model}"
|
|
1140
|
+
api_key env.${providerDef.apiKeyEnv}
|
|
1141
|
+
}
|
|
1142
|
+
}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return definitions.join("\n\n");
|
|
1146
|
+
}
|
|
1147
|
+
function withDefaultClients(bamlSource, providers) {
|
|
1148
|
+
const hasDefaultClient = bamlSource.includes("client<llm> OpenAI_");
|
|
1149
|
+
if (hasDefaultClient) {
|
|
1150
|
+
return bamlSource;
|
|
1151
|
+
}
|
|
1152
|
+
const defaultClients = generateClientDefinitions(providers);
|
|
1153
|
+
return `${defaultClients}
|
|
1154
|
+
|
|
1155
|
+
${bamlSource}`;
|
|
1156
|
+
}
|
|
1157
|
+
function extractFunctionName(bamlSource) {
|
|
1158
|
+
const match = bamlSource.match(/function\s+(\w+)\s*\(/);
|
|
1159
|
+
return match?.[1] ?? null;
|
|
1160
|
+
}
|
|
1161
|
+
function extractFunctionParameters(bamlSource) {
|
|
1162
|
+
const functionMatch = bamlSource.match(/function\s+\w+\s*\(([^)]*)\)\s*->/);
|
|
1163
|
+
if (!functionMatch) {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
const paramsString = functionMatch[1].trim();
|
|
1167
|
+
if (!paramsString) {
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
const params = [];
|
|
1171
|
+
const paramParts = splitParameters(paramsString);
|
|
1172
|
+
for (const part of paramParts) {
|
|
1173
|
+
const trimmed = part.trim();
|
|
1174
|
+
if (!trimmed) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
const paramMatch = trimmed.match(/^(\w+)\s*:\s*(.+)$/);
|
|
1178
|
+
if (paramMatch) {
|
|
1179
|
+
const name = paramMatch[1];
|
|
1180
|
+
let type = paramMatch[2].trim();
|
|
1181
|
+
const isOptional = type.endsWith("?");
|
|
1182
|
+
if (isOptional) {
|
|
1183
|
+
type = type.slice(0, -1);
|
|
1184
|
+
}
|
|
1185
|
+
params.push({ name, type, isOptional });
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return params;
|
|
1189
|
+
}
|
|
1190
|
+
function splitParameters(paramsString) {
|
|
1191
|
+
const parts = [];
|
|
1192
|
+
let current = "";
|
|
1193
|
+
let depth = 0;
|
|
1194
|
+
for (const char of paramsString) {
|
|
1195
|
+
if (char === "<") {
|
|
1196
|
+
depth++;
|
|
1197
|
+
current += char;
|
|
1198
|
+
} else if (char === ">") {
|
|
1199
|
+
depth--;
|
|
1200
|
+
current += char;
|
|
1201
|
+
} else if (char === "," && depth === 0) {
|
|
1202
|
+
parts.push(current);
|
|
1203
|
+
current = "";
|
|
1204
|
+
} else {
|
|
1205
|
+
current += char;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (current.trim()) {
|
|
1209
|
+
parts.push(current);
|
|
1210
|
+
}
|
|
1211
|
+
return parts;
|
|
1212
|
+
}
|
|
1213
|
+
function coerceToType(value, expectedType) {
|
|
1214
|
+
if (expectedType === "string") {
|
|
1215
|
+
return value;
|
|
1216
|
+
}
|
|
1217
|
+
if (expectedType === "int") {
|
|
1218
|
+
const parsed = Number.parseInt(value, 10);
|
|
1219
|
+
if (!Number.isNaN(parsed)) {
|
|
1220
|
+
return parsed;
|
|
1221
|
+
}
|
|
1222
|
+
return value;
|
|
1223
|
+
}
|
|
1224
|
+
if (expectedType === "float") {
|
|
1225
|
+
const parsed = Number.parseFloat(value);
|
|
1226
|
+
if (!Number.isNaN(parsed)) {
|
|
1227
|
+
return parsed;
|
|
1228
|
+
}
|
|
1229
|
+
return value;
|
|
1230
|
+
}
|
|
1231
|
+
if (expectedType === "bool") {
|
|
1232
|
+
const lower = value.toLowerCase();
|
|
1233
|
+
if (lower === "true") {
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
if (lower === "false") {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
return value;
|
|
1240
|
+
}
|
|
1241
|
+
if (expectedType.endsWith("[]")) {
|
|
1242
|
+
try {
|
|
1243
|
+
const parsed = JSON.parse(value);
|
|
1244
|
+
if (Array.isArray(parsed)) {
|
|
1245
|
+
return parsed;
|
|
1246
|
+
}
|
|
1247
|
+
} catch {
|
|
1248
|
+
}
|
|
1249
|
+
return value;
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
return JSON.parse(value);
|
|
1253
|
+
} catch {
|
|
1254
|
+
return value;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function coerceInputs(inputs, expectedTypes) {
|
|
1258
|
+
const coerced = {};
|
|
1259
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
1260
|
+
if (typeof value === "string") {
|
|
1261
|
+
const expectedType = expectedTypes.get(key);
|
|
1262
|
+
if (expectedType) {
|
|
1263
|
+
coerced[key] = coerceToType(value, expectedType);
|
|
1264
|
+
} else {
|
|
1265
|
+
coerced[key] = value;
|
|
1266
|
+
}
|
|
1267
|
+
} else {
|
|
1268
|
+
coerced[key] = value;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return coerced;
|
|
1272
|
+
}
|
|
1273
|
+
function objToDict(obj, depth = 0, maxDepth = 5) {
|
|
1274
|
+
if (depth > maxDepth) {
|
|
1275
|
+
return `<max depth reached: ${typeof obj}>`;
|
|
1276
|
+
}
|
|
1277
|
+
if (obj === null || obj === void 0 || typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean") {
|
|
1278
|
+
return obj;
|
|
1279
|
+
}
|
|
1280
|
+
if (Array.isArray(obj)) {
|
|
1281
|
+
return obj.map((item) => objToDict(item, depth + 1, maxDepth));
|
|
1282
|
+
}
|
|
1283
|
+
if (typeof obj === "object") {
|
|
1284
|
+
const result = {};
|
|
1285
|
+
if (obj.constructor && obj.constructor.name !== "Object") {
|
|
1286
|
+
result.__type__ = obj.constructor.name;
|
|
1287
|
+
}
|
|
1288
|
+
for (const key of Object.keys(obj)) {
|
|
1289
|
+
if (key.startsWith("_")) {
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
const value = obj[key];
|
|
1294
|
+
if (typeof value === "function") {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
result[key] = objToDict(value, depth + 1, maxDepth);
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
result[key] = `<error: ${error instanceof Error ? error.message : String(error)}>`;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const proto = Object.getPrototypeOf(obj);
|
|
1304
|
+
if (proto && proto !== Object.prototype) {
|
|
1305
|
+
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
|
1306
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
1307
|
+
if (key.startsWith("_") || key === "constructor" || key in result) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
if (descriptor.get) {
|
|
1311
|
+
try {
|
|
1312
|
+
const value = obj[key];
|
|
1313
|
+
if (typeof value !== "function") {
|
|
1314
|
+
result[key] = objToDict(value, depth + 1, maxDepth);
|
|
1315
|
+
}
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
return String(obj);
|
|
1326
|
+
}
|
|
1327
|
+
function serializeCollector(collector) {
|
|
1328
|
+
try {
|
|
1329
|
+
return objToDict(collector, 0, 5);
|
|
1330
|
+
} catch (_error) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
var ALLOWED_ENV_KEYS = ["OPENAI_API_KEY"];
|
|
1335
|
+
function filterEnvVars(envVars) {
|
|
1336
|
+
const filtered = {};
|
|
1337
|
+
for (const key of ALLOWED_ENV_KEYS) {
|
|
1338
|
+
const value = envVars[key];
|
|
1339
|
+
if (value) {
|
|
1340
|
+
filtered[key] = value;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return filtered;
|
|
1344
|
+
}
|
|
1345
|
+
async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
|
|
1346
|
+
const { BamlRuntime, Collector } = await loadBaml();
|
|
1347
|
+
const functionName = extractFunctionName(bamlSource);
|
|
1348
|
+
if (!functionName) {
|
|
1349
|
+
throw new Error("No function found in BAML source");
|
|
1350
|
+
}
|
|
1351
|
+
const fullSource = withDefaultClients(bamlSource, providers);
|
|
1352
|
+
const filteredEnvVars = filterEnvVars(envVars);
|
|
1353
|
+
const runtime = BamlRuntime.fromFiles(
|
|
1354
|
+
"/tmp/baml_runtime",
|
|
1355
|
+
{ "source.baml": fullSource },
|
|
1356
|
+
filteredEnvVars
|
|
1357
|
+
);
|
|
1358
|
+
const ctx = runtime.createContextManager();
|
|
1359
|
+
const collector = new Collector("bitfab-collector");
|
|
1360
|
+
const params = extractFunctionParameters(bamlSource);
|
|
1361
|
+
const expectedTypes = new Map(params.map((p) => [p.name, p.type]));
|
|
1362
|
+
const args = coerceInputs(inputs, expectedTypes);
|
|
1363
|
+
const functionResult = await runtime.callFunction(
|
|
1364
|
+
functionName,
|
|
1365
|
+
args,
|
|
1366
|
+
ctx,
|
|
1367
|
+
null,
|
|
1368
|
+
// TypeBuilder
|
|
1369
|
+
null,
|
|
1370
|
+
// ClientRegistry
|
|
1371
|
+
[collector],
|
|
1372
|
+
// Collectors - capture execution data
|
|
1373
|
+
{},
|
|
1374
|
+
// Tags
|
|
1375
|
+
filteredEnvVars
|
|
1376
|
+
);
|
|
1377
|
+
if (!functionResult.isOk()) {
|
|
1378
|
+
throw new Error("BAML function execution failed");
|
|
1379
|
+
}
|
|
1380
|
+
const rawCollector = serializeCollector(collector);
|
|
1381
|
+
return {
|
|
1382
|
+
result: functionResult.parsed(false),
|
|
1383
|
+
rawCollector
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/client.ts
|
|
1388
|
+
init_constants();
|
|
1389
|
+
init_http();
|
|
1390
|
+
|
|
1391
|
+
// src/langgraph.ts
|
|
1392
|
+
init_constants();
|
|
1393
|
+
init_http();
|
|
1394
|
+
var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
|
|
1395
|
+
var LANGGRAPH_METADATA_KEYS = [
|
|
1396
|
+
"langgraph_step",
|
|
1397
|
+
"langgraph_node",
|
|
1398
|
+
"langgraph_triggers",
|
|
1399
|
+
"langgraph_path",
|
|
1400
|
+
"langgraph_checkpoint_ns"
|
|
1401
|
+
];
|
|
1402
|
+
function nowIso2() {
|
|
1403
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1404
|
+
}
|
|
1405
|
+
var MAX_SERIALIZE_DEPTH = 6;
|
|
1406
|
+
function safeSerialize2(value) {
|
|
1407
|
+
return safeSerializeInner(value, 0, /* @__PURE__ */ new WeakSet());
|
|
1408
|
+
}
|
|
1409
|
+
function safeSerializeInner(value, depth, seen) {
|
|
1410
|
+
if (value === null || value === void 0) {
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1414
|
+
return value;
|
|
1415
|
+
}
|
|
1416
|
+
const className = value?.constructor?.name ?? typeof value;
|
|
1417
|
+
if (depth > MAX_SERIALIZE_DEPTH) {
|
|
1418
|
+
return `<${className}>`;
|
|
1419
|
+
}
|
|
1420
|
+
if (typeof value === "object") {
|
|
1421
|
+
if (seen.has(value)) {
|
|
1422
|
+
return `<cycle ${className}>`;
|
|
1423
|
+
}
|
|
1424
|
+
seen.add(value);
|
|
1425
|
+
}
|
|
1426
|
+
if (Array.isArray(value)) {
|
|
1427
|
+
return value.map((item) => safeSerializeInner(item, depth + 1, seen));
|
|
1428
|
+
}
|
|
1429
|
+
if (typeof value === "object") {
|
|
1430
|
+
if (typeof value.toJSON === "function") {
|
|
1431
|
+
try {
|
|
1432
|
+
return value.toJSON();
|
|
1433
|
+
} catch {
|
|
1434
|
+
return `<${className}>`;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
const result = {};
|
|
1439
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1440
|
+
if (!k.startsWith("_")) {
|
|
1441
|
+
result[k] = safeSerializeInner(v, depth + 1, seen);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return result;
|
|
1445
|
+
} catch {
|
|
1446
|
+
return `<${className}>`;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
return String(value);
|
|
1451
|
+
} catch {
|
|
1452
|
+
return `<${className}>`;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
function convertMessage(message) {
|
|
1456
|
+
if (typeof message !== "object" || message === null) {
|
|
1457
|
+
return { role: "unknown", content: String(message) };
|
|
1458
|
+
}
|
|
1459
|
+
const msg = message;
|
|
1460
|
+
if (typeof msg.toDict === "function") {
|
|
1461
|
+
return msg.toDict();
|
|
1462
|
+
}
|
|
1463
|
+
const typeToRole = {
|
|
1464
|
+
human: "user",
|
|
1465
|
+
ai: "assistant",
|
|
1466
|
+
system: "system",
|
|
1467
|
+
tool: "tool",
|
|
1468
|
+
function: "function"
|
|
1469
|
+
};
|
|
1470
|
+
const result = {};
|
|
1471
|
+
const msgType = msg._getType ? String(msg._getType()) : msg.type;
|
|
1472
|
+
result.role = (msgType ? typeToRole[msgType] : void 0) ?? msg.role ?? "unknown";
|
|
1473
|
+
result.content = msg.content ?? "";
|
|
1474
|
+
if (msg.tool_calls) {
|
|
1475
|
+
result.tool_calls = msg.tool_calls;
|
|
1476
|
+
}
|
|
1477
|
+
if (msg.tool_call_id) {
|
|
1478
|
+
result.tool_call_id = msg.tool_call_id;
|
|
1479
|
+
}
|
|
1480
|
+
if (msg.name) {
|
|
1481
|
+
result.name = msg.name;
|
|
1482
|
+
}
|
|
1483
|
+
return result;
|
|
1484
|
+
}
|
|
1485
|
+
function extractModelName(serialized, metadata) {
|
|
1486
|
+
if (serialized) {
|
|
1487
|
+
const kwargs = serialized.kwargs;
|
|
1488
|
+
if (kwargs) {
|
|
1489
|
+
const model = kwargs.model_name ?? kwargs.model ?? kwargs.model_id;
|
|
1490
|
+
if (model) {
|
|
1491
|
+
return String(model);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (metadata) {
|
|
1496
|
+
const lsModel = metadata.ls_model_name;
|
|
1497
|
+
if (lsModel) {
|
|
1498
|
+
return String(lsModel);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
function extractUsage2(output) {
|
|
1504
|
+
const usage = {};
|
|
1505
|
+
const llmOutput = output.llmOutput;
|
|
1506
|
+
const tokenUsage = llmOutput?.tokenUsage ?? llmOutput?.token_usage ?? llmOutput?.usage ?? {};
|
|
1507
|
+
const inputTokens = tokenUsage.promptTokens ?? tokenUsage.prompt_tokens ?? tokenUsage.input_tokens;
|
|
1508
|
+
const outputTokens = tokenUsage.completionTokens ?? tokenUsage.completion_tokens ?? tokenUsage.output_tokens;
|
|
1509
|
+
const totalTokens = tokenUsage.totalTokens ?? tokenUsage.total_tokens;
|
|
1510
|
+
if (inputTokens !== void 0 && inputTokens !== null) {
|
|
1511
|
+
usage.inputTokens = inputTokens;
|
|
1512
|
+
}
|
|
1513
|
+
if (outputTokens !== void 0 && outputTokens !== null) {
|
|
1514
|
+
usage.outputTokens = outputTokens;
|
|
1515
|
+
}
|
|
1516
|
+
if (totalTokens !== void 0 && totalTokens !== null) {
|
|
1517
|
+
usage.totalTokens = totalTokens;
|
|
1518
|
+
}
|
|
1519
|
+
return usage;
|
|
1520
|
+
}
|
|
1521
|
+
function extractLangGraphMetadata(metadata) {
|
|
1522
|
+
if (!metadata) {
|
|
1523
|
+
return {};
|
|
1524
|
+
}
|
|
1525
|
+
const result = {};
|
|
1526
|
+
for (const key of LANGGRAPH_METADATA_KEYS) {
|
|
1527
|
+
if (key in metadata) {
|
|
1528
|
+
result[key] = metadata[key];
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return result;
|
|
1532
|
+
}
|
|
1533
|
+
var BitfabLangGraphCallbackHandler = class {
|
|
1534
|
+
constructor(config) {
|
|
1535
|
+
this.name = "BitfabLangGraphCallbackHandler";
|
|
1536
|
+
this.ignoreRetriever = true;
|
|
1537
|
+
this.ignoreRetry = true;
|
|
1538
|
+
this.ignoreCustomEvent = true;
|
|
1539
|
+
this.runToSpan = /* @__PURE__ */ new Map();
|
|
1540
|
+
this.invocations = /* @__PURE__ */ new Map();
|
|
1541
|
+
this.httpClient = new HttpClient({
|
|
1542
|
+
apiKey: config.apiKey,
|
|
1543
|
+
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
1544
|
+
timeout: config.timeout ?? 1e4
|
|
1545
|
+
});
|
|
1546
|
+
this.traceFunctionKey = config.traceFunctionKey;
|
|
1547
|
+
this.getActiveSpanContext = config.getActiveSpanContext ?? null;
|
|
1548
|
+
}
|
|
1549
|
+
// ── lifecycle helpers ──────────────────────────────────────────
|
|
1550
|
+
startSpan(runId, parentRunId, name, spanType, inputData, metadata, tags) {
|
|
1551
|
+
const parentSpan = parentRunId ? this.runToSpan.get(parentRunId) : void 0;
|
|
1552
|
+
const willHide = tags?.includes(LANGSMITH_HIDDEN_TAG) === true;
|
|
1553
|
+
let invocation;
|
|
1554
|
+
let effectiveParentId;
|
|
1555
|
+
if (parentSpan) {
|
|
1556
|
+
const existing = this.invocations.get(parentSpan.rootRunId);
|
|
1557
|
+
if (existing) {
|
|
1558
|
+
invocation = existing;
|
|
1559
|
+
} else {
|
|
1560
|
+
invocation = {
|
|
1561
|
+
traceId: parentSpan.traceId,
|
|
1562
|
+
activeContext: null,
|
|
1563
|
+
rootRunId: parentSpan.rootRunId
|
|
1564
|
+
};
|
|
1565
|
+
this.invocations.set(invocation.rootRunId, invocation);
|
|
1566
|
+
}
|
|
1567
|
+
if (!willHide) {
|
|
1568
|
+
let resolved = parentSpan;
|
|
1569
|
+
while (resolved?.hidden === true) {
|
|
1570
|
+
resolved = resolved.parentId ? this.runToSpan.get(resolved.parentId) : void 0;
|
|
1571
|
+
}
|
|
1572
|
+
effectiveParentId = resolved ? resolved.spanId : invocation.activeContext?.spanId ?? null;
|
|
1573
|
+
} else {
|
|
1574
|
+
effectiveParentId = parentRunId ?? null;
|
|
1575
|
+
}
|
|
1576
|
+
} else {
|
|
1577
|
+
const activeContext = this.getActiveSpanContext?.() ?? null;
|
|
1578
|
+
invocation = {
|
|
1579
|
+
traceId: activeContext ? activeContext.traceId : crypto.randomUUID(),
|
|
1580
|
+
activeContext,
|
|
1581
|
+
rootRunId: runId
|
|
1582
|
+
};
|
|
1583
|
+
this.invocations.set(runId, invocation);
|
|
1584
|
+
effectiveParentId = activeContext?.spanId ?? null;
|
|
1585
|
+
}
|
|
1586
|
+
const lgMetadata = extractLangGraphMetadata(metadata);
|
|
1587
|
+
const contexts = Object.keys(lgMetadata).length > 0 ? [lgMetadata] : [];
|
|
1588
|
+
const spanInfo = {
|
|
1589
|
+
spanId: runId,
|
|
1590
|
+
traceId: invocation.traceId,
|
|
1591
|
+
rootRunId: invocation.rootRunId,
|
|
1592
|
+
parentId: effectiveParentId,
|
|
1593
|
+
startedAt: nowIso2(),
|
|
1594
|
+
name,
|
|
1595
|
+
type: spanType,
|
|
1596
|
+
input: safeSerialize2(inputData),
|
|
1597
|
+
contexts
|
|
1598
|
+
};
|
|
1599
|
+
if (willHide) {
|
|
1600
|
+
spanInfo.hidden = true;
|
|
1601
|
+
}
|
|
1602
|
+
this.runToSpan.set(runId, spanInfo);
|
|
1603
|
+
return spanInfo;
|
|
1604
|
+
}
|
|
1605
|
+
completeSpan(runId, output, error, extraContexts) {
|
|
1606
|
+
const spanInfo = this.runToSpan.get(runId);
|
|
1607
|
+
if (!spanInfo) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
this.runToSpan.delete(runId);
|
|
1611
|
+
spanInfo.endedAt = nowIso2();
|
|
1612
|
+
spanInfo.output = safeSerialize2(output);
|
|
1613
|
+
if (error !== void 0) {
|
|
1614
|
+
spanInfo.error = error;
|
|
1615
|
+
}
|
|
1616
|
+
if (extraContexts && Object.keys(extraContexts).length > 0) {
|
|
1617
|
+
spanInfo.contexts.push(extraContexts);
|
|
1618
|
+
}
|
|
1619
|
+
this.sendSpan(spanInfo);
|
|
1620
|
+
if (runId === spanInfo.rootRunId) {
|
|
1621
|
+
const invocation = this.invocations.get(runId);
|
|
1622
|
+
this.sendTraceCompletion(spanInfo, invocation?.activeContext ?? null);
|
|
1623
|
+
this.invocations.delete(runId);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
sendSpan(spanInfo) {
|
|
1627
|
+
const spanData = {
|
|
1628
|
+
name: spanInfo.name,
|
|
1629
|
+
type: spanInfo.type
|
|
1630
|
+
};
|
|
1631
|
+
if (spanInfo.input !== void 0) {
|
|
1632
|
+
spanData.input = spanInfo.input;
|
|
1633
|
+
}
|
|
1634
|
+
if (spanInfo.output !== void 0) {
|
|
1635
|
+
spanData.output = spanInfo.output;
|
|
1636
|
+
}
|
|
1637
|
+
if (spanInfo.error !== void 0) {
|
|
1638
|
+
spanData.error = spanInfo.error;
|
|
1639
|
+
}
|
|
1640
|
+
if (spanInfo.contexts.length > 0) {
|
|
1641
|
+
spanData.contexts = spanInfo.contexts;
|
|
1642
|
+
}
|
|
1643
|
+
if (spanInfo.hidden) {
|
|
1644
|
+
spanData.hidden = true;
|
|
1645
|
+
}
|
|
1646
|
+
const rawSpan = {
|
|
1647
|
+
id: spanInfo.spanId,
|
|
1648
|
+
trace_id: spanInfo.traceId,
|
|
1649
|
+
started_at: spanInfo.startedAt,
|
|
1650
|
+
ended_at: spanInfo.endedAt ?? nowIso2(),
|
|
1651
|
+
span_data: spanData
|
|
1652
|
+
};
|
|
1653
|
+
if (spanInfo.parentId !== null) {
|
|
1654
|
+
rawSpan.parent_id = spanInfo.parentId;
|
|
1655
|
+
}
|
|
1656
|
+
const payload = {
|
|
1657
|
+
type: "sdk-function",
|
|
1658
|
+
source: "typescript-sdk-langgraph",
|
|
1659
|
+
traceFunctionKey: this.traceFunctionKey,
|
|
1660
|
+
sourceTraceId: spanInfo.traceId,
|
|
1661
|
+
rawSpan
|
|
1662
|
+
};
|
|
1663
|
+
try {
|
|
1664
|
+
this.httpClient.sendExternalSpan(payload);
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
sendTraceCompletion(rootSpan, activeContext) {
|
|
1669
|
+
const completed = activeContext === null;
|
|
1670
|
+
const traceData = {
|
|
1671
|
+
type: "sdk-function",
|
|
1672
|
+
source: "typescript-sdk-langgraph",
|
|
1673
|
+
traceFunctionKey: this.traceFunctionKey,
|
|
1674
|
+
externalTrace: {
|
|
1675
|
+
id: rootSpan.traceId,
|
|
1676
|
+
started_at: rootSpan.startedAt,
|
|
1677
|
+
ended_at: rootSpan.endedAt ?? nowIso2(),
|
|
1678
|
+
workflow_name: this.traceFunctionKey
|
|
1679
|
+
},
|
|
1680
|
+
completed
|
|
1681
|
+
};
|
|
1682
|
+
try {
|
|
1683
|
+
this.httpClient.sendExternalTrace(traceData);
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
// ── chain callbacks (graph nodes) ─────────────────────────────
|
|
1688
|
+
async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata) {
|
|
1689
|
+
try {
|
|
1690
|
+
const idArr = chain.id;
|
|
1691
|
+
const name = chain.name ?? idArr?.[idArr.length - 1] ?? "chain";
|
|
1692
|
+
this.startSpan(
|
|
1693
|
+
runId,
|
|
1694
|
+
parentRunId,
|
|
1695
|
+
String(name),
|
|
1696
|
+
"agent",
|
|
1697
|
+
inputs,
|
|
1698
|
+
metadata,
|
|
1699
|
+
tags
|
|
1700
|
+
);
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
async handleChainEnd(outputs, runId) {
|
|
1705
|
+
try {
|
|
1706
|
+
this.completeSpan(runId, outputs);
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
async handleChainError(error, runId) {
|
|
1711
|
+
try {
|
|
1712
|
+
const errorObj = error;
|
|
1713
|
+
if (errorObj?.constructor?.name === "GraphBubbleUp") {
|
|
1714
|
+
this.completeSpan(runId, void 0, void 0);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
this.completeSpan(
|
|
1718
|
+
runId,
|
|
1719
|
+
void 0,
|
|
1720
|
+
error instanceof Error ? error.message : String(error)
|
|
1721
|
+
);
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
// ── LLM callbacks ─────────────────────────────────────────────
|
|
1726
|
+
async handleChatModelStart(llm, messages, runId, parentRunId, _extraParams, tags, metadata) {
|
|
1727
|
+
try {
|
|
1728
|
+
const model = extractModelName(llm, metadata);
|
|
1729
|
+
const idArr = llm.id;
|
|
1730
|
+
const name = model ?? idArr?.[idArr.length - 1] ?? "llm";
|
|
1731
|
+
const converted = messages.map((batch) => batch.map(convertMessage));
|
|
1732
|
+
const spanInfo = this.startSpan(
|
|
1733
|
+
runId,
|
|
1734
|
+
parentRunId,
|
|
1735
|
+
String(name),
|
|
1736
|
+
"llm",
|
|
1737
|
+
converted,
|
|
1738
|
+
metadata,
|
|
1739
|
+
tags
|
|
1740
|
+
);
|
|
1741
|
+
spanInfo.model = model;
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async handleLLMStart(llm, prompts, runId, parentRunId, _extraParams, tags, metadata) {
|
|
1746
|
+
try {
|
|
1747
|
+
const model = extractModelName(llm, metadata);
|
|
1748
|
+
const idArr = llm.id;
|
|
1749
|
+
const name = model ?? idArr?.[idArr.length - 1] ?? "llm";
|
|
1750
|
+
const spanInfo = this.startSpan(
|
|
1751
|
+
runId,
|
|
1752
|
+
parentRunId,
|
|
1753
|
+
String(name),
|
|
1754
|
+
"llm",
|
|
1755
|
+
prompts,
|
|
1756
|
+
metadata,
|
|
1757
|
+
tags
|
|
1758
|
+
);
|
|
1759
|
+
spanInfo.model = model;
|
|
1760
|
+
} catch {
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async handleLLMEnd(output, runId) {
|
|
1764
|
+
try {
|
|
1765
|
+
let llmOutput;
|
|
1766
|
+
const generations = output.generations;
|
|
1767
|
+
if (generations?.length && generations[generations.length - 1]?.length) {
|
|
1768
|
+
const gen = generations[generations.length - 1][generations[generations.length - 1].length - 1];
|
|
1769
|
+
const msg = gen.message;
|
|
1770
|
+
llmOutput = msg ? convertMessage(msg) : gen.text ?? String(gen);
|
|
1771
|
+
}
|
|
1772
|
+
const usage = extractUsage2(output);
|
|
1773
|
+
const spanInfo = this.runToSpan.get(runId);
|
|
1774
|
+
const model = spanInfo?.model;
|
|
1775
|
+
const llmContext = {};
|
|
1776
|
+
if (model) {
|
|
1777
|
+
llmContext.model = model;
|
|
1778
|
+
}
|
|
1779
|
+
Object.assign(llmContext, usage);
|
|
1780
|
+
this.completeSpan(
|
|
1781
|
+
runId,
|
|
1782
|
+
llmOutput,
|
|
1783
|
+
void 0,
|
|
1784
|
+
Object.keys(llmContext).length > 0 ? llmContext : void 0
|
|
1785
|
+
);
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
async handleLLMError(error, runId) {
|
|
1790
|
+
try {
|
|
1791
|
+
this.completeSpan(
|
|
1792
|
+
runId,
|
|
1793
|
+
void 0,
|
|
1794
|
+
error instanceof Error ? error.message : String(error)
|
|
1795
|
+
);
|
|
1796
|
+
} catch {
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async handleLLMNewToken() {
|
|
1800
|
+
}
|
|
1801
|
+
// ── tool callbacks ────────────────────────────────────────────
|
|
1802
|
+
async handleToolStart(tool, input, runId, parentRunId, tags, metadata) {
|
|
1803
|
+
try {
|
|
1804
|
+
const name = tool.name ?? "tool";
|
|
1805
|
+
this.startSpan(
|
|
1806
|
+
runId,
|
|
1807
|
+
parentRunId,
|
|
1808
|
+
String(name),
|
|
1809
|
+
"function",
|
|
1810
|
+
input,
|
|
1811
|
+
metadata,
|
|
1812
|
+
tags
|
|
1813
|
+
);
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async handleToolEnd(output, runId) {
|
|
1818
|
+
try {
|
|
1819
|
+
this.completeSpan(runId, output);
|
|
1820
|
+
} catch {
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
async handleToolError(error, runId) {
|
|
1824
|
+
try {
|
|
1825
|
+
this.completeSpan(
|
|
1826
|
+
runId,
|
|
1827
|
+
void 0,
|
|
1828
|
+
error instanceof Error ? error.message : String(error)
|
|
1829
|
+
);
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
// ── retriever callbacks ───────────────────────────────────────
|
|
1834
|
+
async handleRetrieverStart(retriever, query, runId, parentRunId, tags, metadata) {
|
|
1835
|
+
try {
|
|
1836
|
+
const name = retriever.name ?? "retriever";
|
|
1837
|
+
this.startSpan(
|
|
1838
|
+
runId,
|
|
1839
|
+
parentRunId,
|
|
1840
|
+
String(name),
|
|
1841
|
+
"function",
|
|
1842
|
+
query,
|
|
1843
|
+
metadata,
|
|
1844
|
+
tags
|
|
1845
|
+
);
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async handleRetrieverEnd(documents, runId) {
|
|
1850
|
+
try {
|
|
1851
|
+
this.completeSpan(runId, documents);
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async handleRetrieverError(error, runId) {
|
|
1856
|
+
try {
|
|
1857
|
+
this.completeSpan(
|
|
1858
|
+
runId,
|
|
1859
|
+
void 0,
|
|
1860
|
+
error instanceof Error ? error.message : String(error)
|
|
1861
|
+
);
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
// src/client.ts
|
|
1868
|
+
init_replayContext();
|
|
1869
|
+
init_serialize();
|
|
1870
|
+
|
|
1871
|
+
// src/tracing.ts
|
|
1872
|
+
init_constants();
|
|
1873
|
+
init_http();
|
|
1874
|
+
var BitfabOpenAITracingProcessor = class {
|
|
1875
|
+
/**
|
|
1876
|
+
* Initialize the tracing processor.
|
|
1877
|
+
*
|
|
1878
|
+
* @param config - Configuration options
|
|
1879
|
+
*/
|
|
1880
|
+
constructor(config) {
|
|
1881
|
+
this.activeTraces = {};
|
|
1882
|
+
this.activeSpanMappings = {};
|
|
1883
|
+
this.httpClient = new HttpClient({
|
|
1884
|
+
apiKey: config.apiKey,
|
|
1885
|
+
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
1886
|
+
timeout: config.timeout ?? 1e4
|
|
1887
|
+
});
|
|
1888
|
+
this.getActiveSpanContext = config.getActiveSpanContext ?? null;
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Called when a trace is started.
|
|
1892
|
+
* If there's an active withSpan context, the trace ID is remapped to the
|
|
1893
|
+
* outer trace and sent to pre-create the external_traces row on the server.
|
|
1894
|
+
*/
|
|
1895
|
+
async onTraceStart(trace) {
|
|
1896
|
+
this.activeTraces[trace.traceId] = trace;
|
|
1897
|
+
const activeContext = this.getActiveSpanContext?.();
|
|
1898
|
+
if (activeContext) {
|
|
1899
|
+
this.activeSpanMappings[trace.traceId] = activeContext;
|
|
1900
|
+
}
|
|
1901
|
+
this.sendTrace(trace, activeContext ? { id: activeContext.traceId } : {});
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Called when a trace is ended.
|
|
1905
|
+
* If mapped to a withSpan trace, sends with remapped ID and completed=false
|
|
1906
|
+
* since the parent withSpan handles completion.
|
|
1907
|
+
*/
|
|
1908
|
+
async onTraceEnd(trace) {
|
|
1909
|
+
const mapping = this.activeSpanMappings[trace.traceId];
|
|
1910
|
+
this.sendTrace(
|
|
1911
|
+
trace,
|
|
1912
|
+
mapping ? { id: mapping.traceId } : { completed: true }
|
|
1913
|
+
);
|
|
1914
|
+
delete this.activeSpanMappings[trace.traceId];
|
|
1915
|
+
delete this.activeTraces[trace.traceId];
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Called when a span is started.
|
|
1919
|
+
*/
|
|
1920
|
+
// biome-ignore lint/suspicious/noExplicitAny: OpenAI Agents SDK uses any for span data
|
|
1921
|
+
async onSpanStart(span) {
|
|
1922
|
+
this.sendSpan(span);
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Called when a span is ended.
|
|
1926
|
+
*
|
|
1927
|
+
* Send all spans to Bitfab for complete trace capture.
|
|
1928
|
+
*/
|
|
1929
|
+
// biome-ignore lint/suspicious/noExplicitAny: OpenAI Agents SDK uses any for span data
|
|
1930
|
+
async onSpanEnd(span) {
|
|
1931
|
+
this.sendSpan(span);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Called when a trace is being flushed.
|
|
1935
|
+
*/
|
|
1936
|
+
async forceFlush() {
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Called when the trace processor is shutting down.
|
|
1940
|
+
*/
|
|
1941
|
+
async shutdown(_timeout) {
|
|
1942
|
+
this.activeTraces = {};
|
|
1943
|
+
this.activeSpanMappings = {};
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Send trace to Bitfab API (fire-and-forget).
|
|
1947
|
+
* When traceIdOverride is provided, the trace ID is remapped to link
|
|
1948
|
+
* the OpenAI trace into an outer withSpan trace.
|
|
1949
|
+
*/
|
|
1950
|
+
sendTrace(trace, overrides = {}) {
|
|
1951
|
+
try {
|
|
1952
|
+
const { completed, ...traceOverrides } = overrides;
|
|
1953
|
+
const traceData = trace.toJSON();
|
|
1954
|
+
Object.assign(traceData, traceOverrides);
|
|
1955
|
+
this.httpClient.sendExternalTrace({
|
|
1956
|
+
type: "openai",
|
|
1957
|
+
source: "typescript-sdk-openai-tracing",
|
|
1958
|
+
externalTrace: traceData,
|
|
1959
|
+
completed: completed ?? false
|
|
1960
|
+
});
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Export span to JSON object, collecting any errors.
|
|
1966
|
+
*/
|
|
1967
|
+
exportSpan(span) {
|
|
1968
|
+
const errors = [];
|
|
1969
|
+
let serializedSpan;
|
|
1970
|
+
try {
|
|
1971
|
+
const jsonResult = span.toJSON();
|
|
1972
|
+
if (typeof jsonResult !== "object" || jsonResult === null) {
|
|
1973
|
+
errors.push({
|
|
1974
|
+
step: "span.toJSON()",
|
|
1975
|
+
error: `Returned unexpected type: ${typeof jsonResult}`
|
|
1976
|
+
});
|
|
1977
|
+
serializedSpan = {};
|
|
1978
|
+
} else {
|
|
1979
|
+
serializedSpan = jsonResult;
|
|
1980
|
+
}
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
errors.push({
|
|
1983
|
+
step: "span.toJSON()",
|
|
1984
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1985
|
+
});
|
|
1986
|
+
serializedSpan = {};
|
|
1987
|
+
}
|
|
1988
|
+
if (!serializedSpan.span_data) {
|
|
1989
|
+
serializedSpan.span_data = {};
|
|
1990
|
+
}
|
|
1991
|
+
return [serializedSpan, errors];
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Extract and add input/response to serialized span, updating errors list.
|
|
1995
|
+
*/
|
|
1996
|
+
extractSpanInputResponse(span, serializedSpan, errors) {
|
|
1997
|
+
const spanData = serializedSpan.span_data;
|
|
1998
|
+
try {
|
|
1999
|
+
spanData.input = span.spanData?._input || [];
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
errors.push({
|
|
2002
|
+
step: "access_input",
|
|
2003
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
try {
|
|
2007
|
+
spanData.response = span.spanData?._response || null;
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
errors.push({
|
|
2010
|
+
step: "access_response",
|
|
2011
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* If the span's trace is mapped to a withSpan trace, rewrite trace_id and parent_id.
|
|
2017
|
+
*/
|
|
2018
|
+
applySpanOverrides(serializedSpan, traceId) {
|
|
2019
|
+
const mapping = this.activeSpanMappings[traceId];
|
|
2020
|
+
if (mapping) {
|
|
2021
|
+
serializedSpan.trace_id = mapping.traceId;
|
|
2022
|
+
if (!serializedSpan.parent_id) {
|
|
2023
|
+
serializedSpan.parent_id = mapping.spanId;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Build span payload for the external spans API.
|
|
2029
|
+
*/
|
|
2030
|
+
buildSpanPayload(serializedSpan, errors) {
|
|
2031
|
+
const payload = {
|
|
2032
|
+
type: "openai",
|
|
2033
|
+
source: "typescript-sdk-openai-tracing",
|
|
2034
|
+
sourceTraceId: serializedSpan.trace_id ?? "unknown",
|
|
2035
|
+
rawSpan: serializedSpan
|
|
2036
|
+
};
|
|
2037
|
+
if (errors.length > 0) {
|
|
2038
|
+
payload.errors = errors;
|
|
2039
|
+
}
|
|
2040
|
+
return payload;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Send span to Bitfab API (fire-and-forget).
|
|
2044
|
+
* If the span belongs to a trace mapped to a withSpan trace, the trace_id
|
|
2045
|
+
* and parent_id are rewritten to link the span into the withSpan tree.
|
|
2046
|
+
*/
|
|
2047
|
+
sendSpan(span) {
|
|
2048
|
+
const errors = [];
|
|
2049
|
+
const [serializedSpan, exportErrors] = this.exportSpan(span);
|
|
2050
|
+
errors.push(...exportErrors);
|
|
2051
|
+
this.extractSpanInputResponse(span, serializedSpan, errors);
|
|
2052
|
+
this.applySpanOverrides(serializedSpan, span.traceId ?? "");
|
|
2053
|
+
const payload = this.buildSpanPayload(serializedSpan, errors);
|
|
2054
|
+
this.httpClient.sendExternalSpan(payload);
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
// src/client.ts
|
|
2059
|
+
var activeTraceStates = /* @__PURE__ */ new Map();
|
|
2060
|
+
var pendingSpanPromises = /* @__PURE__ */ new Map();
|
|
2061
|
+
var asyncLocalStorage = null;
|
|
2062
|
+
var asyncLocalStorageReady = asyncStorageReady.then(() => {
|
|
2063
|
+
asyncLocalStorage = createAsyncLocalStorage();
|
|
2064
|
+
});
|
|
2065
|
+
var browserSpanStack = [];
|
|
2066
|
+
function getSpanStack() {
|
|
2067
|
+
if (asyncLocalStorage) {
|
|
2068
|
+
return asyncLocalStorage.getStore() ?? [];
|
|
2069
|
+
}
|
|
2070
|
+
return browserSpanStack;
|
|
2071
|
+
}
|
|
2072
|
+
function runWithSpanStack(stack, fn) {
|
|
2073
|
+
if (asyncLocalStorage) {
|
|
2074
|
+
return asyncLocalStorage.run(stack, fn);
|
|
2075
|
+
}
|
|
2076
|
+
const previousStack = browserSpanStack;
|
|
2077
|
+
browserSpanStack = stack;
|
|
2078
|
+
try {
|
|
2079
|
+
const result = fn();
|
|
2080
|
+
if (result instanceof Promise) {
|
|
2081
|
+
return result.finally(() => {
|
|
2082
|
+
browserSpanStack = previousStack;
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
browserSpanStack = previousStack;
|
|
2086
|
+
return result;
|
|
2087
|
+
} catch (error) {
|
|
2088
|
+
browserSpanStack = previousStack;
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function isAsyncGenerator(value) {
|
|
2093
|
+
if (value === null || typeof value !== "object") {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
const candidate = value;
|
|
2097
|
+
return typeof candidate.next === "function" && typeof candidate.return === "function" && typeof candidate.throw === "function" && typeof candidate[Symbol.asyncIterator] === "function";
|
|
2098
|
+
}
|
|
2099
|
+
function wrapAsyncGenerator(source, spanStack, sendSpan) {
|
|
2100
|
+
const yielded = [];
|
|
2101
|
+
let returnValue;
|
|
2102
|
+
let finalized = false;
|
|
2103
|
+
const finalize = (errorMsg) => {
|
|
2104
|
+
if (finalized) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
finalized = true;
|
|
2108
|
+
void sendSpan({
|
|
2109
|
+
result: { yielded, return: returnValue },
|
|
2110
|
+
...errorMsg && { error: errorMsg }
|
|
2111
|
+
});
|
|
2112
|
+
};
|
|
2113
|
+
const step = (method, arg) => runWithSpanStack(spanStack, () => {
|
|
2114
|
+
const op = source[method];
|
|
2115
|
+
return op.call(source, arg);
|
|
2116
|
+
});
|
|
2117
|
+
const handle = async (method, arg) => {
|
|
2118
|
+
try {
|
|
2119
|
+
const result = await step(method, arg);
|
|
2120
|
+
if (result.done) {
|
|
2121
|
+
returnValue = result.value;
|
|
2122
|
+
finalize();
|
|
2123
|
+
} else {
|
|
2124
|
+
yielded.push(result.value);
|
|
2125
|
+
}
|
|
2126
|
+
return result;
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
finalize(error instanceof Error ? error.message : String(error));
|
|
2129
|
+
throw error;
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
const wrapped = {
|
|
2133
|
+
next(arg) {
|
|
2134
|
+
return handle("next", arg);
|
|
2135
|
+
},
|
|
2136
|
+
return(value) {
|
|
2137
|
+
return handle("return", value);
|
|
2138
|
+
},
|
|
2139
|
+
throw(err) {
|
|
2140
|
+
return handle("throw", err);
|
|
2141
|
+
},
|
|
2142
|
+
[Symbol.asyncIterator]() {
|
|
2143
|
+
return wrapped;
|
|
2144
|
+
},
|
|
2145
|
+
[Symbol.asyncDispose]() {
|
|
2146
|
+
return handle("return", void 0).then(() => void 0);
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
return wrapped;
|
|
2150
|
+
}
|
|
2151
|
+
var cachedCollectorClass;
|
|
2152
|
+
async function loadCollectorClass() {
|
|
2153
|
+
if (cachedCollectorClass !== void 0) {
|
|
2154
|
+
return cachedCollectorClass;
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
const baml = await import("@boundaryml/baml");
|
|
2158
|
+
cachedCollectorClass = baml.Collector;
|
|
2159
|
+
return cachedCollectorClass;
|
|
2160
|
+
} catch {
|
|
2161
|
+
cachedCollectorClass = null;
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function extractPromptFromCollector(collector) {
|
|
2166
|
+
try {
|
|
2167
|
+
const c = collector;
|
|
2168
|
+
const calls = c?.last?.calls ?? [];
|
|
2169
|
+
const selectedCall = calls.find((call) => call.selected) ?? calls[0];
|
|
2170
|
+
if (!selectedCall?.httpRequest?.body) {
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
const body = selectedCall.httpRequest.body.json();
|
|
2174
|
+
if (!body || typeof body !== "object") {
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
const messages = body.messages;
|
|
2178
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
const rendered = messages.filter(
|
|
2182
|
+
(msg) => typeof msg === "object" && msg !== null && "role" in msg && typeof msg.role === "string"
|
|
2183
|
+
).map((msg) => ({
|
|
2184
|
+
role: msg.role,
|
|
2185
|
+
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
|
2186
|
+
}));
|
|
2187
|
+
if (rendered.length > 0) {
|
|
2188
|
+
return JSON.stringify(rendered);
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
} catch {
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function extractContextFromCollector(collector) {
|
|
2196
|
+
try {
|
|
2197
|
+
const c = collector;
|
|
2198
|
+
const calls = c?.last?.calls ?? [];
|
|
2199
|
+
const selectedCall = calls.find((call) => call.selected) ?? calls[0];
|
|
2200
|
+
const usage = c?.usage;
|
|
2201
|
+
const context = {};
|
|
2202
|
+
if (selectedCall?.provider) {
|
|
2203
|
+
context.provider = selectedCall.provider;
|
|
2204
|
+
}
|
|
2205
|
+
const body = selectedCall?.httpRequest?.body?.json();
|
|
2206
|
+
if (body && typeof body === "object" && typeof body.model === "string") {
|
|
2207
|
+
context.model = body.model;
|
|
2208
|
+
} else {
|
|
2209
|
+
const url = selectedCall?.httpRequest?.url;
|
|
2210
|
+
if (url) {
|
|
2211
|
+
const match = url.match(/\/models\/([^/:]+)/);
|
|
2212
|
+
if (match?.[1]) {
|
|
2213
|
+
context.model = match[1];
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const inputTokens = usage?.inputTokens ?? selectedCall?.usage?.inputTokens ?? null;
|
|
2218
|
+
const outputTokens = usage?.outputTokens ?? selectedCall?.usage?.outputTokens ?? null;
|
|
2219
|
+
if (inputTokens !== null) {
|
|
2220
|
+
context.inputTokens = inputTokens;
|
|
2221
|
+
}
|
|
2222
|
+
if (outputTokens !== null) {
|
|
2223
|
+
context.outputTokens = outputTokens;
|
|
2224
|
+
}
|
|
2225
|
+
const durationMs = c?.last?.timing?.durationMs ?? null;
|
|
2226
|
+
if (durationMs !== null) {
|
|
2227
|
+
context.durationMs = durationMs;
|
|
2228
|
+
}
|
|
2229
|
+
return Object.keys(context).length > 0 ? context : null;
|
|
2230
|
+
} catch {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
var TRACE_ID_PATTERN = /^[a-zA-Z0-9_\-.:]+$/;
|
|
2235
|
+
var TRACE_ID_MAX_LENGTH = 256;
|
|
2236
|
+
function validateTraceId(traceId) {
|
|
2237
|
+
if (typeof traceId !== "string" || traceId.length === 0) {
|
|
2238
|
+
throw new BitfabError("traceId is required and must be a non-empty string");
|
|
2239
|
+
}
|
|
2240
|
+
if (traceId.length > TRACE_ID_MAX_LENGTH) {
|
|
2241
|
+
throw new BitfabError(
|
|
2242
|
+
`traceId must be ${TRACE_ID_MAX_LENGTH} characters or fewer`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
if (!TRACE_ID_PATTERN.test(traceId)) {
|
|
2246
|
+
throw new BitfabError(
|
|
2247
|
+
`traceId may only contain letters, digits, "_", "-", ".", ":"`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
var noOpSpan = {
|
|
2252
|
+
traceId: "",
|
|
2253
|
+
addContext() {
|
|
2254
|
+
},
|
|
2255
|
+
setPrompt() {
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
var noOpTrace = {
|
|
2259
|
+
setSessionId() {
|
|
2260
|
+
},
|
|
2261
|
+
setMetadata() {
|
|
2262
|
+
},
|
|
2263
|
+
addContext() {
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
function getCurrentSpan() {
|
|
2267
|
+
const stack = getSpanStack();
|
|
2268
|
+
const current = stack[stack.length - 1];
|
|
2269
|
+
if (!current) {
|
|
2270
|
+
return noOpSpan;
|
|
2271
|
+
}
|
|
2272
|
+
return {
|
|
2273
|
+
traceId: current.traceId,
|
|
2274
|
+
addContext(context) {
|
|
2275
|
+
try {
|
|
2276
|
+
if (typeof context !== "object" || context === null) {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
current.contexts.push(context);
|
|
2280
|
+
} catch {
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
2283
|
+
setPrompt(prompt) {
|
|
2284
|
+
try {
|
|
2285
|
+
if (typeof prompt !== "string") {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
current.prompt = prompt;
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
function getCurrentTrace() {
|
|
2295
|
+
const stack = getSpanStack();
|
|
2296
|
+
const current = stack[stack.length - 1];
|
|
2297
|
+
if (!current) {
|
|
2298
|
+
return noOpTrace;
|
|
2299
|
+
}
|
|
2300
|
+
const traceId = current.traceId;
|
|
2301
|
+
const getOrCreateTraceState = () => {
|
|
2302
|
+
let traceState = activeTraceStates.get(traceId);
|
|
2303
|
+
if (!traceState) {
|
|
2304
|
+
traceState = {
|
|
2305
|
+
traceId,
|
|
2306
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2307
|
+
contexts: []
|
|
2308
|
+
};
|
|
2309
|
+
activeTraceStates.set(traceId, traceState);
|
|
2310
|
+
}
|
|
2311
|
+
return traceState;
|
|
2312
|
+
};
|
|
2313
|
+
return {
|
|
2314
|
+
setSessionId(sessionId) {
|
|
2315
|
+
try {
|
|
2316
|
+
const traceState = getOrCreateTraceState();
|
|
2317
|
+
traceState.sessionId = sessionId;
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
},
|
|
2321
|
+
setMetadata(metadata) {
|
|
2322
|
+
try {
|
|
2323
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const traceState = getOrCreateTraceState();
|
|
2327
|
+
traceState.metadata = { ...traceState.metadata, ...metadata };
|
|
2328
|
+
} catch {
|
|
2329
|
+
}
|
|
2330
|
+
},
|
|
2331
|
+
addContext(context) {
|
|
2332
|
+
try {
|
|
2333
|
+
if (typeof context !== "object" || context === null) {
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
const traceState = getOrCreateTraceState();
|
|
2337
|
+
traceState.contexts.push(context);
|
|
2338
|
+
} catch {
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
var Bitfab = class {
|
|
2344
|
+
/**
|
|
2345
|
+
* Initialize the Bitfab client.
|
|
2346
|
+
*
|
|
2347
|
+
* @param config - Configuration options for the client
|
|
2348
|
+
*/
|
|
2349
|
+
constructor(config) {
|
|
2350
|
+
this.apiKey = config.apiKey;
|
|
2351
|
+
this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
|
|
2352
|
+
this.timeout = config.timeout ?? 12e4;
|
|
2353
|
+
this.envVars = config.envVars ?? {};
|
|
2354
|
+
const enabled = config.enabled ?? true;
|
|
2355
|
+
if (enabled && (!config.apiKey || config.apiKey.trim() === "")) {
|
|
2356
|
+
console.warn(
|
|
2357
|
+
"Bitfab: apiKey is empty \u2014 tracing is disabled. Provide a valid API key to enable tracing."
|
|
2358
|
+
);
|
|
2359
|
+
this.enabled = false;
|
|
2360
|
+
} else {
|
|
2361
|
+
this.enabled = enabled;
|
|
2362
|
+
}
|
|
2363
|
+
this.bamlClient = config.bamlClient ?? null;
|
|
2364
|
+
this.httpClient = new HttpClient({
|
|
2365
|
+
apiKey: this.apiKey,
|
|
2366
|
+
serviceUrl: this.serviceUrl,
|
|
2367
|
+
timeout: this.timeout
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Fetch the function with its current version and BAML prompt from the server.
|
|
2372
|
+
*
|
|
2373
|
+
* @param methodName - The name of the method to fetch
|
|
2374
|
+
* @returns The function with current version, BAML prompt, and provider definitions
|
|
2375
|
+
* @throws {BitfabError} If the function is not found or an error occurs
|
|
2376
|
+
*/
|
|
2377
|
+
async fetchFunctionVersion(methodName) {
|
|
2378
|
+
const result = await this.httpClient.lookupFunction(methodName);
|
|
2379
|
+
if (result.id === null) {
|
|
2380
|
+
throw new BitfabError(
|
|
2381
|
+
`Function "${methodName}" not found. Create it at: ${this.serviceUrl}/functions`,
|
|
2382
|
+
"/functions"
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
if (!result.prompt) {
|
|
2386
|
+
throw new BitfabError(
|
|
2387
|
+
`Function "${methodName}" has no prompt configured. Add one at: ${this.serviceUrl}/functions/${result.id}`,
|
|
2388
|
+
`/functions/${result.id}`
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
return result;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Call a method with the given named arguments via BAML execution.
|
|
2395
|
+
*
|
|
2396
|
+
* @param methodName - The name of the method to call
|
|
2397
|
+
* @param inputs - Named arguments to pass to the method
|
|
2398
|
+
* @returns The result of the BAML function execution
|
|
2399
|
+
* @throws {BitfabError} If service_url is not set, or if an error occurs
|
|
2400
|
+
*/
|
|
2401
|
+
async call(methodName, inputs = {}) {
|
|
2402
|
+
try {
|
|
2403
|
+
const functionVersion = await this.fetchFunctionVersion(methodName);
|
|
2404
|
+
const executionResult = await runFunctionWithBaml(
|
|
2405
|
+
functionVersion.prompt,
|
|
2406
|
+
inputs,
|
|
2407
|
+
functionVersion.providers,
|
|
2408
|
+
this.envVars
|
|
2409
|
+
);
|
|
2410
|
+
const resultStr = typeof executionResult.result === "string" ? executionResult.result : JSON.stringify(executionResult.result);
|
|
2411
|
+
this.httpClient.sendInternalTrace(functionVersion.id, {
|
|
2412
|
+
result: resultStr,
|
|
2413
|
+
source: "typescript-sdk",
|
|
2414
|
+
...Object.keys(inputs).length > 0 && { inputs },
|
|
2415
|
+
...executionResult.rawCollector != null && {
|
|
2416
|
+
rawCollector: executionResult.rawCollector
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
return executionResult.result;
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
if (error instanceof BitfabError) {
|
|
2422
|
+
throw error;
|
|
2423
|
+
}
|
|
2424
|
+
if (error instanceof Error) {
|
|
2425
|
+
throw new BitfabError(error.message);
|
|
2426
|
+
}
|
|
2427
|
+
throw new BitfabError("Unknown error occurred during local execution");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Get a tracing processor for OpenAI Agents SDK integration.
|
|
2432
|
+
*
|
|
2433
|
+
* This processor automatically captures traces and spans from the OpenAI Agents SDK
|
|
2434
|
+
* and sends them to Bitfab for monitoring and analysis.
|
|
2435
|
+
*
|
|
2436
|
+
* Example usage:
|
|
2437
|
+
* ```typescript
|
|
2438
|
+
* import { addTraceProcessor } from '@openai/agents';
|
|
2439
|
+
*
|
|
2440
|
+
* const client = new Bitfab({ apiKey: 'your-api-key' });
|
|
2441
|
+
* const processor = client.getOpenAiTracingProcessor();
|
|
2442
|
+
* addTraceProcessor(processor);
|
|
2443
|
+
* ```
|
|
2444
|
+
*
|
|
2445
|
+
* @returns A BitfabOpenAITracingProcessor instance configured for this client
|
|
2446
|
+
*/
|
|
2447
|
+
getOpenAiTracingProcessor() {
|
|
2448
|
+
return new BitfabOpenAITracingProcessor({
|
|
2449
|
+
apiKey: this.apiKey,
|
|
2450
|
+
serviceUrl: this.serviceUrl,
|
|
2451
|
+
getActiveSpanContext: () => {
|
|
2452
|
+
const stack = getSpanStack();
|
|
2453
|
+
return stack[stack.length - 1] ?? null;
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Get a LangGraph/LangChain callback handler for tracing.
|
|
2459
|
+
*
|
|
2460
|
+
* The handler captures graph node execution, LLM calls, and tool
|
|
2461
|
+
* invocations as Bitfab spans with proper parent-child hierarchy.
|
|
2462
|
+
*
|
|
2463
|
+
* ```typescript
|
|
2464
|
+
* const handler = client.getLangGraphCallbackHandler("my-agent");
|
|
2465
|
+
* const result = await agent.invoke(
|
|
2466
|
+
* { messages: [...] },
|
|
2467
|
+
* { callbacks: [handler] },
|
|
2468
|
+
* );
|
|
2469
|
+
* ```
|
|
2470
|
+
*
|
|
2471
|
+
* @param traceFunctionKey - Groups traces under this key in Bitfab
|
|
2472
|
+
* @returns A BitfabLangGraphCallbackHandler configured for this client
|
|
2473
|
+
*/
|
|
2474
|
+
getLangGraphCallbackHandler(traceFunctionKey) {
|
|
2475
|
+
return new BitfabLangGraphCallbackHandler({
|
|
2476
|
+
apiKey: this.apiKey,
|
|
2477
|
+
traceFunctionKey,
|
|
2478
|
+
serviceUrl: this.serviceUrl,
|
|
2479
|
+
getActiveSpanContext: () => {
|
|
2480
|
+
const stack = getSpanStack();
|
|
2481
|
+
return stack[stack.length - 1] ?? null;
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Get a Claude Agent SDK handler for tracing.
|
|
2487
|
+
*
|
|
2488
|
+
* The handler captures LLM turns, tool invocations, and subagent
|
|
2489
|
+
* execution as Bitfab spans with proper parent-child hierarchy.
|
|
2490
|
+
*
|
|
2491
|
+
* ```typescript
|
|
2492
|
+
* const handler = client.getClaudeAgentHandler("my-agent");
|
|
2493
|
+
* const options = handler.instrumentOptions({
|
|
2494
|
+
* model: "claude-sonnet-4-5-...",
|
|
2495
|
+
* });
|
|
2496
|
+
* const sdkClient = new ClaudeSDKClient(options);
|
|
2497
|
+
* await sdkClient.connect();
|
|
2498
|
+
* await sdkClient.query("Do something");
|
|
2499
|
+
* for await (const msg of handler.wrapResponse(sdkClient.receiveResponse())) {
|
|
2500
|
+
* // process messages
|
|
2501
|
+
* }
|
|
2502
|
+
* ```
|
|
2503
|
+
*
|
|
2504
|
+
* @param traceFunctionKey - Groups traces under this key in Bitfab
|
|
2505
|
+
* @returns A BitfabClaudeAgentHandler configured for this client
|
|
2506
|
+
*/
|
|
2507
|
+
getClaudeAgentHandler(traceFunctionKey) {
|
|
2508
|
+
return new BitfabClaudeAgentHandler({
|
|
2509
|
+
apiKey: this.apiKey,
|
|
2510
|
+
traceFunctionKey,
|
|
2511
|
+
serviceUrl: this.serviceUrl,
|
|
2512
|
+
getActiveSpanContext: () => {
|
|
2513
|
+
const stack = getSpanStack();
|
|
2514
|
+
return stack[stack.length - 1] ?? null;
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Wrap a BAML client method to automatically capture prompt and LLM metadata.
|
|
2520
|
+
*
|
|
2521
|
+
* Creates a BAML Collector, calls the method through a tracked client,
|
|
2522
|
+
* then extracts rendered messages and token usage — calling setPrompt()
|
|
2523
|
+
* and addContext() on the current span automatically.
|
|
2524
|
+
*
|
|
2525
|
+
* The BAML client can be provided in the constructor or passed explicitly:
|
|
2526
|
+
*
|
|
2527
|
+
* ```typescript
|
|
2528
|
+
* // Option 1: bamlClient in constructor (use wrapBAML with just the method)
|
|
2529
|
+
* const client = new Bitfab({ apiKey: 'your-api-key', bamlClient: b });
|
|
2530
|
+
* const traced = client.withSpan('classify', { type: 'llm' },
|
|
2531
|
+
* client.wrapBAML(b.ClassifyText)
|
|
2532
|
+
* );
|
|
2533
|
+
*
|
|
2534
|
+
* // Option 2: pass bamlClient at call site
|
|
2535
|
+
* const client = new Bitfab({ apiKey: 'your-api-key' });
|
|
2536
|
+
* const traced = client.withSpan('classify', { type: 'llm' },
|
|
2537
|
+
* client.wrapBAML(b, b.ClassifyText)
|
|
2538
|
+
* );
|
|
2539
|
+
* ```
|
|
2540
|
+
*
|
|
2541
|
+
* @param methodOrClient - Either a BAML method (uses constructor bamlClient) or the BAML client instance
|
|
2542
|
+
* @param maybeMethodOrOptions - The BAML method when the first argument is a client, or WrapBAMLOptions when the first argument is the method
|
|
2543
|
+
* @param maybeOptions - WrapBAMLOptions when using the two-argument (client, method) form
|
|
2544
|
+
* @returns An async function with the same signature that instruments the BAML call
|
|
2545
|
+
*/
|
|
2546
|
+
wrapBAML(methodOrClient, maybeMethodOrOptions, maybeOptions) {
|
|
2547
|
+
let bamlClient;
|
|
2548
|
+
let method;
|
|
2549
|
+
let options;
|
|
2550
|
+
if (typeof maybeMethodOrOptions === "function") {
|
|
2551
|
+
bamlClient = methodOrClient;
|
|
2552
|
+
method = maybeMethodOrOptions;
|
|
2553
|
+
options = maybeOptions;
|
|
2554
|
+
} else {
|
|
2555
|
+
bamlClient = this.bamlClient;
|
|
2556
|
+
method = methodOrClient;
|
|
2557
|
+
options = maybeMethodOrOptions;
|
|
2558
|
+
if (!bamlClient) {
|
|
2559
|
+
throw new BitfabError(
|
|
2560
|
+
"bamlClient is required for wrapBAML. Pass it in the constructor or as the first argument."
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
const methodName = method.name;
|
|
2565
|
+
if (!methodName) {
|
|
2566
|
+
throw new BitfabError(
|
|
2567
|
+
"wrapBAML requires a named function (e.g., b.ClassifyText)."
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
loadCollectorClass();
|
|
2571
|
+
const wrappedFn = async (...args) => {
|
|
2572
|
+
const CollectorClass = await loadCollectorClass();
|
|
2573
|
+
if (!CollectorClass) {
|
|
2574
|
+
wrappedFn.collector = null;
|
|
2575
|
+
return await bamlClient[methodName](...args);
|
|
2576
|
+
}
|
|
2577
|
+
const collector = new CollectorClass("bitfab-baml-tracing");
|
|
2578
|
+
const trackedClient = bamlClient.withOptions({ collector });
|
|
2579
|
+
const trackedMethod = trackedClient[methodName];
|
|
2580
|
+
const result = await trackedMethod.bind(
|
|
2581
|
+
trackedClient
|
|
2582
|
+
)(...args);
|
|
2583
|
+
wrappedFn.collector = collector;
|
|
2584
|
+
try {
|
|
2585
|
+
const prompt = extractPromptFromCollector(collector);
|
|
2586
|
+
if (prompt) {
|
|
2587
|
+
getCurrentSpan().setPrompt(prompt);
|
|
2588
|
+
}
|
|
2589
|
+
const metadata = extractContextFromCollector(collector);
|
|
2590
|
+
if (metadata) {
|
|
2591
|
+
getCurrentSpan().addContext(metadata);
|
|
2592
|
+
}
|
|
2593
|
+
} catch {
|
|
2594
|
+
}
|
|
2595
|
+
try {
|
|
2596
|
+
options?.onCollector?.(collector);
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
return result;
|
|
2600
|
+
};
|
|
2601
|
+
wrappedFn.collector = null;
|
|
2602
|
+
return wrappedFn;
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Wrap a function to automatically create a span for its inputs and outputs.
|
|
2606
|
+
*
|
|
2607
|
+
* The wrapped function behaves identically to the original, but sends
|
|
2608
|
+
* span data to Bitfab in the background after each call.
|
|
2609
|
+
*
|
|
2610
|
+
* Example usage:
|
|
2611
|
+
* ```typescript
|
|
2612
|
+
* const client = new Bitfab({ apiKey: 'your-api-key' });
|
|
2613
|
+
*
|
|
2614
|
+
* async function processOrder(orderId: string, items: string[]): Promise<{ total: number }> {
|
|
2615
|
+
* // ... process order
|
|
2616
|
+
* return { total: 100 };
|
|
2617
|
+
* }
|
|
2618
|
+
*
|
|
2619
|
+
* // Basic usage (defaults to "custom" span type)
|
|
2620
|
+
* const tracedProcessOrder = client.withSpan('order-processing', processOrder);
|
|
2621
|
+
*
|
|
2622
|
+
* // With explicit span type
|
|
2623
|
+
* const tracedProcessOrder = client.withSpan('order-processing', { type: 'function' }, processOrder);
|
|
2624
|
+
*
|
|
2625
|
+
* // Call the wrapped function normally
|
|
2626
|
+
* const result = await tracedProcessOrder('order-123', ['item-1', 'item-2']);
|
|
2627
|
+
* // Span is automatically sent to Bitfab
|
|
2628
|
+
* ```
|
|
2629
|
+
*
|
|
2630
|
+
* @param traceFunctionKey - A string identifier for grouping spans (e.g., 'order-processing', 'user-auth')
|
|
2631
|
+
* @param optionsOrFn - Either SpanOptions or the function to wrap
|
|
2632
|
+
* @param maybeFn - The function to wrap if options were provided
|
|
2633
|
+
* @returns A wrapped function with the same signature that creates spans for inputs and outputs
|
|
2634
|
+
*/
|
|
2635
|
+
withSpan(traceFunctionKey, optionsOrFn, maybeFn) {
|
|
2636
|
+
if (!this.enabled) {
|
|
2637
|
+
const fn2 = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
2638
|
+
return fn2;
|
|
2639
|
+
}
|
|
2640
|
+
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
2641
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
2642
|
+
const self = this;
|
|
2643
|
+
const fnIsAsyncFunction = fn.constructor.name === "AsyncFunction";
|
|
2644
|
+
const fnReturnsPromise = fnIsAsyncFunction || (() => {
|
|
2645
|
+
try {
|
|
2646
|
+
const src = fn.toString();
|
|
2647
|
+
return /\b(?:Promise|await)\b/.test(src);
|
|
2648
|
+
} catch {
|
|
2649
|
+
return false;
|
|
2650
|
+
}
|
|
2651
|
+
})();
|
|
2652
|
+
const wrappedFn = function(...args) {
|
|
2653
|
+
if (!asyncLocalStorage && !isAsyncStorageInitDone()) {
|
|
2654
|
+
return asyncLocalStorageReady.then(
|
|
2655
|
+
() => wrappedFn.apply(this, args)
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
const currentStack = getSpanStack();
|
|
2659
|
+
const parentContext = currentStack[currentStack.length - 1];
|
|
2660
|
+
const traceId = parentContext?.traceId ?? crypto.randomUUID();
|
|
2661
|
+
const spanId = crypto.randomUUID();
|
|
2662
|
+
const parentSpanId = parentContext?.spanId ?? null;
|
|
2663
|
+
const isRootSpan = parentSpanId === null;
|
|
2664
|
+
const newContext = { traceId, spanId, contexts: [] };
|
|
2665
|
+
const newStack = [...currentStack, newContext];
|
|
2666
|
+
const inputs = args;
|
|
2667
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2668
|
+
if (isRootSpan && !activeTraceStates.has(traceId)) {
|
|
2669
|
+
const replayCtxAtRoot = getReplayContext();
|
|
2670
|
+
activeTraceStates.set(traceId, {
|
|
2671
|
+
traceId,
|
|
2672
|
+
startedAt,
|
|
2673
|
+
contexts: [],
|
|
2674
|
+
...replayCtxAtRoot?.testRunId && {
|
|
2675
|
+
testRunId: replayCtxAtRoot.testRunId
|
|
2676
|
+
},
|
|
2677
|
+
...replayCtxAtRoot?.inputSourceTraceId && {
|
|
2678
|
+
inputSourceTraceId: replayCtxAtRoot.inputSourceTraceId
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
pendingSpanPromises.set(traceId, []);
|
|
2682
|
+
}
|
|
2683
|
+
const functionName = fn.name !== "" ? fn.name : void 0;
|
|
2684
|
+
const baseSpanParams = {
|
|
2685
|
+
traceFunctionKey,
|
|
2686
|
+
functionName,
|
|
2687
|
+
spanName: options.name ?? functionName ?? traceFunctionKey,
|
|
2688
|
+
traceId,
|
|
2689
|
+
spanId,
|
|
2690
|
+
parentSpanId,
|
|
2691
|
+
inputs,
|
|
2692
|
+
startedAt,
|
|
2693
|
+
spanType: options.type ?? "custom"
|
|
2694
|
+
};
|
|
2695
|
+
const sendSpan = async (params) => {
|
|
2696
|
+
try {
|
|
2697
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2698
|
+
const replayCtx = getReplayContext();
|
|
2699
|
+
const spanPromise = self.sendWrapperSpan({
|
|
2700
|
+
...baseSpanParams,
|
|
2701
|
+
...params,
|
|
2702
|
+
contexts: newContext.contexts,
|
|
2703
|
+
prompt: newContext.prompt,
|
|
2704
|
+
endedAt,
|
|
2705
|
+
...replayCtx?.testRunId && { testRunId: replayCtx.testRunId },
|
|
2706
|
+
...replayCtx?.inputSourceSpanId && {
|
|
2707
|
+
inputSourceSpanId: replayCtx.inputSourceSpanId
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
if (isRootSpan) {
|
|
2711
|
+
const pending = pendingSpanPromises.get(traceId) ?? [];
|
|
2712
|
+
pending.push(spanPromise);
|
|
2713
|
+
await Promise.race([
|
|
2714
|
+
Promise.allSettled(pending),
|
|
2715
|
+
new Promise((resolve) => setTimeout(resolve, 5e3))
|
|
2716
|
+
]);
|
|
2717
|
+
pendingSpanPromises.delete(traceId);
|
|
2718
|
+
const traceState = activeTraceStates.get(traceId);
|
|
2719
|
+
self.sendTraceCompletion({
|
|
2720
|
+
traceFunctionKey,
|
|
2721
|
+
traceId,
|
|
2722
|
+
startedAt: traceState?.startedAt ?? startedAt,
|
|
2723
|
+
endedAt,
|
|
2724
|
+
sessionId: traceState?.sessionId,
|
|
2725
|
+
metadata: traceState?.metadata,
|
|
2726
|
+
contexts: traceState?.contexts ?? [],
|
|
2727
|
+
testRunId: traceState?.testRunId,
|
|
2728
|
+
inputSourceTraceId: traceState?.inputSourceTraceId
|
|
2729
|
+
});
|
|
2730
|
+
activeTraceStates.delete(traceId);
|
|
2731
|
+
} else {
|
|
2732
|
+
const pending = pendingSpanPromises.get(traceId);
|
|
2733
|
+
if (pending) {
|
|
2734
|
+
pending.push(spanPromise);
|
|
2735
|
+
} else {
|
|
2736
|
+
pendingSpanPromises.set(traceId, [spanPromise]);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
} catch {
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
const replayCtxForMock = getReplayContext();
|
|
2743
|
+
if (replayCtxForMock?.mockTree && !isRootSpan) {
|
|
2744
|
+
const counters = replayCtxForMock.callCounters;
|
|
2745
|
+
const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
|
|
2746
|
+
const callIndex = counters.get(counterKey) ?? 0;
|
|
2747
|
+
counters.set(counterKey, callIndex + 1);
|
|
2748
|
+
const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
|
|
2749
|
+
if (shouldMock) {
|
|
2750
|
+
const mockKey = `${counterKey}:${callIndex}`;
|
|
2751
|
+
const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
|
|
2752
|
+
if (mockSpan) {
|
|
2753
|
+
let output = mockSpan.output;
|
|
2754
|
+
if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
|
|
2755
|
+
output = deserializeValue({
|
|
2756
|
+
json: mockSpan.output,
|
|
2757
|
+
meta: mockSpan.outputMeta
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
void sendSpan({ result: output });
|
|
2761
|
+
if (fnReturnsPromise) {
|
|
2762
|
+
return Promise.resolve(output);
|
|
2763
|
+
}
|
|
2764
|
+
return output;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const executeWithContext = () => {
|
|
2769
|
+
const result = fn(...args);
|
|
2770
|
+
if (result instanceof Promise) {
|
|
2771
|
+
return result.then((resolvedResult) => {
|
|
2772
|
+
void sendSpan({ result: resolvedResult });
|
|
2773
|
+
return resolvedResult;
|
|
2774
|
+
}).catch((error) => {
|
|
2775
|
+
void sendSpan({
|
|
2776
|
+
result: void 0,
|
|
2777
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2778
|
+
});
|
|
2779
|
+
throw error;
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
if (isAsyncGenerator(result)) {
|
|
2783
|
+
return wrapAsyncGenerator(result, newStack, sendSpan);
|
|
2784
|
+
}
|
|
2785
|
+
void sendSpan({ result });
|
|
2786
|
+
return result;
|
|
2787
|
+
};
|
|
2788
|
+
return runWithSpanStack(newStack, executeWithContext);
|
|
2789
|
+
};
|
|
2790
|
+
return wrappedFn;
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Get a detached handle to a previously-created trace, looked up by the
|
|
2794
|
+
* caller-supplied id (the same id passed at trace creation).
|
|
2795
|
+
*
|
|
2796
|
+
* The returned handle is not tied to AsyncLocalStorage — each method sends
|
|
2797
|
+
* to the server immediately. Useful for adding context to a trace from a
|
|
2798
|
+
* different process or thread than the one that created it.
|
|
2799
|
+
*
|
|
2800
|
+
* Throws synchronously if `traceId` is malformed (empty, too long, or
|
|
2801
|
+
* contains characters outside `[a-zA-Z0-9_\-.:]`). Server returns 404 if
|
|
2802
|
+
* no trace exists with that id in the org; the failure surfaces as a
|
|
2803
|
+
* logged warning (fire-and-forget) or via the awaited promise.
|
|
2804
|
+
*
|
|
2805
|
+
* Example:
|
|
2806
|
+
* ```typescript
|
|
2807
|
+
* const trace = client.getTrace("order_abc_123");
|
|
2808
|
+
* await trace.addContext({ refund_status: "approved" });
|
|
2809
|
+
* await trace.setMetadata({ region: "us-west" });
|
|
2810
|
+
* ```
|
|
2811
|
+
*/
|
|
2812
|
+
getTrace(traceId) {
|
|
2813
|
+
validateTraceId(traceId);
|
|
2814
|
+
return {
|
|
2815
|
+
traceId,
|
|
2816
|
+
addContext: (context) => {
|
|
2817
|
+
if (!this.enabled) {
|
|
2818
|
+
return Promise.resolve();
|
|
2819
|
+
}
|
|
2820
|
+
if (typeof context !== "object" || context === null) {
|
|
2821
|
+
return Promise.resolve();
|
|
2822
|
+
}
|
|
2823
|
+
return this.httpClient.patchTrace(traceId, {
|
|
2824
|
+
appendContexts: [context]
|
|
2825
|
+
});
|
|
2826
|
+
},
|
|
2827
|
+
setMetadata: (metadata) => {
|
|
2828
|
+
if (!this.enabled) {
|
|
2829
|
+
return Promise.resolve();
|
|
2830
|
+
}
|
|
2831
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
2832
|
+
return Promise.resolve();
|
|
2833
|
+
}
|
|
2834
|
+
return this.httpClient.patchTrace(traceId, { mergeMetadata: metadata });
|
|
2835
|
+
},
|
|
2836
|
+
setSessionId: (sessionId) => {
|
|
2837
|
+
if (!this.enabled) {
|
|
2838
|
+
return Promise.resolve();
|
|
2839
|
+
}
|
|
2840
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2841
|
+
return Promise.resolve();
|
|
2842
|
+
}
|
|
2843
|
+
return this.httpClient.patchTrace(traceId, { setSessionId: sessionId });
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Get a function wrapper for a specific trace function key.
|
|
2849
|
+
*
|
|
2850
|
+
* This provides a fluent API alternative to calling withSpan directly,
|
|
2851
|
+
* allowing you to bind the traceFunctionKey once and wrap multiple functions.
|
|
2852
|
+
*
|
|
2853
|
+
* Example usage:
|
|
2854
|
+
* ```typescript
|
|
2855
|
+
* const client = new Bitfab({ apiKey: 'your-api-key' });
|
|
2856
|
+
*
|
|
2857
|
+
* const orderFunc = client.getFunction('order-processing');
|
|
2858
|
+
* const tracedProcessOrder = orderFunc.withSpan(processOrder);
|
|
2859
|
+
* const tracedValidateOrder = orderFunc.withSpan(validateOrder);
|
|
2860
|
+
* ```
|
|
2861
|
+
*
|
|
2862
|
+
* @param traceFunctionKey - A string identifier for grouping spans
|
|
2863
|
+
* @returns A BitfabFunction instance for wrapping functions
|
|
2864
|
+
*/
|
|
2865
|
+
getFunction(traceFunctionKey) {
|
|
2866
|
+
return new BitfabFunction(this, traceFunctionKey);
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Send trace completion when a root span ends.
|
|
2870
|
+
* Internal method to record trace completion with end time.
|
|
2871
|
+
* Fire-and-forget - sends to externalTraces endpoint via httpClient.
|
|
2872
|
+
*/
|
|
2873
|
+
sendTraceCompletion(params) {
|
|
2874
|
+
const rawTrace = {
|
|
2875
|
+
id: params.traceId,
|
|
2876
|
+
started_at: params.startedAt,
|
|
2877
|
+
ended_at: params.endedAt,
|
|
2878
|
+
workflow_name: params.traceFunctionKey
|
|
2879
|
+
};
|
|
2880
|
+
if (params.metadata && Object.keys(params.metadata).length > 0) {
|
|
2881
|
+
rawTrace.metadata = params.metadata;
|
|
2882
|
+
}
|
|
2883
|
+
if (params.contexts && params.contexts.length > 0) {
|
|
2884
|
+
rawTrace.contexts = params.contexts;
|
|
2885
|
+
}
|
|
2886
|
+
if (params.inputSourceTraceId) {
|
|
2887
|
+
rawTrace.input_source_trace_id = params.inputSourceTraceId;
|
|
2888
|
+
}
|
|
2889
|
+
this.httpClient.sendExternalTrace({
|
|
2890
|
+
type: "sdk-function",
|
|
2891
|
+
source: "typescript-sdk-function",
|
|
2892
|
+
traceFunctionKey: params.traceFunctionKey,
|
|
2893
|
+
externalTrace: rawTrace,
|
|
2894
|
+
completed: true,
|
|
2895
|
+
...params.sessionId && { sessionId: params.sessionId },
|
|
2896
|
+
...params.testRunId && { testRunId: params.testRunId }
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Send a wrapper span from function execution.
|
|
2901
|
+
* Internal method to record spans when using withSpan.
|
|
2902
|
+
* Fire-and-forget - sends to externalSpans endpoint via httpClient.
|
|
2903
|
+
*/
|
|
2904
|
+
sendWrapperSpan(params) {
|
|
2905
|
+
const serializedInputs = serializeValue(params.inputs);
|
|
2906
|
+
const serializedResult = serializeValue(params.result);
|
|
2907
|
+
const externalSpan = {
|
|
2908
|
+
id: params.spanId,
|
|
2909
|
+
trace_id: params.traceId,
|
|
2910
|
+
started_at: params.startedAt,
|
|
2911
|
+
ended_at: params.endedAt,
|
|
2912
|
+
span_data: {
|
|
2913
|
+
name: params.spanName,
|
|
2914
|
+
type: params.spanType,
|
|
2915
|
+
input: serializedInputs.json,
|
|
2916
|
+
output: serializedResult.json,
|
|
2917
|
+
// Include superjson meta for type preservation
|
|
2918
|
+
...serializedInputs.meta !== void 0 && {
|
|
2919
|
+
input_meta: serializedInputs.meta
|
|
2920
|
+
},
|
|
2921
|
+
...serializedResult.meta !== void 0 && {
|
|
2922
|
+
output_meta: serializedResult.meta
|
|
2923
|
+
},
|
|
2924
|
+
...params.functionName !== void 0 && {
|
|
2925
|
+
function_name: params.functionName
|
|
2926
|
+
},
|
|
2927
|
+
...params.error !== void 0 && { error: params.error },
|
|
2928
|
+
...params.contexts && params.contexts.length > 0 && {
|
|
2929
|
+
contexts: params.contexts
|
|
2930
|
+
},
|
|
2931
|
+
...params.prompt !== void 0 && { prompt: params.prompt }
|
|
2932
|
+
}
|
|
2933
|
+
};
|
|
2934
|
+
if (params.parentSpanId) {
|
|
2935
|
+
externalSpan.parent_id = params.parentSpanId;
|
|
2936
|
+
}
|
|
2937
|
+
if (params.inputSourceSpanId) {
|
|
2938
|
+
externalSpan.input_source_span_id = params.inputSourceSpanId;
|
|
2939
|
+
}
|
|
2940
|
+
return this.httpClient.sendExternalSpan({
|
|
2941
|
+
type: "sdk-function",
|
|
2942
|
+
source: "typescript-sdk-function",
|
|
2943
|
+
sourceTraceId: params.traceId,
|
|
2944
|
+
traceFunctionKey: params.traceFunctionKey,
|
|
2945
|
+
rawSpan: externalSpan,
|
|
2946
|
+
...params.testRunId && { testRunId: params.testRunId }
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Replay historical traces through a function and create a test run.
|
|
2951
|
+
*
|
|
2952
|
+
* Fetches the last N traces for the given trace function key, re-runs each
|
|
2953
|
+
* through the provided function, and returns comparison data.
|
|
2954
|
+
*
|
|
2955
|
+
* The function must have been wrapped with `withSpan` — replay injects
|
|
2956
|
+
* `testRunId` via async context so new spans are linked to the test run.
|
|
2957
|
+
*
|
|
2958
|
+
* @param traceFunctionKey - The trace function key to replay
|
|
2959
|
+
* @param fn - The function to replay (must be the return value of `withSpan`)
|
|
2960
|
+
* @param options - Optional replay options (limit, traceIds)
|
|
2961
|
+
* @returns ReplayResult with items, testRunId, and testRunUrl
|
|
2962
|
+
*/
|
|
2963
|
+
async replay(traceFunctionKey, fn, options) {
|
|
2964
|
+
const { replay: doReplay } = await Promise.resolve().then(() => (init_replay(), replay_exports));
|
|
2965
|
+
return doReplay(
|
|
2966
|
+
this.httpClient,
|
|
2967
|
+
this.serviceUrl,
|
|
2968
|
+
traceFunctionKey,
|
|
2969
|
+
fn,
|
|
2970
|
+
options
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
var BitfabFunction = class {
|
|
2975
|
+
constructor(client, traceFunctionKey) {
|
|
2976
|
+
this.client = client;
|
|
2977
|
+
this.traceFunctionKey = traceFunctionKey;
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Wrap a function to automatically create a span for its inputs and outputs.
|
|
2981
|
+
*
|
|
2982
|
+
* The wrapped function behaves identically to the original, but sends
|
|
2983
|
+
* span data to Bitfab in the background after each call.
|
|
2984
|
+
*
|
|
2985
|
+
* Example usage:
|
|
2986
|
+
* ```typescript
|
|
2987
|
+
* const orderFunc = client.getFunction('order-processing');
|
|
2988
|
+
*
|
|
2989
|
+
* // Basic usage (defaults to "custom" span type)
|
|
2990
|
+
* const tracedProcessOrder = orderFunc.withSpan(processOrder);
|
|
2991
|
+
*
|
|
2992
|
+
* // With explicit span type
|
|
2993
|
+
* const tracedProcessOrder = orderFunc.withSpan({ type: 'function' }, processOrder);
|
|
2994
|
+
* ```
|
|
2995
|
+
*
|
|
2996
|
+
* @param optionsOrFn - Either SpanOptions or the function to wrap
|
|
2997
|
+
* @param maybeFn - The function to wrap if options were provided
|
|
2998
|
+
* @returns A wrapped function with the same signature that creates spans
|
|
2999
|
+
*/
|
|
3000
|
+
withSpan(optionsOrFn, maybeFn) {
|
|
3001
|
+
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
3002
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
3003
|
+
return this.client.withSpan(this.traceFunctionKey, options, fn);
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Wrap a BAML client method to automatically capture prompt and LLM metadata.
|
|
3007
|
+
* Delegates to the parent client's wrapBAML method.
|
|
3008
|
+
*
|
|
3009
|
+
* @param methodOrClient - Either a BAML method (uses constructor bamlClient) or the BAML client instance
|
|
3010
|
+
* @param maybeMethodOrOptions - The BAML method when the first argument is a client, or WrapBAMLOptions when the first argument is the method
|
|
3011
|
+
* @param maybeOptions - WrapBAMLOptions when using the two-argument (client, method) form
|
|
3012
|
+
* @returns An async function with the same signature that instruments the BAML call
|
|
3013
|
+
*/
|
|
3014
|
+
wrapBAML(methodOrClient, maybeMethodOrOptions, maybeOptions) {
|
|
3015
|
+
return this.client.wrapBAML(
|
|
3016
|
+
methodOrClient,
|
|
3017
|
+
maybeMethodOrOptions,
|
|
3018
|
+
maybeOptions
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
|
|
3023
|
+
// src/index.ts
|
|
3024
|
+
init_constants();
|
|
3025
|
+
init_http();
|
|
3026
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3027
|
+
0 && (module.exports = {
|
|
3028
|
+
Bitfab,
|
|
3029
|
+
BitfabClaudeAgentHandler,
|
|
3030
|
+
BitfabError,
|
|
3031
|
+
BitfabFunction,
|
|
3032
|
+
BitfabLangGraphCallbackHandler,
|
|
3033
|
+
BitfabOpenAITracingProcessor,
|
|
3034
|
+
DEFAULT_SERVICE_URL,
|
|
3035
|
+
__version__,
|
|
3036
|
+
flushTraces,
|
|
3037
|
+
getCurrentSpan,
|
|
3038
|
+
getCurrentTrace
|
|
3039
|
+
});
|
|
3040
|
+
//# sourceMappingURL=index.cjs.map
|