@bitfab/sdk 0.33.6 → 0.34.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.
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  BitfabError,
3
+ DEFAULT_SERVICE_URL,
4
+ HttpClient,
3
5
  __privateAdd,
4
6
  __privateGet,
5
7
  __privateSet,
@@ -14,513 +16,7 @@ import {
14
16
  toJsonSafe,
15
17
  toJsonSafeReport,
16
18
  warnOnce
17
- } from "./chunk-DPV6PBWE.js";
18
-
19
- // src/version.generated.ts
20
- var __version__ = "0.33.6";
21
-
22
- // src/constants.ts
23
- var DEFAULT_SERVICE_URL = "https://bitfab.ai";
24
-
25
- // src/unrefTimer.ts
26
- function unrefTimer(timer) {
27
- const handle = timer;
28
- if (typeof handle.unref === "function") {
29
- handle.unref();
30
- }
31
- }
32
-
33
- // src/http.ts
34
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
35
- function serializePayloadBody(payload) {
36
- try {
37
- return { body: JSON.stringify(payload), dropped: [] };
38
- } catch {
39
- const dropped = [];
40
- const sanitize = (value, seen) => {
41
- const t = typeof value;
42
- if (value === null || t === "string" || t === "number" || t === "boolean") {
43
- return value;
44
- }
45
- if (t === "bigint") {
46
- dropped.push("BigInt");
47
- return "<unserializable: BigInt>";
48
- }
49
- if (t === "function") {
50
- const name = value.name || "Function";
51
- dropped.push(name);
52
- return `<unserializable: ${name}>`;
53
- }
54
- if (t === "symbol") {
55
- dropped.push("Symbol");
56
- return "<unserializable: Symbol>";
57
- }
58
- if (t !== "object") {
59
- return void 0;
60
- }
61
- const obj = value;
62
- const className = obj.constructor?.name || "object";
63
- if (seen.has(obj)) {
64
- dropped.push(className);
65
- return `<cycle: ${className}>`;
66
- }
67
- seen.add(obj);
68
- let result;
69
- if (Array.isArray(obj)) {
70
- result = obj.map((item) => sanitize(item, seen));
71
- } else if (typeof obj.toJSON === "function") {
72
- try {
73
- result = sanitize(obj.toJSON(), seen);
74
- } catch {
75
- dropped.push(className);
76
- result = `<unserializable: ${className}>`;
77
- }
78
- } else {
79
- try {
80
- const out = {};
81
- for (const [k, v] of Object.entries(obj)) {
82
- out[k] = sanitize(v, seen);
83
- }
84
- result = out;
85
- } catch {
86
- warnOnce(
87
- "payload:field-getter-threw",
88
- "a value with a throwing getter/proxy could not be serialized into a span payload; it was replaced with a placeholder. The span still ships with its other fields intact."
89
- );
90
- dropped.push(className);
91
- result = `<unserializable: ${className}>`;
92
- }
93
- }
94
- seen.delete(obj);
95
- return result;
96
- };
97
- let sanitized;
98
- try {
99
- sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
100
- } catch (error) {
101
- const message = error instanceof Error ? error.message : String(error);
102
- return {
103
- body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
104
- dropped
105
- };
106
- }
107
- if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
108
- const obj = sanitized;
109
- const existing = Array.isArray(obj.errors) ? obj.errors : [];
110
- obj.errors = [
111
- ...existing,
112
- {
113
- source: "sdk",
114
- step: "json_serialize",
115
- error: `stubbed non-serializable value(s): ${[
116
- ...new Set(dropped)
117
- ].join(", ")}`
118
- }
119
- ];
120
- }
121
- return { body: JSON.stringify(sanitized), dropped };
122
- }
123
- }
124
- var pendingTracePromises = /* @__PURE__ */ new Set();
125
- function awaitOnExit(promise) {
126
- pendingTracePromises.add(promise);
127
- void promise.finally(() => {
128
- pendingTracePromises.delete(promise);
129
- }).catch(() => {
130
- });
131
- return promise;
132
- }
133
- async function flushTraces(timeoutMs = 5e3) {
134
- if (pendingTracePromises.size === 0) {
135
- return;
136
- }
137
- let timer;
138
- try {
139
- await Promise.race([
140
- Promise.allSettled(Array.from(pendingTracePromises)),
141
- new Promise((resolve) => {
142
- timer = setTimeout(resolve, timeoutMs);
143
- unrefTimer(timer);
144
- })
145
- ]);
146
- } finally {
147
- if (timer) {
148
- clearTimeout(timer);
149
- }
150
- }
151
- }
152
- if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
153
- let isFlushing = false;
154
- process.on("beforeExit", () => {
155
- if (pendingTracePromises.size > 0 && !isFlushing) {
156
- isFlushing = true;
157
- Promise.allSettled(
158
- Array.from(pendingTracePromises).map(
159
- (p) => p.catch(() => {
160
- })
161
- )
162
- ).then(() => {
163
- isFlushing = false;
164
- }).catch(() => {
165
- isFlushing = false;
166
- });
167
- }
168
- });
169
- }
170
- var HttpClient = class {
171
- constructor(config) {
172
- this.apiKey = config.apiKey;
173
- this.serviceUrl = config.serviceUrl;
174
- this.timeout = config.timeout ?? 12e4;
175
- }
176
- /**
177
- * Resolve the API key at the moment it is needed (request time), invoking
178
- * the function form if one was supplied. Never read at construction.
179
- */
180
- resolveApiKey() {
181
- return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
182
- }
183
- /**
184
- * Make an HTTP request to the Bitfab API. Defaults to POST; pass
185
- * `options.method` to use a different verb (e.g. "PATCH").
186
- *
187
- * @param endpoint - The API endpoint (without base URL)
188
- * @param payload - The request body
189
- * @param options - Optional request options
190
- * @returns The parsed JSON response
191
- * @throws {BitfabError} If the request fails
192
- */
193
- async request(endpoint, payload, options) {
194
- const url = `${this.serviceUrl}${endpoint}`;
195
- const timeout = options?.timeout ?? this.timeout;
196
- const method = options?.method ?? "POST";
197
- const controller = new AbortController();
198
- const timeoutId = setTimeout(() => controller.abort(), timeout);
199
- const { body, dropped } = serializePayloadBody(payload);
200
- if (dropped.length > 0) {
201
- try {
202
- console.warn(
203
- `Bitfab: request body to ${endpoint} held ${dropped.length} non-serializable value(s) (${[...new Set(dropped)].join(", ")}); they were stubbed so the span still sends, but the trace may be incomplete or not replayable. Capture a JSON-safe projection of this input to make it replayable.`
204
- );
205
- } catch {
206
- }
207
- }
208
- try {
209
- const response = await fetch(url, {
210
- method,
211
- headers: {
212
- "Content-Type": "application/json",
213
- Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
214
- },
215
- body,
216
- signal: controller.signal
217
- });
218
- if (!response.ok) {
219
- const errorText = await response.text();
220
- throw new BitfabError(
221
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
222
- );
223
- }
224
- const result = await response.json();
225
- if (result.error) {
226
- if (result.url) {
227
- throw new BitfabError(
228
- `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
229
- result.url
230
- );
231
- }
232
- throw new BitfabError(result.error);
233
- }
234
- return result;
235
- } catch (error) {
236
- if (error instanceof BitfabError) {
237
- throw error;
238
- }
239
- if (error instanceof Error) {
240
- if (error.name === "AbortError") {
241
- throw new BitfabError(`Request timed out after ${timeout}ms`);
242
- }
243
- throw new BitfabError(error.message);
244
- }
245
- throw new BitfabError("Unknown error occurred");
246
- } finally {
247
- clearTimeout(timeoutId);
248
- }
249
- }
250
- /**
251
- * Look up a function by name.
252
- * Blocks until complete - needed for function execution.
253
- */
254
- async lookupFunction(name) {
255
- return this.request("/api/sdk/functions/lookup", { name });
256
- }
257
- async getTraceSpan(traceId, lookup) {
258
- const searchParams = new URLSearchParams();
259
- if (lookup.id !== void 0) {
260
- searchParams.set("id", lookup.id);
261
- } else {
262
- searchParams.set("name", lookup.name);
263
- searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
264
- }
265
- const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
266
- const response = await this.get(endpoint);
267
- return response.span;
268
- }
269
- async get(endpoint) {
270
- const url = `${this.serviceUrl}${endpoint}`;
271
- const controller = new AbortController();
272
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
273
- try {
274
- const response = await fetch(url, {
275
- method: "GET",
276
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
277
- signal: controller.signal
278
- });
279
- if (!response.ok) {
280
- const errorText = await response.text();
281
- throw new BitfabError(
282
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
283
- );
284
- }
285
- return await response.json();
286
- } catch (error) {
287
- if (error instanceof BitfabError) {
288
- throw error;
289
- }
290
- if (error instanceof Error) {
291
- if (error.name === "AbortError") {
292
- throw new BitfabError(`Request timed out after ${this.timeout}ms`);
293
- }
294
- throw new BitfabError(error.message);
295
- }
296
- throw new BitfabError("Unknown error occurred");
297
- } finally {
298
- clearTimeout(timeoutId);
299
- }
300
- }
301
- /**
302
- * Send an internal trace (from BAML execution).
303
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
304
- */
305
- sendInternalTrace(functionId, payload) {
306
- void awaitOnExit(
307
- this.request(`/api/sdk/functions/${functionId}/traces`, {
308
- ...payload,
309
- sdkVersion: __version__
310
- })
311
- ).catch((error) => {
312
- try {
313
- console.error("Bitfab: Failed to create trace:", error);
314
- } catch {
315
- }
316
- });
317
- }
318
- /**
319
- * Send an external span (from withSpan wrapper or OpenAI tracing).
320
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
321
- * Returns the tracked promise so callers can optionally await it.
322
- */
323
- sendExternalSpan(payload) {
324
- return awaitOnExit(
325
- this.request("/api/sdk/externalSpans", {
326
- ...payload,
327
- sdkVersion: __version__
328
- })
329
- ).catch((error) => {
330
- try {
331
- console.error("Bitfab: Failed to create external span:", error);
332
- } catch {
333
- }
334
- });
335
- }
336
- /**
337
- * Send an external trace (from OpenAI tracing).
338
- * Fire-and-forget with awaitOnExit - doesn't block the caller.
339
- * Returns the tracked promise so callers can optionally await it
340
- * (the replay path does, so trace completions are persisted before
341
- * `completeReplay` builds the trace-ID mapping).
342
- */
343
- sendExternalTrace(payload) {
344
- return awaitOnExit(
345
- this.request("/api/sdk/externalTraces", {
346
- ...payload,
347
- sdkVersion: __version__
348
- })
349
- ).catch((error) => {
350
- try {
351
- console.error("Bitfab: Failed to create external trace:", error);
352
- } catch {
353
- }
354
- });
355
- }
356
- /**
357
- * Partial update of an existing trace identified by its Bitfab trace ID.
358
- * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
359
- * returns a tracked promise that callers may optionally await.
360
- */
361
- patchTrace(traceId, payload) {
362
- const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
363
- return awaitOnExit(
364
- this.request(endpoint, payload, { method: "PATCH" })
365
- ).catch((error) => {
366
- try {
367
- console.error("Bitfab: Failed to patch trace:", error);
368
- } catch {
369
- }
370
- });
371
- }
372
- /**
373
- * Start a replay session by fetching historical traces.
374
- * Blocking call - creates a test run and returns lightweight item references.
375
- */
376
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
377
- const payload = { traceFunctionKey };
378
- if (limit !== void 0) {
379
- payload.limit = limit;
380
- }
381
- if (traceIds) {
382
- payload.traceIds = traceIds;
383
- }
384
- if (name !== void 0) {
385
- payload.name = name;
386
- }
387
- if (codeChangeDescription !== void 0) {
388
- payload.codeChangeDescription = codeChangeDescription;
389
- }
390
- if (codeChangeFiles !== void 0) {
391
- payload.codeChangeFiles = codeChangeFiles;
392
- }
393
- if (includeDbBranchLease) {
394
- payload.includeDbBranchLease = true;
395
- payload.lazyDbBranchLease = true;
396
- }
397
- if (experimentGroupId !== void 0) {
398
- payload.experimentGroupId = experimentGroupId;
399
- }
400
- if (datasetId !== void 0) {
401
- payload.datasetId = datasetId;
402
- }
403
- if (graderIds !== void 0) {
404
- payload.graderIds = graderIds;
405
- }
406
- if (dbBranchSettings !== void 0) {
407
- payload.dbBranchSettings = dbBranchSettings;
408
- }
409
- const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
410
- return this.request("/api/sdk/replay/start", payload, {
411
- timeout
412
- });
413
- }
414
- /**
415
- * Fetch an external span by ID.
416
- * Blocking GET request.
417
- */
418
- async getExternalSpan(spanId) {
419
- const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
420
- const controller = new AbortController();
421
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
422
- try {
423
- const response = await fetch(url, {
424
- method: "GET",
425
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
426
- signal: controller.signal
427
- });
428
- if (!response.ok) {
429
- const errorText = await response.text();
430
- throw new BitfabError(
431
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
432
- );
433
- }
434
- return await response.json();
435
- } catch (error) {
436
- if (error instanceof BitfabError) {
437
- throw error;
438
- }
439
- if (error instanceof Error) {
440
- if (error.name === "AbortError") {
441
- throw new BitfabError("Request timed out after 30000ms");
442
- }
443
- throw new BitfabError(error.message);
444
- }
445
- throw new BitfabError("Unknown error occurred");
446
- } finally {
447
- clearTimeout(timeoutId);
448
- }
449
- }
450
- /**
451
- * Fetch the span tree for a root span.
452
- * Blocking GET request.
453
- *
454
- * Pass `includeOutputs: false` for a payload-free tree (structure +
455
- * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
456
- * span instead of all up front. Omit it (default eager) for `mock: "all"`.
457
- */
458
- async getSpanTree(externalSpanId, options) {
459
- const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
460
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
461
- const controller = new AbortController();
462
- const timeoutId = setTimeout(() => controller.abort(), 3e4);
463
- try {
464
- const response = await fetch(url, {
465
- method: "GET",
466
- headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
467
- signal: controller.signal
468
- });
469
- if (!response.ok) {
470
- const errorText = await response.text();
471
- throw new BitfabError(
472
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
473
- );
474
- }
475
- return await response.json();
476
- } catch (error) {
477
- if (error instanceof BitfabError) {
478
- throw error;
479
- }
480
- if (error instanceof Error) {
481
- if (error.name === "AbortError") {
482
- throw new BitfabError("Request timed out after 30000ms");
483
- }
484
- throw new BitfabError(error.message);
485
- }
486
- throw new BitfabError("Unknown error occurred");
487
- } finally {
488
- clearTimeout(timeoutId);
489
- }
490
- }
491
- /**
492
- * Mark a replay test run as completed.
493
- * Blocking call.
494
- */
495
- async completeReplay(testRunId) {
496
- return this.request(
497
- "/api/sdk/replay/complete",
498
- { testRunId },
499
- { timeout: 3e4 }
500
- );
501
- }
502
- /**
503
- * Ask the server to materialize a per-trace DB branch lease from a
504
- * captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
505
- * snapshot + preview branch and polls operations to readiness, which
506
- * can take seconds.
507
- */
508
- async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
509
- return this.request(
510
- "/api/sdk/replay/resolveDbBranchLease",
511
- { testRunId, traceId, dbBranchSettings },
512
- { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
513
- );
514
- }
515
- /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
516
- async releaseDbBranchLease(neonBranchId) {
517
- await this.request(
518
- "/api/sdk/replay/releaseDbBranchLease",
519
- { neonBranchId },
520
- { timeout: 3e4 }
521
- );
522
- }
523
- };
19
+ } from "./chunk-4J36FZ4K.js";
524
20
 
525
21
  // src/processorPayload.ts
526
22
  var SERIALIZATION_DEGRADED_STEP = "serialization_degraded";
@@ -641,7 +137,8 @@ var BitfabClaudeAgentHandler = class {
641
137
  // its root. The prompt is not present anywhere in the message stream, so it
642
138
  // must be handed in explicitly.
643
139
  this.hasRootInput = false;
644
- this.httpClient = new HttpClient({
140
+ this.ownsHttpClient = config._httpClient === void 0;
141
+ this.httpClient = config._httpClient ?? new HttpClient({
645
142
  apiKey: config.apiKey,
646
143
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
647
144
  timeout: config.timeout ?? 1e4
@@ -654,6 +151,14 @@ var BitfabClaudeAgentHandler = class {
654
151
  this.subagentStartHook = this.subagentStartHook.bind(this);
655
152
  this.subagentStopHook = this.subagentStopHook.bind(this);
656
153
  }
154
+ /**
155
+ * Flush and release the span transport this handler started. A no-op when
156
+ * the handler borrowed a `Bitfab` client's HTTP client: that client's
157
+ * `close()` owns the worker's lifetime.
158
+ */
159
+ async close(timeoutMs) {
160
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
161
+ }
657
162
  // ── trace lifecycle ──────────────────────────────────────────
658
163
  ensureTrace() {
659
164
  if (this.traceId !== null) {
@@ -1659,7 +1164,8 @@ var BitfabLangGraphCallbackHandler = class {
1659
1164
  this.ignoreCustomEvent = true;
1660
1165
  this.runToSpan = /* @__PURE__ */ new Map();
1661
1166
  this.invocations = /* @__PURE__ */ new Map();
1662
- this.httpClient = new HttpClient({
1167
+ this.ownsHttpClient = config._httpClient === void 0;
1168
+ this.httpClient = config._httpClient ?? new HttpClient({
1663
1169
  apiKey: config.apiKey,
1664
1170
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
1665
1171
  timeout: config.timeout ?? 1e4
@@ -1667,6 +1173,14 @@ var BitfabLangGraphCallbackHandler = class {
1667
1173
  this.traceFunctionKey = config.traceFunctionKey;
1668
1174
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
1669
1175
  }
1176
+ /**
1177
+ * Flush and release the span transport this handler started. A no-op when
1178
+ * the handler borrowed a `Bitfab` client's HTTP client: that client's
1179
+ * `close()` owns the worker's lifetime.
1180
+ */
1181
+ async close(timeoutMs) {
1182
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
1183
+ }
1670
1184
  // ── lifecycle helpers ──────────────────────────────────────────
1671
1185
  startSpan(runId, parentRunId, name, spanType, inputData, metadata, tags) {
1672
1186
  const parentSpan = parentRunId ? this.runToSpan.get(parentRunId) : void 0;
@@ -1751,7 +1265,9 @@ var BitfabLangGraphCallbackHandler = class {
1751
1265
  if (extraContexts && Object.keys(extraContexts).length > 0) {
1752
1266
  spanInfo.contexts.push(extraContexts);
1753
1267
  }
1754
- this.sendSpan(spanInfo);
1268
+ if (spanInfo.hidden !== true) {
1269
+ this.sendSpan(spanInfo);
1270
+ }
1755
1271
  if (runId === spanInfo.rootRunId) {
1756
1272
  const invocation = this.invocations.get(runId);
1757
1273
  this.sendTraceCompletion(spanInfo, invocation?.activeContext ?? null);
@@ -1775,9 +1291,6 @@ var BitfabLangGraphCallbackHandler = class {
1775
1291
  if (spanInfo.contexts.length > 0) {
1776
1292
  spanData.contexts = spanInfo.contexts;
1777
1293
  }
1778
- if (spanInfo.hidden) {
1779
- spanData.hidden = true;
1780
- }
1781
1294
  const rawSpan = {
1782
1295
  id: spanInfo.spanId,
1783
1296
  trace_id: spanInfo.traceId,
@@ -2139,7 +1652,8 @@ var BitfabOpenAITracingProcessor = class {
2139
1652
  this.activeTraces = {};
2140
1653
  this.activeSpanMappings = {};
2141
1654
  this.canonicalTraceIds = {};
2142
- this.httpClient = new HttpClient({
1655
+ this.ownsHttpClient = config._httpClient === void 0;
1656
+ this.httpClient = config._httpClient ?? new HttpClient({
2143
1657
  apiKey: config.apiKey,
2144
1658
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
2145
1659
  timeout: config.timeout ?? 1e4
@@ -2155,6 +1669,14 @@ var BitfabOpenAITracingProcessor = class {
2155
1669
  this.canonicalTraceIds[sourceTraceId] = created;
2156
1670
  return created;
2157
1671
  }
1672
+ /**
1673
+ * Flush and release the span transport this processor started. A no-op when
1674
+ * the processor borrowed a `Bitfab` client's HTTP client: that client's
1675
+ * `close()` owns the worker's lifetime.
1676
+ */
1677
+ async close(timeoutMs) {
1678
+ return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
1679
+ }
2158
1680
  /**
2159
1681
  * Called when a trace is started.
2160
1682
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -2209,14 +1731,16 @@ var BitfabOpenAITracingProcessor = class {
2209
1731
  * Called when a trace is being flushed.
2210
1732
  */
2211
1733
  async forceFlush() {
1734
+ await this.httpClient.waitForPendingRequests();
2212
1735
  }
2213
1736
  /**
2214
1737
  * Called when the trace processor is shutting down.
2215
1738
  */
2216
- async shutdown(_timeout) {
1739
+ async shutdown(timeout) {
2217
1740
  this.activeTraces = {};
2218
1741
  this.activeSpanMappings = {};
2219
1742
  this.canonicalTraceIds = {};
1743
+ await this.close(timeout);
2220
1744
  }
2221
1745
  /**
2222
1746
  * Send trace to Bitfab API (fire-and-forget).
@@ -2482,7 +2006,6 @@ var BitfabVercelAiHandler = class {
2482
2006
 
2483
2007
  // src/client.ts
2484
2008
  var activeTraceStates = /* @__PURE__ */ new Map();
2485
- var pendingSpanPromises = /* @__PURE__ */ new Map();
2486
2009
  var asyncLocalStorage = null;
2487
2010
  var SPAN_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.spanContextStorage");
2488
2011
  var initializeAsyncContext = () => {
@@ -2838,6 +2361,23 @@ var Bitfab = class {
2838
2361
  timeout: this.timeout
2839
2362
  });
2840
2363
  }
2364
+ /**
2365
+ * Flush and permanently close this client's tracing resources: its pending
2366
+ * requests and the single span-transport worker shared by its decorators and
2367
+ * framework handlers.
2368
+ *
2369
+ * Resolves `false` when delivery failed or the deadline expired. Long-lived
2370
+ * processes never need this (the transport batches in the background and the
2371
+ * exit hook drains it); scripts and tests that want a hard guarantee should
2372
+ * await it.
2373
+ *
2374
+ * Deliberately not a `Symbol.asyncDispose` method: the SDK targets runtimes
2375
+ * where that symbol may be absent, and a computed key on a missing symbol
2376
+ * throws at class-definition time, taking the whole SDK down on load.
2377
+ */
2378
+ close(timeoutMs) {
2379
+ return this.httpClient.close(timeoutMs);
2380
+ }
2841
2381
  /**
2842
2382
  * Resolve the API key lazily, the first time a span actually needs it.
2843
2383
  *
@@ -2987,7 +2527,8 @@ var Bitfab = class {
2987
2527
  getActiveSpanContext: () => {
2988
2528
  const stack = getSpanStack();
2989
2529
  return stack[stack.length - 1] ?? null;
2990
- }
2530
+ },
2531
+ _httpClient: this.httpClient
2991
2532
  });
2992
2533
  }
2993
2534
  /**
@@ -3047,7 +2588,8 @@ var Bitfab = class {
3047
2588
  getActiveSpanContext: () => {
3048
2589
  const stack = getSpanStack();
3049
2590
  return stack[stack.length - 1] ?? null;
3050
- }
2591
+ },
2592
+ _httpClient: this.httpClient
3051
2593
  });
3052
2594
  }
3053
2595
  /**
@@ -3099,7 +2641,8 @@ var Bitfab = class {
3099
2641
  getActiveSpanContext: () => {
3100
2642
  const stack = getSpanStack();
3101
2643
  return stack[stack.length - 1] ?? null;
3102
- }
2644
+ },
2645
+ _httpClient: this.httpClient
3103
2646
  });
3104
2647
  }
3105
2648
  /**
@@ -3352,7 +2895,6 @@ var Bitfab = class {
3352
2895
  },
3353
2896
  dbSnapshotRef
3354
2897
  });
3355
- pendingSpanPromises.set(traceId, []);
3356
2898
  registeredTraceId = traceId;
3357
2899
  }
3358
2900
  const functionName = fn.name !== "" ? fn.name : void 0;
@@ -3367,57 +2909,29 @@ var Bitfab = class {
3367
2909
  startedAt,
3368
2910
  spanType: options.type ?? "custom"
3369
2911
  };
3370
- const sendSpan = async (params, spanOpts) => {
2912
+ const sendSpan = async (params) => {
3371
2913
  const replayCtx = getReplayContext();
3372
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3373
- let resolvePersistence;
3374
- if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
3375
- persistenceCollector.push(
3376
- new Promise((resolve) => {
3377
- resolvePersistence = resolve;
3378
- })
3379
- );
3380
- }
3381
2914
  try {
3382
2915
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
3383
2916
  const traceDropped = activeTraceStates.get(traceId)?.dropped === true;
3384
- const spanPromise = traceDropped ? Promise.resolve() : self.sendWrapperSpan({
3385
- ...baseSpanParams,
3386
- ...params,
3387
- contexts: newContext.contexts,
3388
- prompt: newContext.prompt,
3389
- endedAt,
3390
- ...replayCtx?.testRunId && {
3391
- testRunId: replayCtx.testRunId
3392
- },
3393
- ...replayCtx?.inputSourceSpanId && {
3394
- inputSourceSpanId: replayCtx.inputSourceSpanId
3395
- }
3396
- });
3397
- if (isRootSpan) {
3398
- const pending = pendingSpanPromises.get(traceId) ?? [];
3399
- pending.push(spanPromise);
3400
- if (persistenceCollector) {
3401
- await Promise.allSettled(pending);
3402
- } else {
3403
- let raceTimer;
3404
- try {
3405
- await Promise.race([
3406
- Promise.allSettled(pending),
3407
- new Promise((resolve) => {
3408
- raceTimer = setTimeout(resolve, 5e3);
3409
- unrefTimer(raceTimer);
3410
- })
3411
- ]);
3412
- } finally {
3413
- if (raceTimer) {
3414
- clearTimeout(raceTimer);
3415
- }
2917
+ if (!traceDropped) {
2918
+ self.sendWrapperSpan({
2919
+ ...baseSpanParams,
2920
+ ...params,
2921
+ contexts: newContext.contexts,
2922
+ prompt: newContext.prompt,
2923
+ endedAt,
2924
+ ...replayCtx?.testRunId && {
2925
+ testRunId: replayCtx.testRunId
2926
+ },
2927
+ ...replayCtx?.inputSourceSpanId && {
2928
+ inputSourceSpanId: replayCtx.inputSourceSpanId
3416
2929
  }
3417
- }
3418
- pendingSpanPromises.delete(traceId);
2930
+ });
2931
+ }
2932
+ if (isRootSpan) {
3419
2933
  const traceState = activeTraceStates.get(traceId);
3420
- const completionPromise = self.sendTraceCompletion({
2934
+ self.sendTraceCompletion({
3421
2935
  traceFunctionKey,
3422
2936
  traceId,
3423
2937
  startedAt: traceState?.startedAt ?? startedAt,
@@ -3443,20 +2957,8 @@ var Bitfab = class {
3443
2957
  }
3444
2958
  });
3445
2959
  activeTraceStates.delete(traceId);
3446
- if (persistenceCollector) {
3447
- await completionPromise;
3448
- }
3449
- } else {
3450
- const pending = pendingSpanPromises.get(traceId);
3451
- if (pending) {
3452
- pending.push(spanPromise);
3453
- } else {
3454
- pendingSpanPromises.set(traceId, [spanPromise]);
3455
- }
3456
2960
  }
3457
2961
  } catch {
3458
- } finally {
3459
- resolvePersistence?.();
3460
2962
  }
3461
2963
  };
3462
2964
  const replayCtxForMock = getReplayContext();
@@ -3540,30 +3042,14 @@ var Bitfab = class {
3540
3042
  }
3541
3043
  const recordSpan = (result) => {
3542
3044
  if (options.finalize) {
3543
- const replayCtx = getReplayContext();
3544
- const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
3545
- let resolvePersistence;
3546
- if (persistenceCollector) {
3547
- persistenceCollector.push(
3548
- new Promise((resolve) => {
3549
- resolvePersistence = resolve;
3550
- })
3551
- );
3552
- }
3553
- void Promise.resolve().then(() => options.finalize(result)).then(
3554
- (output) => sendSpan(
3555
- { result: output },
3556
- { skipPersistenceRegistration: true }
3557
- )
3558
- ).catch(
3559
- (error) => sendSpan(
3560
- {
3045
+ void self.httpClient.trackDeferred(
3046
+ Promise.resolve().then(() => options.finalize(result)).then((output) => sendSpan({ result: output })).catch(
3047
+ (error) => sendSpan({
3561
3048
  result: void 0,
3562
3049
  error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
3563
- },
3564
- { skipPersistenceRegistration: true }
3050
+ })
3565
3051
  )
3566
- ).finally(() => resolvePersistence?.());
3052
+ );
3567
3053
  } else {
3568
3054
  void sendSpan({ result });
3569
3055
  }
@@ -3591,7 +3077,6 @@ var Bitfab = class {
3591
3077
  } catch (setupError) {
3592
3078
  if (registeredTraceId) {
3593
3079
  activeTraceStates.delete(registeredTraceId);
3594
- pendingSpanPromises.delete(registeredTraceId);
3595
3080
  }
3596
3081
  if (getReplayContext()) {
3597
3082
  throw setupError;
@@ -3714,7 +3199,7 @@ var Bitfab = class {
3714
3199
  /**
3715
3200
  * Send trace completion when a root span ends.
3716
3201
  * Internal method to record trace completion with end time.
3717
- * Fire-and-forget - sends to externalTraces endpoint via httpClient.
3202
+ * Queued on the client's span transport; delivery is the transport's job.
3718
3203
  */
3719
3204
  sendTraceCompletion(params) {
3720
3205
  const rawTrace = {
@@ -3750,7 +3235,7 @@ var Bitfab = class {
3750
3235
  accessed: params.dbSnapshotUsage.accessed
3751
3236
  };
3752
3237
  }
3753
- return this.httpClient.sendExternalTrace({
3238
+ this.httpClient.sendExternalTrace({
3754
3239
  id: params.traceId,
3755
3240
  type: "sdk-function",
3756
3241
  source: "typescript-sdk-function",
@@ -3765,7 +3250,7 @@ var Bitfab = class {
3765
3250
  /**
3766
3251
  * Send a wrapper span from function execution.
3767
3252
  * Internal method to record spans when using withSpan.
3768
- * Fire-and-forget - sends to externalSpans endpoint via httpClient.
3253
+ * Queued on the client's span transport; delivery is the transport's job.
3769
3254
  */
3770
3255
  sendWrapperSpan(params) {
3771
3256
  const serializedInputs = serializeValue(params.inputs);
@@ -3806,7 +3291,7 @@ var Bitfab = class {
3806
3291
  if (params.inputSourceSpanId) {
3807
3292
  externalSpan.input_source_span_id = params.inputSourceSpanId;
3808
3293
  }
3809
- return this.httpClient.sendExternalSpan({
3294
+ this.httpClient.sendExternalSpan({
3810
3295
  id: params.spanId,
3811
3296
  traceId: params.traceId,
3812
3297
  type: "sdk-function",
@@ -3840,7 +3325,7 @@ var Bitfab = class {
3840
3325
  `Function is wrapped with trace function key '${wrappedKey}' but replay was called with '${traceFunctionKey}'. Pass matching keys, or pass the unwrapped function to replay it under the explicit key.`
3841
3326
  );
3842
3327
  }
3843
- const { replay: doReplay } = await import("./replay-SRQI4QMY.js");
3328
+ const { replay: doReplay } = await import("./replay-QFNYP7EY.js");
3844
3329
  return doReplay(
3845
3330
  this.httpClient,
3846
3331
  this.serviceUrl,
@@ -4044,9 +3529,6 @@ var finalizers = {
4044
3529
  };
4045
3530
 
4046
3531
  export {
4047
- __version__,
4048
- DEFAULT_SERVICE_URL,
4049
- flushTraces,
4050
3532
  BitfabClaudeAgentHandler,
4051
3533
  SUPPORTED_PROVIDERS,
4052
3534
  BitfabLangGraphCallbackHandler,
@@ -4060,4 +3542,4 @@ export {
4060
3542
  BitfabFunction,
4061
3543
  finalizers
4062
3544
  };
4063
- //# sourceMappingURL=chunk-JANSX65W.js.map
3545
+ //# sourceMappingURL=chunk-ESKBRHVG.js.map