@bitfab/sdk 0.28.11 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -73,6 +73,76 @@ declare class BitfabError extends Error {
73
73
  constructor(message: string, url?: string | undefined);
74
74
  }
75
75
 
76
+ /**
77
+ * Selective mock overrides for replay.
78
+ *
79
+ * A mock override injects a custom value into a specific span (node) during
80
+ * replay: the matched span short-circuits its real execution and returns the
81
+ * value you supply, so downstream real code runs against the substituted
82
+ * output. This is a third mock mode alongside "run real code" and "replay
83
+ * recorded output" (see {@link MockStrategy}).
84
+ *
85
+ * The matcher and value are deliberately separate so matching stays cheap
86
+ * (structural metadata only, no output fetch) and the recorded output is
87
+ * fetched lazily - and only if the value function actually asks for it via
88
+ * {@link MockOverrideCtx.getOriginalOutput}.
89
+ */
90
+ /**
91
+ * Structural identity of a span during replay, passed to a {@link NodeMatcher}.
92
+ * Carries no output payload - matching must not depend on the recorded output,
93
+ * so the output fetch stays lazy and gated.
94
+ */
95
+ interface SpanNodeMeta {
96
+ /** The `withSpan` key of the executing span (its code-side identity). */
97
+ traceFunctionKey: string;
98
+ /** Resolved span name: `options.name ?? fn.name ?? traceFunctionKey`. */
99
+ spanName: string;
100
+ /** Span type, e.g. "llm", "agent", "tool", "custom". */
101
+ type: string;
102
+ /**
103
+ * The id of this span in the original (replayed) trace, when it exists in
104
+ * that trace's tree. Undefined when the live span has no recorded
105
+ * counterpart (e.g. a span the changed code newly introduced).
106
+ */
107
+ originalSpanId?: string;
108
+ }
109
+ /** Context passed to a {@link MockValue} function for a matched span. */
110
+ interface MockOverrideCtx {
111
+ /** The matched span's structural metadata. */
112
+ node: SpanNodeMeta;
113
+ /** The live replay args passed to the wrapped function this run. */
114
+ inputs: unknown[];
115
+ /**
116
+ * Lazily fetch this span's ORIGINAL recorded output (deserialized). The fetch
117
+ * happens only when called and is memoized per replay item, so a purely flat
118
+ * override that never calls it triggers zero output round trips. Rejects if
119
+ * the span has no recorded counterpart. Being async, it is only usable for
120
+ * spans wrapping async functions.
121
+ */
122
+ getOriginalOutput: () => Promise<unknown>;
123
+ }
124
+ /** Selects which spans an override applies to. Runs on structural metadata. */
125
+ type NodeMatcher = (node: SpanNodeMeta) => boolean;
126
+ /** The function form of {@link MockValue}, receiving the override context. */
127
+ type MockValueFn = (ctx: MockOverrideCtx) => unknown | Promise<unknown>;
128
+ /**
129
+ * The value injected for a matched span: either a flat value used as-is, or a
130
+ * function of the {@link MockOverrideCtx} that returns one (or a Promise of
131
+ * one). Full replacement - the value IS the span's output, no merge with the
132
+ * recorded output.
133
+ *
134
+ * The flat side is spelled out (rather than `unknown`) so an inline function
135
+ * still gets a typed `ctx`: `unknown | Fn` would collapse to `unknown` and drop
136
+ * the contextual type. To inject a value that is itself a function, wrap it:
137
+ * `value: () => theFunction`.
138
+ */
139
+ type MockValue = MockValueFn | string | number | boolean | bigint | symbol | object | null | undefined;
140
+ /** One (match, value) pair. */
141
+ interface MockOverride {
142
+ match: NodeMatcher;
143
+ value: MockValue;
144
+ }
145
+
76
146
  /**
77
147
  * HTTP client utilities for Bitfab API requests.
78
148
  *
@@ -654,6 +724,15 @@ interface ReplayOptions {
654
724
  * - "all": every child withSpan returns historical output
655
725
  */
656
726
  mock?: MockStrategy;
727
+ /**
728
+ * Selective mock overrides: inject custom values into specific spans during
729
+ * replay, so downstream real code runs against the substituted output. Each
730
+ * override is a `{ match, value }` pair; the first matcher that
731
+ * matches a span wins. These take precedence over any overrides registered on
732
+ * the client via `registerMockOverride`, and over the base `mock` strategy - a
733
+ * span no override matches falls back to that strategy. See {@link MockOverride}.
734
+ */
735
+ mockOverride?: MockOverride | MockOverride[];
657
736
  /**
658
737
  * Per-trace environment. When the source trace carries a DB branching
659
738
  * snapshot, the SDK populates `environment.databaseUrl` before invoking
@@ -1192,6 +1271,12 @@ declare class Bitfab {
1192
1271
  private readonly httpClient;
1193
1272
  private readonly bamlClient;
1194
1273
  private readonly dbSnapshot;
1274
+ /**
1275
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
1276
+ * to every `replay` on this client (after any per-call `mockOverride`). In
1277
+ * registration order; first matcher wins within this list.
1278
+ */
1279
+ private readonly mockOverrides;
1195
1280
  /**
1196
1281
  * Initialize the Bitfab client.
1197
1282
  *
@@ -1499,6 +1584,30 @@ declare class Bitfab {
1499
1584
  * determines how many traces replay.
1500
1585
  * @returns ReplayResult with items, testRunId, and testRunUrl
1501
1586
  */
1587
+ /**
1588
+ * Register a mock override applied to every subsequent `replay` on this
1589
+ * client, so downstream real code runs against a value you supply for the
1590
+ * matched span. Instance-scoped (no global state); call {@link clearMockOverrides}
1591
+ * to reset. Per-call `replay({ mockOverride })` overrides take precedence, and
1592
+ * both take precedence over the base `mock` strategy.
1593
+ *
1594
+ * ```ts
1595
+ * // Object form (value is a flat value here)
1596
+ * bitfab.registerMockOverride({
1597
+ * match: (node) => node.traceFunctionKey === "classify-intent",
1598
+ * value: { label: "refund" },
1599
+ * })
1600
+ * // Ordered form (equivalent); value may also be a function of the context
1601
+ * bitfab.registerMockOverride(
1602
+ * (node) => node.traceFunctionKey === "classify-intent",
1603
+ * ({ inputs }) => ({ label: "refund" }),
1604
+ * )
1605
+ * ```
1606
+ */
1607
+ registerMockOverride(override: MockOverride): void;
1608
+ registerMockOverride(match: NodeMatcher, value: MockValue): void;
1609
+ /** Remove all overrides registered via {@link registerMockOverride}. */
1610
+ clearMockOverrides(): void;
1502
1611
  replay<TReturn>(traceFunctionKey: string, fn: (...args: any[]) => TReturn | Promise<TReturn>, options?: ReplayOptions): Promise<ReplayResult<TReturn>>;
1503
1612
  }
1504
1613
  /**
@@ -1649,7 +1758,7 @@ declare class BitfabFunction {
1649
1758
  /**
1650
1759
  * SDK version from package.json (injected at build time)
1651
1760
  */
1652
- declare const __version__ = "0.28.11";
1761
+ declare const __version__ = "0.29.0";
1653
1762
 
1654
1763
  /**
1655
1764
  * Constants for the Bitfab SDK.
@@ -1717,4 +1826,4 @@ declare const finalizers: {
1717
1826
  readableStream: typeof readableStream;
1718
1827
  };
1719
1828
 
1720
- export { type ActiveSpanContext, type AdaptContext, type AdaptInputsFn, type AllowedEnvVars, BITFAB_PROGRESS_PREFIX, type BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, type BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler as BitfabLangChainCallbackHandler, BitfabLangGraphCallbackHandler, type BitfabLanguageModelMiddleware, BitfabOpenAIAgentHandler, BitfabOpenAITracingProcessor, BitfabVercelAiHandler, type CapturedSpan, type CodeChangeFile, type CurrentSpan, type CurrentTrace, DEFAULT_SERVICE_URL, type DbSnapshotConfig, type DbSnapshotProvider, type DbSnapshotRef, type DetachedTrace, type MockStrategy, type ProviderDefinition, ReplayEnvironment, type ReplayEnvironmentSnapshot, type ReplayItem, type ReplayOptions, type ReplayProgress, type ReplayResult, SUPPORTED_PROVIDERS, type SpanLookup, type SpanOccurrence, type SpanOptions, type SpanType, type TokenUsage, type TraceResponse, type TracingProcessor, type VercelCallParams, type VercelGenerateResult, type VercelStreamResult, type WrapBAMLOptions, type WrappedBamlFn, __version__, finalizers, flushTraces, getCurrentSpan, getCurrentTrace, reportReplayProgress };
1829
+ export { type ActiveSpanContext, type AdaptContext, type AdaptInputsFn, type AllowedEnvVars, BITFAB_PROGRESS_PREFIX, type BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, type BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler as BitfabLangChainCallbackHandler, BitfabLangGraphCallbackHandler, type BitfabLanguageModelMiddleware, BitfabOpenAIAgentHandler, BitfabOpenAITracingProcessor, BitfabVercelAiHandler, type CapturedSpan, type CodeChangeFile, type CurrentSpan, type CurrentTrace, DEFAULT_SERVICE_URL, type DbSnapshotConfig, type DbSnapshotProvider, type DbSnapshotRef, type DetachedTrace, type MockOverride, type MockOverrideCtx, type MockStrategy, type MockValue, type NodeMatcher, type ProviderDefinition, ReplayEnvironment, type ReplayEnvironmentSnapshot, type ReplayItem, type ReplayOptions, type ReplayProgress, type ReplayResult, SUPPORTED_PROVIDERS, type SpanLookup, type SpanNodeMeta, type SpanOccurrence, type SpanOptions, type SpanType, type TokenUsage, type TraceResponse, type TracingProcessor, type VercelCallParams, type VercelGenerateResult, type VercelStreamResult, type WrapBAMLOptions, type WrappedBamlFn, __version__, finalizers, flushTraces, getCurrentSpan, getCurrentTrace, reportReplayProgress };
package/dist/index.d.ts CHANGED
@@ -73,6 +73,76 @@ declare class BitfabError extends Error {
73
73
  constructor(message: string, url?: string | undefined);
74
74
  }
75
75
 
76
+ /**
77
+ * Selective mock overrides for replay.
78
+ *
79
+ * A mock override injects a custom value into a specific span (node) during
80
+ * replay: the matched span short-circuits its real execution and returns the
81
+ * value you supply, so downstream real code runs against the substituted
82
+ * output. This is a third mock mode alongside "run real code" and "replay
83
+ * recorded output" (see {@link MockStrategy}).
84
+ *
85
+ * The matcher and value are deliberately separate so matching stays cheap
86
+ * (structural metadata only, no output fetch) and the recorded output is
87
+ * fetched lazily - and only if the value function actually asks for it via
88
+ * {@link MockOverrideCtx.getOriginalOutput}.
89
+ */
90
+ /**
91
+ * Structural identity of a span during replay, passed to a {@link NodeMatcher}.
92
+ * Carries no output payload - matching must not depend on the recorded output,
93
+ * so the output fetch stays lazy and gated.
94
+ */
95
+ interface SpanNodeMeta {
96
+ /** The `withSpan` key of the executing span (its code-side identity). */
97
+ traceFunctionKey: string;
98
+ /** Resolved span name: `options.name ?? fn.name ?? traceFunctionKey`. */
99
+ spanName: string;
100
+ /** Span type, e.g. "llm", "agent", "tool", "custom". */
101
+ type: string;
102
+ /**
103
+ * The id of this span in the original (replayed) trace, when it exists in
104
+ * that trace's tree. Undefined when the live span has no recorded
105
+ * counterpart (e.g. a span the changed code newly introduced).
106
+ */
107
+ originalSpanId?: string;
108
+ }
109
+ /** Context passed to a {@link MockValue} function for a matched span. */
110
+ interface MockOverrideCtx {
111
+ /** The matched span's structural metadata. */
112
+ node: SpanNodeMeta;
113
+ /** The live replay args passed to the wrapped function this run. */
114
+ inputs: unknown[];
115
+ /**
116
+ * Lazily fetch this span's ORIGINAL recorded output (deserialized). The fetch
117
+ * happens only when called and is memoized per replay item, so a purely flat
118
+ * override that never calls it triggers zero output round trips. Rejects if
119
+ * the span has no recorded counterpart. Being async, it is only usable for
120
+ * spans wrapping async functions.
121
+ */
122
+ getOriginalOutput: () => Promise<unknown>;
123
+ }
124
+ /** Selects which spans an override applies to. Runs on structural metadata. */
125
+ type NodeMatcher = (node: SpanNodeMeta) => boolean;
126
+ /** The function form of {@link MockValue}, receiving the override context. */
127
+ type MockValueFn = (ctx: MockOverrideCtx) => unknown | Promise<unknown>;
128
+ /**
129
+ * The value injected for a matched span: either a flat value used as-is, or a
130
+ * function of the {@link MockOverrideCtx} that returns one (or a Promise of
131
+ * one). Full replacement - the value IS the span's output, no merge with the
132
+ * recorded output.
133
+ *
134
+ * The flat side is spelled out (rather than `unknown`) so an inline function
135
+ * still gets a typed `ctx`: `unknown | Fn` would collapse to `unknown` and drop
136
+ * the contextual type. To inject a value that is itself a function, wrap it:
137
+ * `value: () => theFunction`.
138
+ */
139
+ type MockValue = MockValueFn | string | number | boolean | bigint | symbol | object | null | undefined;
140
+ /** One (match, value) pair. */
141
+ interface MockOverride {
142
+ match: NodeMatcher;
143
+ value: MockValue;
144
+ }
145
+
76
146
  /**
77
147
  * HTTP client utilities for Bitfab API requests.
78
148
  *
@@ -654,6 +724,15 @@ interface ReplayOptions {
654
724
  * - "all": every child withSpan returns historical output
655
725
  */
656
726
  mock?: MockStrategy;
727
+ /**
728
+ * Selective mock overrides: inject custom values into specific spans during
729
+ * replay, so downstream real code runs against the substituted output. Each
730
+ * override is a `{ match, value }` pair; the first matcher that
731
+ * matches a span wins. These take precedence over any overrides registered on
732
+ * the client via `registerMockOverride`, and over the base `mock` strategy - a
733
+ * span no override matches falls back to that strategy. See {@link MockOverride}.
734
+ */
735
+ mockOverride?: MockOverride | MockOverride[];
657
736
  /**
658
737
  * Per-trace environment. When the source trace carries a DB branching
659
738
  * snapshot, the SDK populates `environment.databaseUrl` before invoking
@@ -1192,6 +1271,12 @@ declare class Bitfab {
1192
1271
  private readonly httpClient;
1193
1272
  private readonly bamlClient;
1194
1273
  private readonly dbSnapshot;
1274
+ /**
1275
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
1276
+ * to every `replay` on this client (after any per-call `mockOverride`). In
1277
+ * registration order; first matcher wins within this list.
1278
+ */
1279
+ private readonly mockOverrides;
1195
1280
  /**
1196
1281
  * Initialize the Bitfab client.
1197
1282
  *
@@ -1499,6 +1584,30 @@ declare class Bitfab {
1499
1584
  * determines how many traces replay.
1500
1585
  * @returns ReplayResult with items, testRunId, and testRunUrl
1501
1586
  */
1587
+ /**
1588
+ * Register a mock override applied to every subsequent `replay` on this
1589
+ * client, so downstream real code runs against a value you supply for the
1590
+ * matched span. Instance-scoped (no global state); call {@link clearMockOverrides}
1591
+ * to reset. Per-call `replay({ mockOverride })` overrides take precedence, and
1592
+ * both take precedence over the base `mock` strategy.
1593
+ *
1594
+ * ```ts
1595
+ * // Object form (value is a flat value here)
1596
+ * bitfab.registerMockOverride({
1597
+ * match: (node) => node.traceFunctionKey === "classify-intent",
1598
+ * value: { label: "refund" },
1599
+ * })
1600
+ * // Ordered form (equivalent); value may also be a function of the context
1601
+ * bitfab.registerMockOverride(
1602
+ * (node) => node.traceFunctionKey === "classify-intent",
1603
+ * ({ inputs }) => ({ label: "refund" }),
1604
+ * )
1605
+ * ```
1606
+ */
1607
+ registerMockOverride(override: MockOverride): void;
1608
+ registerMockOverride(match: NodeMatcher, value: MockValue): void;
1609
+ /** Remove all overrides registered via {@link registerMockOverride}. */
1610
+ clearMockOverrides(): void;
1502
1611
  replay<TReturn>(traceFunctionKey: string, fn: (...args: any[]) => TReturn | Promise<TReturn>, options?: ReplayOptions): Promise<ReplayResult<TReturn>>;
1503
1612
  }
1504
1613
  /**
@@ -1649,7 +1758,7 @@ declare class BitfabFunction {
1649
1758
  /**
1650
1759
  * SDK version from package.json (injected at build time)
1651
1760
  */
1652
- declare const __version__ = "0.28.11";
1761
+ declare const __version__ = "0.29.0";
1653
1762
 
1654
1763
  /**
1655
1764
  * Constants for the Bitfab SDK.
@@ -1717,4 +1826,4 @@ declare const finalizers: {
1717
1826
  readableStream: typeof readableStream;
1718
1827
  };
1719
1828
 
1720
- export { type ActiveSpanContext, type AdaptContext, type AdaptInputsFn, type AllowedEnvVars, BITFAB_PROGRESS_PREFIX, type BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, type BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler as BitfabLangChainCallbackHandler, BitfabLangGraphCallbackHandler, type BitfabLanguageModelMiddleware, BitfabOpenAIAgentHandler, BitfabOpenAITracingProcessor, BitfabVercelAiHandler, type CapturedSpan, type CodeChangeFile, type CurrentSpan, type CurrentTrace, DEFAULT_SERVICE_URL, type DbSnapshotConfig, type DbSnapshotProvider, type DbSnapshotRef, type DetachedTrace, type MockStrategy, type ProviderDefinition, ReplayEnvironment, type ReplayEnvironmentSnapshot, type ReplayItem, type ReplayOptions, type ReplayProgress, type ReplayResult, SUPPORTED_PROVIDERS, type SpanLookup, type SpanOccurrence, type SpanOptions, type SpanType, type TokenUsage, type TraceResponse, type TracingProcessor, type VercelCallParams, type VercelGenerateResult, type VercelStreamResult, type WrapBAMLOptions, type WrappedBamlFn, __version__, finalizers, flushTraces, getCurrentSpan, getCurrentTrace, reportReplayProgress };
1829
+ export { type ActiveSpanContext, type AdaptContext, type AdaptInputsFn, type AllowedEnvVars, BITFAB_PROGRESS_PREFIX, type BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, type BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler as BitfabLangChainCallbackHandler, BitfabLangGraphCallbackHandler, type BitfabLanguageModelMiddleware, BitfabOpenAIAgentHandler, BitfabOpenAITracingProcessor, BitfabVercelAiHandler, type CapturedSpan, type CodeChangeFile, type CurrentSpan, type CurrentTrace, DEFAULT_SERVICE_URL, type DbSnapshotConfig, type DbSnapshotProvider, type DbSnapshotRef, type DetachedTrace, type MockOverride, type MockOverrideCtx, type MockStrategy, type MockValue, type NodeMatcher, type ProviderDefinition, ReplayEnvironment, type ReplayEnvironmentSnapshot, type ReplayItem, type ReplayOptions, type ReplayProgress, type ReplayResult, SUPPORTED_PROVIDERS, type SpanLookup, type SpanNodeMeta, type SpanOccurrence, type SpanOptions, type SpanType, type TokenUsage, type TraceResponse, type TracingProcessor, type VercelCallParams, type VercelGenerateResult, type VercelStreamResult, type WrapBAMLOptions, type WrappedBamlFn, __version__, finalizers, flushTraces, getCurrentSpan, getCurrentTrace, reportReplayProgress };
package/dist/index.js CHANGED
@@ -14,12 +14,12 @@ import {
14
14
  flushTraces,
15
15
  getCurrentSpan,
16
16
  getCurrentTrace
17
- } from "./chunk-ZBWTCBVQ.js";
17
+ } from "./chunk-2M5AWVVQ.js";
18
18
  import {
19
19
  BITFAB_PROGRESS_PREFIX,
20
20
  BitfabError,
21
21
  reportReplayProgress
22
- } from "./chunk-5E4BUIYA.js";
22
+ } from "./chunk-V3XORTWI.js";
23
23
  export {
24
24
  BITFAB_PROGRESS_PREFIX,
25
25
  Bitfab,
package/dist/node.cjs CHANGED
@@ -289,6 +289,22 @@ var init_randomUuid = __esm({
289
289
  }
290
290
  });
291
291
 
292
+ // src/mockOverride.ts
293
+ function resolveMockValue(value, ctx) {
294
+ return typeof value === "function" ? value(ctx) : value;
295
+ }
296
+ function normalizeMockOverrides(mockOverride) {
297
+ if (mockOverride === void 0) {
298
+ return [];
299
+ }
300
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
301
+ }
302
+ var init_mockOverride = __esm({
303
+ "src/mockOverride.ts"() {
304
+ "use strict";
305
+ }
306
+ });
307
+
292
308
  // src/replayContext.ts
293
309
  function getReplayContext() {
294
310
  return replayContextStorage?.getStore() ?? null;
@@ -364,6 +380,7 @@ function buildMockTree(rootNode) {
364
380
  counters.set(counterKey, index + 1);
365
381
  spans.set(`${counterKey}:${index}`, {
366
382
  sourceSpanId: node.sourceSpanId,
383
+ externalSpanId: node.externalSpanId,
367
384
  output: node.output,
368
385
  outputMeta: node.outputMeta
369
386
  });
@@ -377,7 +394,7 @@ function buildMockTree(rootNode) {
377
394
  }
378
395
  return { spans };
379
396
  }
380
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
397
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, environment, adaptInputs) {
381
398
  const lease = environment ? serverItem.dbBranchLease : void 0;
382
399
  let inputs = [];
383
400
  let originalOutput;
@@ -396,28 +413,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
396
413
  sourceSpanId: serverItem.externalSpanId
397
414
  });
398
415
  }
416
+ const hasOverrides = resolvedOverrides.length > 0;
417
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
418
+ const includeOutputs = mockStrategy === "all";
399
419
  let mockTree;
400
- if (mockStrategy === "all" || mockStrategy === "marked") {
420
+ if (needTree) {
401
421
  try {
402
422
  const treeResponse = await httpClient.getSpanTree(
403
- serverItem.externalSpanId
423
+ serverItem.externalSpanId,
424
+ { includeOutputs }
404
425
  );
405
426
  if (treeResponse.root) {
406
427
  mockTree = buildMockTree(treeResponse.root);
407
- } else if (mockStrategy === "all") {
428
+ } else if (mockStrategy === "all" || hasOverrides) {
408
429
  throw new BitfabError(
409
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
430
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
410
431
  );
411
432
  } else {
412
433
  mockTree = void 0;
413
434
  }
414
435
  } catch (e) {
415
- if (mockStrategy === "all") {
436
+ if (mockStrategy === "all" || hasOverrides) {
416
437
  throw e;
417
438
  }
418
439
  mockTree = void 0;
419
440
  }
420
441
  }
442
+ const outputCache = /* @__PURE__ */ new Map();
443
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
444
+ let pending = outputCache.get(externalSpanId);
445
+ if (!pending) {
446
+ pending = httpClient.getExternalSpan(externalSpanId).then(
447
+ (s) => deserializeOutput(
448
+ s.rawData?.span_data ?? {}
449
+ )
450
+ );
451
+ outputCache.set(externalSpanId, pending);
452
+ }
453
+ return pending;
454
+ } : void 0;
421
455
  const maybePromise = runWithReplayContext(
422
456
  {
423
457
  testRunId,
@@ -428,6 +462,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
428
462
  mockTree,
429
463
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
430
464
  mockStrategy,
465
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
466
+ fetchSpanOutput,
431
467
  dbBranchLease: lease,
432
468
  pendingPersistence
433
469
  },
@@ -484,7 +520,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
484
520
  await Promise.all(workers);
485
521
  return results;
486
522
  }
487
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
523
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
488
524
  if (options?.traceIds !== void 0) {
489
525
  if (options.traceIds.length === 0) {
490
526
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -524,6 +560,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
524
560
  );
525
561
  const mockStrategy = options?.mock ?? "marked";
526
562
  const maxConcurrency = options?.maxConcurrency ?? 10;
563
+ const resolvedOverrides = [
564
+ ...normalizeMockOverrides(options?.mockOverride),
565
+ ...registeredOverrides
566
+ ];
527
567
  const tasks = serverItems.map(
528
568
  (serverItem) => () => processItem(
529
569
  httpClient,
@@ -531,6 +571,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
531
571
  fn,
532
572
  testRunId,
533
573
  mockStrategy,
574
+ resolvedOverrides,
534
575
  options?.environment,
535
576
  options?.adaptInputs
536
577
  )
@@ -657,6 +698,7 @@ var init_replay = __esm({
657
698
  "src/replay.ts"() {
658
699
  "use strict";
659
700
  init_errors();
701
+ init_mockOverride();
660
702
  init_randomUuid();
661
703
  init_replayContext();
662
704
  init_serialize();
@@ -697,7 +739,7 @@ registerAsyncLocalStorageClass(
697
739
  );
698
740
 
699
741
  // src/version.generated.ts
700
- var __version__ = "0.28.11";
742
+ var __version__ = "0.29.0";
701
743
 
702
744
  // src/constants.ts
703
745
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -1126,9 +1168,14 @@ var HttpClient = class {
1126
1168
  /**
1127
1169
  * Fetch the span tree for a root span.
1128
1170
  * Blocking GET request.
1171
+ *
1172
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1173
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1174
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1129
1175
  */
1130
- async getSpanTree(externalSpanId) {
1131
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1176
+ async getSpanTree(externalSpanId, options) {
1177
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1178
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1132
1179
  const controller = new AbortController();
1133
1180
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1134
1181
  try {
@@ -2714,6 +2761,9 @@ var BitfabLangGraphCallbackHandler = class {
2714
2761
  }
2715
2762
  };
2716
2763
 
2764
+ // src/client.ts
2765
+ init_mockOverride();
2766
+
2717
2767
  // src/openaiAgentSdk.ts
2718
2768
  var BitfabOpenAIAgentHandler = class {
2719
2769
  constructor(config) {
@@ -3529,6 +3579,12 @@ var Bitfab = class {
3529
3579
  constructor(config) {
3530
3580
  /** Gate the empty-key warning to fire at most once. */
3531
3581
  this.apiKeyWarned = false;
3582
+ /**
3583
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3584
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3585
+ * registration order; first matcher wins within this list.
3586
+ */
3587
+ this.mockOverrides = [];
3532
3588
  this.apiKeyConfig = config.apiKey;
3533
3589
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3534
3590
  this.timeout = config.timeout ?? 12e4;
@@ -4141,24 +4197,77 @@ var Bitfab = class {
4141
4197
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4142
4198
  const callIndex = counters.get(counterKey) ?? 0;
4143
4199
  counters.set(counterKey, callIndex + 1);
4144
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4145
- if (shouldMock) {
4146
- const mockKey = `${counterKey}:${callIndex}`;
4147
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4148
- if (mockSpan) {
4149
- let output = mockSpan.output;
4150
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4151
- output = deserializeValue({
4152
- json: mockSpan.output,
4153
- meta: mockSpan.outputMeta
4154
- });
4155
- }
4200
+ const mockKey = `${counterKey}:${callIndex}`;
4201
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4202
+ const emitMock = (output) => {
4203
+ void sendSpan({ result: output, mocked: true });
4204
+ if (fnReturnsPromise) {
4205
+ return Promise.resolve(output);
4206
+ }
4207
+ return output;
4208
+ };
4209
+ const emitMockAsync = (pending) => {
4210
+ if (!fnReturnsPromise) {
4211
+ throw new BitfabError(
4212
+ `Cannot mock synchronous span "${traceFunctionKey}" with an asynchronously-resolved value (lazy recorded-output fetch or an async value function). Make the wrapped function async, or use mock: "all" so recorded outputs are fetched eagerly.`
4213
+ );
4214
+ }
4215
+ return (async () => {
4216
+ const output = await pending;
4156
4217
  void sendSpan({ result: output, mocked: true });
4157
- if (fnReturnsPromise) {
4158
- return Promise.resolve(output);
4159
- }
4160
4218
  return output;
4219
+ })();
4220
+ };
4221
+ const resolveRecordedOutput = () => {
4222
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4223
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4224
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4161
4225
  }
4226
+ if (!mockSpan) {
4227
+ return Promise.reject(
4228
+ new BitfabError(
4229
+ `No recorded span to source output for "${traceFunctionKey}".`
4230
+ )
4231
+ );
4232
+ }
4233
+ let output = mockSpan.output;
4234
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4235
+ output = deserializeValue({
4236
+ json: mockSpan.output,
4237
+ meta: mockSpan.outputMeta
4238
+ });
4239
+ }
4240
+ return output;
4241
+ };
4242
+ if (replayCtxForMock.mockOverrides?.length) {
4243
+ const nodeMeta = {
4244
+ traceFunctionKey,
4245
+ spanName: baseSpanParams.spanName,
4246
+ type: options.type ?? "custom",
4247
+ originalSpanId: mockSpan?.sourceSpanId
4248
+ };
4249
+ const override = replayCtxForMock.mockOverrides.find(
4250
+ (o) => o.match(nodeMeta)
4251
+ );
4252
+ if (override) {
4253
+ const injected = resolveMockValue(override.value, {
4254
+ node: nodeMeta,
4255
+ inputs: args,
4256
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4257
+ });
4258
+ if (injected instanceof Promise) {
4259
+ return emitMockAsync(injected);
4260
+ }
4261
+ return emitMock(injected);
4262
+ }
4263
+ }
4264
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4265
+ if (shouldMock && mockSpan) {
4266
+ const recorded = resolveRecordedOutput();
4267
+ if (recorded instanceof Promise) {
4268
+ return emitMockAsync(recorded);
4269
+ }
4270
+ return emitMock(recorded);
4162
4271
  }
4163
4272
  }
4164
4273
  const recordSpan = (result) => {
@@ -4438,26 +4547,14 @@ var Bitfab = class {
4438
4547
  ...params.mocked && { mocked: true }
4439
4548
  });
4440
4549
  }
4441
- /**
4442
- * Replay historical traces through a function and create a test run.
4443
- *
4444
- * Fetches the last N traces for the given trace function key, re-runs each
4445
- * through the provided function, and returns comparison data.
4446
- *
4447
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4448
- * plain callable: plain callables are wrapped internally so each replayed
4449
- * invocation records a trace tied to the test run. The plain-callable form
4450
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4451
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4452
- * root in the app.
4453
- *
4454
- * @param traceFunctionKey - The trace function key to replay
4455
- * @param fn - The function to run recorded inputs through
4456
- * @param options - Optional replay options. When `traceIds` is passed,
4457
- * `limit` is ignored (with a warning): an explicit ID list already
4458
- * determines how many traces replay.
4459
- * @returns ReplayResult with items, testRunId, and testRunUrl
4460
- */
4550
+ registerMockOverride(overrideOrMatch, value) {
4551
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4552
+ this.mockOverrides.push(override);
4553
+ }
4554
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4555
+ clearMockOverrides() {
4556
+ this.mockOverrides.length = 0;
4557
+ }
4461
4558
  async replay(traceFunctionKey, fn, options) {
4462
4559
  const wrappedKey = fn._bitfabTraceFunctionKey;
4463
4560
  let replayFn = fn;
@@ -4478,7 +4575,8 @@ var Bitfab = class {
4478
4575
  this.serviceUrl,
4479
4576
  traceFunctionKey,
4480
4577
  replayFn,
4481
- options
4578
+ options,
4579
+ this.mockOverrides
4482
4580
  );
4483
4581
  }
4484
4582
  };