bitfab 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
@@ -23,12 +23,12 @@ import {
23
23
  flushTraces,
24
24
  getCurrentSpan,
25
25
  getCurrentTrace
26
- } from "./chunk-ZBWTCBVQ.js";
26
+ } from "./chunk-2M5AWVVQ.js";
27
27
  import {
28
28
  BITFAB_PROGRESS_PREFIX,
29
29
  BitfabError,
30
30
  reportReplayProgress
31
- } from "./chunk-5E4BUIYA.js";
31
+ } from "./chunk-V3XORTWI.js";
32
32
  export {
33
33
  BITFAB_PROGRESS_PREFIX,
34
34
  Bitfab,
package/dist/node.cjs CHANGED
@@ -298,6 +298,22 @@ var init_randomUuid = __esm({
298
298
  }
299
299
  });
300
300
 
301
+ // src/mockOverride.ts
302
+ function resolveMockValue(value, ctx) {
303
+ return typeof value === "function" ? value(ctx) : value;
304
+ }
305
+ function normalizeMockOverrides(mockOverride) {
306
+ if (mockOverride === void 0) {
307
+ return [];
308
+ }
309
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
310
+ }
311
+ var init_mockOverride = __esm({
312
+ "src/mockOverride.ts"() {
313
+ "use strict";
314
+ }
315
+ });
316
+
301
317
  // src/replayContext.ts
302
318
  function getReplayContext() {
303
319
  return replayContextStorage?.getStore() ?? null;
@@ -373,6 +389,7 @@ function buildMockTree(rootNode) {
373
389
  counters.set(counterKey, index + 1);
374
390
  spans.set(`${counterKey}:${index}`, {
375
391
  sourceSpanId: node.sourceSpanId,
392
+ externalSpanId: node.externalSpanId,
376
393
  output: node.output,
377
394
  outputMeta: node.outputMeta
378
395
  });
@@ -386,7 +403,7 @@ function buildMockTree(rootNode) {
386
403
  }
387
404
  return { spans };
388
405
  }
389
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
406
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, environment, adaptInputs) {
390
407
  const lease = environment ? serverItem.dbBranchLease : void 0;
391
408
  let inputs = [];
392
409
  let originalOutput;
@@ -405,28 +422,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
405
422
  sourceSpanId: serverItem.externalSpanId
406
423
  });
407
424
  }
425
+ const hasOverrides = resolvedOverrides.length > 0;
426
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
427
+ const includeOutputs = mockStrategy === "all";
408
428
  let mockTree;
409
- if (mockStrategy === "all" || mockStrategy === "marked") {
429
+ if (needTree) {
410
430
  try {
411
431
  const treeResponse = await httpClient.getSpanTree(
412
- serverItem.externalSpanId
432
+ serverItem.externalSpanId,
433
+ { includeOutputs }
413
434
  );
414
435
  if (treeResponse.root) {
415
436
  mockTree = buildMockTree(treeResponse.root);
416
- } else if (mockStrategy === "all") {
437
+ } else if (mockStrategy === "all" || hasOverrides) {
417
438
  throw new BitfabError(
418
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
439
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
419
440
  );
420
441
  } else {
421
442
  mockTree = void 0;
422
443
  }
423
444
  } catch (e) {
424
- if (mockStrategy === "all") {
445
+ if (mockStrategy === "all" || hasOverrides) {
425
446
  throw e;
426
447
  }
427
448
  mockTree = void 0;
428
449
  }
429
450
  }
451
+ const outputCache = /* @__PURE__ */ new Map();
452
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
453
+ let pending = outputCache.get(externalSpanId);
454
+ if (!pending) {
455
+ pending = httpClient.getExternalSpan(externalSpanId).then(
456
+ (s) => deserializeOutput(
457
+ s.rawData?.span_data ?? {}
458
+ )
459
+ );
460
+ outputCache.set(externalSpanId, pending);
461
+ }
462
+ return pending;
463
+ } : void 0;
430
464
  const maybePromise = runWithReplayContext(
431
465
  {
432
466
  testRunId,
@@ -437,6 +471,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
437
471
  mockTree,
438
472
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
439
473
  mockStrategy,
474
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
475
+ fetchSpanOutput,
440
476
  dbBranchLease: lease,
441
477
  pendingPersistence
442
478
  },
@@ -493,7 +529,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
493
529
  await Promise.all(workers);
494
530
  return results;
495
531
  }
496
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
532
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
497
533
  if (options?.traceIds !== void 0) {
498
534
  if (options.traceIds.length === 0) {
499
535
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -533,6 +569,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
533
569
  );
534
570
  const mockStrategy = options?.mock ?? "marked";
535
571
  const maxConcurrency = options?.maxConcurrency ?? 10;
572
+ const resolvedOverrides = [
573
+ ...normalizeMockOverrides(options?.mockOverride),
574
+ ...registeredOverrides
575
+ ];
536
576
  const tasks = serverItems.map(
537
577
  (serverItem) => () => processItem(
538
578
  httpClient,
@@ -540,6 +580,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
540
580
  fn,
541
581
  testRunId,
542
582
  mockStrategy,
583
+ resolvedOverrides,
543
584
  options?.environment,
544
585
  options?.adaptInputs
545
586
  )
@@ -666,6 +707,7 @@ var init_replay = __esm({
666
707
  "src/replay.ts"() {
667
708
  "use strict";
668
709
  init_errors();
710
+ init_mockOverride();
669
711
  init_randomUuid();
670
712
  init_replayContext();
671
713
  init_serialize();
@@ -706,7 +748,7 @@ registerAsyncLocalStorageClass(
706
748
  );
707
749
 
708
750
  // src/version.generated.ts
709
- var __version__ = "0.28.11";
751
+ var __version__ = "0.29.0";
710
752
 
711
753
  // src/constants.ts
712
754
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -1135,9 +1177,14 @@ var HttpClient = class {
1135
1177
  /**
1136
1178
  * Fetch the span tree for a root span.
1137
1179
  * Blocking GET request.
1180
+ *
1181
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1182
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1183
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1138
1184
  */
1139
- async getSpanTree(externalSpanId) {
1140
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1185
+ async getSpanTree(externalSpanId, options) {
1186
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1187
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1141
1188
  const controller = new AbortController();
1142
1189
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1143
1190
  try {
@@ -2723,6 +2770,9 @@ var BitfabLangGraphCallbackHandler = class {
2723
2770
  }
2724
2771
  };
2725
2772
 
2773
+ // src/client.ts
2774
+ init_mockOverride();
2775
+
2726
2776
  // src/openaiAgentSdk.ts
2727
2777
  var BitfabOpenAIAgentHandler = class {
2728
2778
  constructor(config) {
@@ -3538,6 +3588,12 @@ var Bitfab = class {
3538
3588
  constructor(config) {
3539
3589
  /** Gate the empty-key warning to fire at most once. */
3540
3590
  this.apiKeyWarned = false;
3591
+ /**
3592
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3593
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3594
+ * registration order; first matcher wins within this list.
3595
+ */
3596
+ this.mockOverrides = [];
3541
3597
  this.apiKeyConfig = config.apiKey;
3542
3598
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3543
3599
  this.timeout = config.timeout ?? 12e4;
@@ -4150,24 +4206,77 @@ var Bitfab = class {
4150
4206
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4151
4207
  const callIndex = counters.get(counterKey) ?? 0;
4152
4208
  counters.set(counterKey, callIndex + 1);
4153
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4154
- if (shouldMock) {
4155
- const mockKey = `${counterKey}:${callIndex}`;
4156
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4157
- if (mockSpan) {
4158
- let output = mockSpan.output;
4159
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4160
- output = deserializeValue({
4161
- json: mockSpan.output,
4162
- meta: mockSpan.outputMeta
4163
- });
4164
- }
4209
+ const mockKey = `${counterKey}:${callIndex}`;
4210
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4211
+ const emitMock = (output) => {
4212
+ void sendSpan({ result: output, mocked: true });
4213
+ if (fnReturnsPromise) {
4214
+ return Promise.resolve(output);
4215
+ }
4216
+ return output;
4217
+ };
4218
+ const emitMockAsync = (pending) => {
4219
+ if (!fnReturnsPromise) {
4220
+ throw new BitfabError(
4221
+ `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.`
4222
+ );
4223
+ }
4224
+ return (async () => {
4225
+ const output = await pending;
4165
4226
  void sendSpan({ result: output, mocked: true });
4166
- if (fnReturnsPromise) {
4167
- return Promise.resolve(output);
4168
- }
4169
4227
  return output;
4228
+ })();
4229
+ };
4230
+ const resolveRecordedOutput = () => {
4231
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4232
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4233
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4170
4234
  }
4235
+ if (!mockSpan) {
4236
+ return Promise.reject(
4237
+ new BitfabError(
4238
+ `No recorded span to source output for "${traceFunctionKey}".`
4239
+ )
4240
+ );
4241
+ }
4242
+ let output = mockSpan.output;
4243
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4244
+ output = deserializeValue({
4245
+ json: mockSpan.output,
4246
+ meta: mockSpan.outputMeta
4247
+ });
4248
+ }
4249
+ return output;
4250
+ };
4251
+ if (replayCtxForMock.mockOverrides?.length) {
4252
+ const nodeMeta = {
4253
+ traceFunctionKey,
4254
+ spanName: baseSpanParams.spanName,
4255
+ type: options.type ?? "custom",
4256
+ originalSpanId: mockSpan?.sourceSpanId
4257
+ };
4258
+ const override = replayCtxForMock.mockOverrides.find(
4259
+ (o) => o.match(nodeMeta)
4260
+ );
4261
+ if (override) {
4262
+ const injected = resolveMockValue(override.value, {
4263
+ node: nodeMeta,
4264
+ inputs: args,
4265
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4266
+ });
4267
+ if (injected instanceof Promise) {
4268
+ return emitMockAsync(injected);
4269
+ }
4270
+ return emitMock(injected);
4271
+ }
4272
+ }
4273
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4274
+ if (shouldMock && mockSpan) {
4275
+ const recorded = resolveRecordedOutput();
4276
+ if (recorded instanceof Promise) {
4277
+ return emitMockAsync(recorded);
4278
+ }
4279
+ return emitMock(recorded);
4171
4280
  }
4172
4281
  }
4173
4282
  const recordSpan = (result) => {
@@ -4447,26 +4556,14 @@ var Bitfab = class {
4447
4556
  ...params.mocked && { mocked: true }
4448
4557
  });
4449
4558
  }
4450
- /**
4451
- * Replay historical traces through a function and create a test run.
4452
- *
4453
- * Fetches the last N traces for the given trace function key, re-runs each
4454
- * through the provided function, and returns comparison data.
4455
- *
4456
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4457
- * plain callable: plain callables are wrapped internally so each replayed
4458
- * invocation records a trace tied to the test run. The plain-callable form
4459
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4460
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4461
- * root in the app.
4462
- *
4463
- * @param traceFunctionKey - The trace function key to replay
4464
- * @param fn - The function to run recorded inputs through
4465
- * @param options - Optional replay options. When `traceIds` is passed,
4466
- * `limit` is ignored (with a warning): an explicit ID list already
4467
- * determines how many traces replay.
4468
- * @returns ReplayResult with items, testRunId, and testRunUrl
4469
- */
4559
+ registerMockOverride(overrideOrMatch, value) {
4560
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4561
+ this.mockOverrides.push(override);
4562
+ }
4563
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4564
+ clearMockOverrides() {
4565
+ this.mockOverrides.length = 0;
4566
+ }
4470
4567
  async replay(traceFunctionKey, fn, options) {
4471
4568
  const wrappedKey = fn._bitfabTraceFunctionKey;
4472
4569
  let replayFn = fn;
@@ -4487,7 +4584,8 @@ var Bitfab = class {
4487
4584
  this.serviceUrl,
4488
4585
  traceFunctionKey,
4489
4586
  replayFn,
4490
- options
4587
+ options,
4588
+ this.mockOverrides
4491
4589
  );
4492
4590
  }
4493
4591
  };