bitfab 0.12.0 → 0.12.2
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/{chunk-QUCK3IU2.js → chunk-3HBV4GQO.js} +19 -6
- package/dist/{chunk-QUCK3IU2.js.map → chunk-3HBV4GQO.js.map} +1 -1
- package/dist/{chunk-VMJPNYAG.js → chunk-HUSTKOQI.js} +2 -2
- package/dist/{chunk-VMJPNYAG.js.map → chunk-HUSTKOQI.js.map} +1 -1
- package/dist/index.cjs +22 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/node.cjs +22 -7
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +2 -2
- package/dist/{replay-6FKD2UEU.js → replay-RJZGLEHI.js} +7 -5
- package/dist/replay-RJZGLEHI.js.map +1 -0
- package/package.json +1 -1
- package/dist/replay-6FKD2UEU.js.map +0 -1
package/dist/node.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
BitfabOpenAITracingProcessor,
|
|
7
7
|
getCurrentSpan,
|
|
8
8
|
getCurrentTrace
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-3HBV4GQO.js";
|
|
10
10
|
import {
|
|
11
11
|
BitfabError,
|
|
12
12
|
DEFAULT_SERVICE_URL,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
assertAsyncStorageRegistered,
|
|
15
15
|
flushTraces,
|
|
16
16
|
registerAsyncLocalStorageClass
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-HUSTKOQI.js";
|
|
18
18
|
|
|
19
19
|
// src/asyncStorageNode.ts
|
|
20
20
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
flushTraces,
|
|
4
4
|
replayContextReady,
|
|
5
5
|
runWithReplayContext
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-HUSTKOQI.js";
|
|
7
7
|
|
|
8
8
|
// src/replay.ts
|
|
9
9
|
function deserializeInputs(spanData) {
|
|
@@ -35,9 +35,11 @@ function buildMockTree(rootNode) {
|
|
|
35
35
|
function walk(node) {
|
|
36
36
|
const key = node.traceFunctionKey;
|
|
37
37
|
if (key) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const name = node.spanName;
|
|
39
|
+
const counterKey = `${key}:${name}`;
|
|
40
|
+
const index = counters.get(counterKey) ?? 0;
|
|
41
|
+
counters.set(counterKey, index + 1);
|
|
42
|
+
spans.set(`${counterKey}:${index}`, {
|
|
41
43
|
sourceSpanId: node.sourceSpanId,
|
|
42
44
|
output: node.output,
|
|
43
45
|
outputMeta: node.outputMeta
|
|
@@ -143,4 +145,4 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
|
|
|
143
145
|
export {
|
|
144
146
|
replay
|
|
145
147
|
};
|
|
146
|
-
//# sourceMappingURL=replay-
|
|
148
|
+
//# sourceMappingURL=replay-RJZGLEHI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/replay.ts"],"sourcesContent":["/**\n * Replay historical traces through a function and create a test run.\n *\n * The replay flow has three phases:\n * 1. Start — fetches historical traces from the server and creates a test run\n * 2. Execute — re-runs each trace's inputs through the provided function locally\n * 3. Complete — marks the test run as completed on the server\n */\n\nimport type {\n CodeChangeFile,\n HttpClient,\n SpanTreeNode,\n TokenUsage,\n} from \"./http.js\"\nimport { flushTraces } from \"./http.js\"\nimport type { MockTree } from \"./replayContext.js\"\nimport { replayContextReady, runWithReplayContext } from \"./replayContext.js\"\nimport { deserializeValue } from \"./serialize.js\"\n\nexport type MockStrategy = \"none\" | \"all\" | \"marked\"\n\nexport interface ReplayOptions {\n /** Maximum number of traces to replay (1–100, default 5). */\n limit?: number\n /** Optional list of specific trace IDs to replay. */\n traceIds?: string[]\n /** Maximum number of items to process in parallel. Set to 1 for sequential. Default 10. */\n maxConcurrency?: number\n /**\n * Description of the code change being tested in this replay. Stored on\n * the resulting experiment so the change can be reviewed alongside results.\n */\n codeChangeDescription?: string\n /**\n * Files edited as part of this code change. Each entry holds the file path\n * and the full `before`/`after` contents — the agent reads each file before\n * and after editing and passes the two strings. Use `\"\"` for newly created\n * files (`before`) or deleted files (`after`).\n */\n codeChangeFiles?: CodeChangeFile[]\n /**\n * Mock strategy for child spans during replay.\n * - \"none\": everything runs real code (default)\n * - \"all\": every child withSpan returns historical output\n * - \"marked\": only spans tagged with { mockOnReplay: true } in SpanOptions are mocked\n */\n mock?: MockStrategy\n}\n\nexport interface ReplayItem<T> {\n /** Deserialized inputs from the original trace. */\n input: unknown[]\n /** The result returned by the function during replay, or undefined on error. */\n result: T | undefined\n /** The original output from the historical trace. */\n originalOutput: unknown\n /** Error message if the function threw, or null on success. */\n error: string | null\n /** Original trace duration in milliseconds, or null if timestamps are missing. */\n durationMs: number | null\n /** Token usage from the original trace, or null if not captured. */\n tokens: TokenUsage | null\n /** Model name from the original trace, or null if not captured. */\n model: string | null\n}\n\nexport type { CodeChangeFile, TokenUsage }\n\nexport interface ReplayResult<T> {\n /** Individual replay items with inputs, results, and comparison data. */\n items: ReplayItem<T>[]\n /** The test run ID created on the server. */\n testRunId: string\n /** Full URL to view the test run in the dashboard. */\n testRunUrl: string\n}\n\n/**\n * Deserialize inputs from a historical span's rawData.\n *\n * Prefers superjson-serialized `input_meta` for type preservation,\n * falls back to the raw `input` field.\n */\nfunction deserializeInputs(spanData: Record<string, unknown>): unknown[] {\n const inputMeta = spanData.input_meta as unknown\n const rawInput = spanData.input\n\n // If superjson meta is available, deserialize with type reconstruction\n if (inputMeta !== undefined && inputMeta !== null) {\n const deserialized = deserializeValue({ json: rawInput, meta: inputMeta })\n if (Array.isArray(deserialized)) {\n return deserialized\n }\n return deserialized !== undefined && deserialized !== null\n ? [deserialized]\n : []\n }\n\n // Fall back to raw input\n if (Array.isArray(rawInput)) {\n return rawInput\n }\n return rawInput !== undefined && rawInput !== null ? [rawInput] : []\n}\n\n/**\n * Deserialize the original output from a historical span's rawData.\n */\nfunction deserializeOutput(spanData: Record<string, unknown>): unknown {\n const outputMeta = spanData.output_meta as unknown\n const rawOutput = spanData.output\n\n if (outputMeta !== undefined && outputMeta !== null) {\n return deserializeValue({ json: rawOutput, meta: outputMeta })\n }\n\n return rawOutput\n}\n\n/**\n * Walk the children of a root span tree node in depth-first order and build\n * a MockTree keyed by `${traceFunctionKey}:${spanName}:${callIndex}`.\n *\n * The historical root itself is NOT walked. At replay time the runtime root\n * span has `isRootSpan === true` and never queries the mockTree (mock\n * interception is skipped for root spans by design), so the root has nothing\n * to look up. Walking it would just leave an unreachable entry in the table.\n *\n * The (key, name) compound match is what disambiguates same-key spans:\n * - A wrapped function's children commonly share its traceFunctionKey via\n * the fluent `getFunction(key).withSpan({ name }, ...)` pattern. They\n * disambiguate by `name`, never colliding with each other.\n * - Recursion: same (key, name) at every depth. callIndex per (key, name)\n * orders them correctly without leaking the historical root into the\n * nested call's slot.\n * - Outer-wrapper replay scripts: the outer wrapper's `name` is distinct\n * from anything in the historical tree (it only exists at replay), so\n * its presence never disturbs counters or lookups for spans that do\n * exist in the historical tree.\n */\nfunction buildMockTree(rootNode: SpanTreeNode): MockTree {\n const spans = new Map<\n string,\n { sourceSpanId: string; output: unknown; outputMeta?: unknown }\n >()\n const counters = new Map<string, number>()\n\n function walk(node: SpanTreeNode): void {\n const key = node.traceFunctionKey\n if (key) {\n const name = node.spanName\n const counterKey = `${key}:${name}`\n const index = counters.get(counterKey) ?? 0\n counters.set(counterKey, index + 1)\n spans.set(`${counterKey}:${index}`, {\n sourceSpanId: node.sourceSpanId,\n output: node.output,\n outputMeta: node.outputMeta,\n })\n }\n for (const child of node.children) {\n walk(child)\n }\n }\n\n for (const child of rootNode.children) {\n walk(child)\n }\n\n return { spans }\n}\n\n/**\n * Execute a single replay item: fetch span data, deserialize inputs, call\n * the function within a replay context that injects testRunId into new spans.\n */\nasync function processItem<TReturn>(\n httpClient: HttpClient,\n serverItem: {\n externalSpanId: string\n durationMs: number | null\n tokens: TokenUsage | null\n model: string | null\n },\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n testRunId: string,\n mockStrategy: MockStrategy,\n): Promise<ReplayItem<TReturn>> {\n const span = await httpClient.getExternalSpan(serverItem.externalSpanId)\n const spanData = (span.rawData?.span_data ?? {}) as Record<string, unknown>\n\n const inputs = deserializeInputs(spanData)\n const originalOutput = deserializeOutput(spanData)\n\n // Build mock tree when mocking is active\n let mockTree: MockTree | undefined\n if (mockStrategy === \"all\" || mockStrategy === \"marked\") {\n const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId)\n mockTree = buildMockTree(treeResponse.root)\n }\n\n let result: TReturn | undefined\n let error: string | null = null\n\n try {\n const maybePromise = runWithReplayContext(\n {\n testRunId,\n inputSourceSpanId: span.id,\n inputSourceTraceId: span.externalTraceId,\n mockTree,\n callCounters: mockTree ? new Map() : undefined,\n mockStrategy,\n },\n () => fn(...inputs),\n )\n result = maybePromise instanceof Promise ? await maybePromise : maybePromise\n } catch (e) {\n error = e instanceof Error ? e.message : String(e)\n }\n\n return {\n input: inputs,\n result,\n originalOutput,\n error,\n durationMs: serverItem.durationMs ?? null,\n tokens: serverItem.tokens ?? null,\n model: serverItem.model ?? null,\n }\n}\n\n/**\n * Run async tasks with a concurrency limit.\n * Each task factory is called when a slot opens; results preserve input order.\n */\nasync function mapWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n maxConcurrency: number,\n): Promise<T[]> {\n const results: T[] = new Array(tasks.length)\n let nextIndex = 0\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++\n results[index] = await tasks[index]()\n }\n }\n\n const workers = Array.from(\n { length: Math.min(maxConcurrency, tasks.length) },\n () => worker(),\n )\n await Promise.all(workers)\n return results\n}\n\n/**\n * Replay historical traces through a function and create a test run.\n *\n * @internal Called by Bitfab.replay — not part of the public API.\n */\nexport async function replay<TReturn>(\n httpClient: HttpClient,\n serviceUrl: string,\n traceFunctionKey: string,\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n options?: ReplayOptions,\n): Promise<ReplayResult<TReturn>> {\n await replayContextReady\n\n const {\n testRunId,\n testRunUrl,\n items: serverItems,\n } = await httpClient.startReplay(\n traceFunctionKey,\n options?.limit ?? 5,\n options?.traceIds,\n options?.codeChangeDescription,\n options?.codeChangeFiles,\n )\n\n const mockStrategy: MockStrategy = options?.mock ?? \"none\"\n const maxConcurrency = options?.maxConcurrency ?? 10\n\n const tasks = serverItems.map(\n (serverItem) => () =>\n processItem(httpClient, serverItem, fn, testRunId, mockStrategy),\n )\n const resultItems = await mapWithConcurrency(tasks, maxConcurrency)\n\n await flushTraces()\n\n try {\n await httpClient.completeReplay(testRunId)\n } catch (e) {\n try {\n console.error(\"Bitfab: Failed to complete replay:\", e)\n } catch {\n // Never crash the host app\n }\n }\n\n return {\n items: resultItems,\n testRunId,\n testRunUrl: `${serviceUrl}${testRunUrl}`,\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/replay.ts"],"sourcesContent":["/**\n * Replay historical traces through a function and create a test run.\n *\n * The replay flow has three phases:\n * 1. Start — fetches historical traces from the server and creates a test run\n * 2. Execute — re-runs each trace's inputs through the provided function locally\n * 3. Complete — marks the test run as completed on the server\n */\n\nimport type {\n CodeChangeFile,\n HttpClient,\n SpanTreeNode,\n TokenUsage,\n} from \"./http.js\"\nimport { flushTraces } from \"./http.js\"\nimport type { MockTree } from \"./replayContext.js\"\nimport { replayContextReady, runWithReplayContext } from \"./replayContext.js\"\nimport { deserializeValue } from \"./serialize.js\"\n\nexport type MockStrategy = \"none\" | \"all\" | \"marked\"\n\nexport interface ReplayOptions {\n /** Maximum number of traces to replay (1–100, default 5). */\n limit?: number\n /** Optional list of specific trace IDs to replay. */\n traceIds?: string[]\n /** Maximum number of items to process in parallel. Set to 1 for sequential. Default 10. */\n maxConcurrency?: number\n /**\n * Description of the code change being tested in this replay. Stored on\n * the resulting experiment so the change can be reviewed alongside results.\n */\n codeChangeDescription?: string\n /**\n * Files edited as part of this code change. Each entry holds the file path\n * and the full `before`/`after` contents — the agent reads each file before\n * and after editing and passes the two strings. Use `\"\"` for newly created\n * files (`before`) or deleted files (`after`).\n */\n codeChangeFiles?: CodeChangeFile[]\n /**\n * Mock strategy for child spans during replay.\n * - \"none\": everything runs real code (default)\n * - \"all\": every child withSpan returns historical output\n * - \"marked\": only spans tagged with { mockOnReplay: true } in SpanOptions are mocked\n */\n mock?: MockStrategy\n}\n\nexport interface ReplayItem<T> {\n /** Deserialized inputs from the original trace. */\n input: unknown[]\n /** The result returned by the function during replay, or undefined on error. */\n result: T | undefined\n /** The original output from the historical trace. */\n originalOutput: unknown\n /** Error message if the function threw, or null on success. */\n error: string | null\n /** Original trace duration in milliseconds, or null if timestamps are missing. */\n durationMs: number | null\n /** Token usage from the original trace, or null if not captured. */\n tokens: TokenUsage | null\n /** Model name from the original trace, or null if not captured. */\n model: string | null\n}\n\nexport type { CodeChangeFile, TokenUsage }\n\nexport interface ReplayResult<T> {\n /** Individual replay items with inputs, results, and comparison data. */\n items: ReplayItem<T>[]\n /** The test run ID created on the server. */\n testRunId: string\n /** Full URL to view the test run in the dashboard. */\n testRunUrl: string\n}\n\n/**\n * Deserialize inputs from a historical span's rawData.\n *\n * Prefers superjson-serialized `input_meta` for type preservation,\n * falls back to the raw `input` field.\n */\nfunction deserializeInputs(spanData: Record<string, unknown>): unknown[] {\n const inputMeta = spanData.input_meta as unknown\n const rawInput = spanData.input\n\n // If superjson meta is available, deserialize with type reconstruction\n if (inputMeta !== undefined && inputMeta !== null) {\n const deserialized = deserializeValue({ json: rawInput, meta: inputMeta })\n if (Array.isArray(deserialized)) {\n return deserialized\n }\n return deserialized !== undefined && deserialized !== null\n ? [deserialized]\n : []\n }\n\n // Fall back to raw input\n if (Array.isArray(rawInput)) {\n return rawInput\n }\n return rawInput !== undefined && rawInput !== null ? [rawInput] : []\n}\n\n/**\n * Deserialize the original output from a historical span's rawData.\n */\nfunction deserializeOutput(spanData: Record<string, unknown>): unknown {\n const outputMeta = spanData.output_meta as unknown\n const rawOutput = spanData.output\n\n if (outputMeta !== undefined && outputMeta !== null) {\n return deserializeValue({ json: rawOutput, meta: outputMeta })\n }\n\n return rawOutput\n}\n\n/**\n * Walk the children of a root span tree node in depth-first order and build\n * a MockTree keyed by `${traceFunctionKey}:${globalCallIndex}`.\n *\n * The root node itself is excluded (it is the span being replayed, not a\n * child to mock). Each unique traceFunctionKey gets an incrementing counter\n * so that repeated calls to the same key are distinguished by call order.\n */\nfunction buildMockTree(rootNode: SpanTreeNode): MockTree {\n const spans = new Map<\n string,\n { sourceSpanId: string; output: unknown; outputMeta?: unknown }\n >()\n const counters = new Map<string, number>()\n\n function walk(node: SpanTreeNode): void {\n const key = node.traceFunctionKey\n if (key) {\n const index = counters.get(key) ?? 0\n counters.set(key, index + 1)\n spans.set(`${key}:${index}`, {\n sourceSpanId: node.sourceSpanId,\n output: node.output,\n outputMeta: node.outputMeta,\n })\n }\n for (const child of node.children) {\n walk(child)\n }\n }\n\n // Walk children of root (not the root itself)\n for (const child of rootNode.children) {\n walk(child)\n }\n\n return { spans }\n}\n\n/**\n * Execute a single replay item: fetch span data, deserialize inputs, call\n * the function within a replay context that injects testRunId into new spans.\n */\nasync function processItem<TReturn>(\n httpClient: HttpClient,\n serverItem: {\n externalSpanId: string\n durationMs: number | null\n tokens: TokenUsage | null\n model: string | null\n },\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n testRunId: string,\n mockStrategy: MockStrategy,\n): Promise<ReplayItem<TReturn>> {\n const span = await httpClient.getExternalSpan(serverItem.externalSpanId)\n const spanData = (span.rawData?.span_data ?? {}) as Record<string, unknown>\n\n const inputs = deserializeInputs(spanData)\n const originalOutput = deserializeOutput(spanData)\n\n // Build mock tree when mocking is active\n let mockTree: MockTree | undefined\n if (mockStrategy === \"all\" || mockStrategy === \"marked\") {\n const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId)\n mockTree = buildMockTree(treeResponse.root)\n }\n\n let result: TReturn | undefined\n let error: string | null = null\n\n try {\n const maybePromise = runWithReplayContext(\n {\n testRunId,\n inputSourceSpanId: span.id,\n inputSourceTraceId: span.externalTraceId,\n mockTree,\n callCounters: mockTree ? new Map() : undefined,\n mockStrategy,\n },\n () => fn(...inputs),\n )\n result = maybePromise instanceof Promise ? await maybePromise : maybePromise\n } catch (e) {\n error = e instanceof Error ? e.message : String(e)\n }\n\n return {\n input: inputs,\n result,\n originalOutput,\n error,\n durationMs: serverItem.durationMs ?? null,\n tokens: serverItem.tokens ?? null,\n model: serverItem.model ?? null,\n }\n}\n\n/**\n * Run async tasks with a concurrency limit.\n * Each task factory is called when a slot opens; results preserve input order.\n */\nasync function mapWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n maxConcurrency: number,\n): Promise<T[]> {\n const results: T[] = new Array(tasks.length)\n let nextIndex = 0\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++\n results[index] = await tasks[index]()\n }\n }\n\n const workers = Array.from(\n { length: Math.min(maxConcurrency, tasks.length) },\n () => worker(),\n )\n await Promise.all(workers)\n return results\n}\n\n/**\n * Replay historical traces through a function and create a test run.\n *\n * @internal Called by Bitfab.replay — not part of the public API.\n */\nexport async function replay<TReturn>(\n httpClient: HttpClient,\n serviceUrl: string,\n traceFunctionKey: string,\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n options?: ReplayOptions,\n): Promise<ReplayResult<TReturn>> {\n await replayContextReady\n\n const {\n testRunId,\n testRunUrl,\n items: serverItems,\n } = await httpClient.startReplay(\n traceFunctionKey,\n options?.limit ?? 5,\n options?.traceIds,\n options?.codeChangeDescription,\n options?.codeChangeFiles,\n )\n\n const mockStrategy: MockStrategy = options?.mock ?? \"none\"\n const maxConcurrency = options?.maxConcurrency ?? 10\n\n const tasks = serverItems.map(\n (serverItem) => () =>\n processItem(httpClient, serverItem, fn, testRunId, mockStrategy),\n )\n const resultItems = await mapWithConcurrency(tasks, maxConcurrency)\n\n await flushTraces()\n\n try {\n await httpClient.completeReplay(testRunId)\n } catch (e) {\n try {\n console.error(\"Bitfab: Failed to complete replay:\", e)\n } catch {\n // Never crash the host app\n }\n }\n\n return {\n items: resultItems,\n testRunId,\n testRunUrl: `${serviceUrl}${testRunUrl}`,\n }\n}\n"],"mappings":"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","names":[]}
|