@bitfab/sdk 0.61.1 → 0.61.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/index.cjs CHANGED
@@ -42,7 +42,7 @@ var __version__, __packageName__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.61.1";
45
+ __version__ = "0.61.2";
46
46
  __packageName__ = "@bitfab/sdk";
47
47
  }
48
48
  });
@@ -2951,6 +2951,46 @@ var init_http = __esm({
2951
2951
  }
2952
2952
  return { ...rest, experimentId, experimentUrl };
2953
2953
  }
2954
+ async resumeReplay(experimentId, traceFunctionKey, codeChangeFiles, force) {
2955
+ const payload = {
2956
+ experimentId,
2957
+ traceFunctionKey,
2958
+ lazyDbBranchLease: true
2959
+ };
2960
+ if (force === true) {
2961
+ payload.force = true;
2962
+ }
2963
+ if (codeChangeFiles !== void 0) {
2964
+ payload.codeChangeFiles = codeChangeFiles;
2965
+ }
2966
+ const git = await resolvedGitState();
2967
+ if (git && !isEmptyGitState(git)) {
2968
+ payload.git = git;
2969
+ }
2970
+ const response = await this.request(
2971
+ "/api/sdk/replay/resume",
2972
+ payload,
2973
+ { timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
2974
+ );
2975
+ const { testRunId, testRunUrl, ...rest } = response;
2976
+ const resumedExperimentId = response.experimentId ?? testRunId;
2977
+ const experimentUrl = response.experimentUrl ?? testRunUrl;
2978
+ if (resumedExperimentId === void 0 || experimentUrl === void 0) {
2979
+ throw new BitfabError(
2980
+ "Replay resume response is missing the experiment ID or URL."
2981
+ );
2982
+ }
2983
+ return {
2984
+ ...rest,
2985
+ experimentId: resumedExperimentId,
2986
+ experimentUrl,
2987
+ work: rest.work ?? [],
2988
+ finished: rest.finished ?? [],
2989
+ skippedOriginalTraceIds: rest.skippedOriginalTraceIds ?? [],
2990
+ includeDbBranchLease: rest.includeDbBranchLease ?? false,
2991
+ dbBranchSettings: rest.dbBranchSettings ?? null
2992
+ };
2993
+ }
2954
2994
  /**
2955
2995
  * Fetch an external span by ID.
2956
2996
  * Blocking GET request.
@@ -3586,6 +3626,88 @@ function expandReplayWork(serverItems, attempts) {
3586
3626
  (_, attempt) => serverItems.map((serverItem) => ({ serverItem, attempt }))
3587
3627
  ).flat();
3588
3628
  }
3629
+ function validateResume(resume, options) {
3630
+ if (resume.trim().length === 0) {
3631
+ throw new BitfabError("resume must be the ID of the experiment to resume.");
3632
+ }
3633
+ const conflicting = RESUME_CONFLICTING_OPTIONS.filter(
3634
+ (name) => options[name] !== void 0 && options[name] !== false
3635
+ );
3636
+ if (conflicting.length > 0) {
3637
+ throw new BitfabError(
3638
+ `resume continues experiment ${resume} with the traces, attempts, and graders it started with, so it cannot be combined with ${conflicting.join(", ")}. Drop those options, or start a new replay without resume.`
3639
+ );
3640
+ }
3641
+ }
3642
+ async function resumeReplaySession(httpClient, experimentId, traceFunctionKey, codeChangeFiles, force, requestedDbBranch) {
3643
+ const response = await httpClient.resumeReplay(
3644
+ experimentId,
3645
+ traceFunctionKey,
3646
+ codeChangeFiles,
3647
+ force
3648
+ );
3649
+ const work = response.work.map(({ itemIndex, attempt }) => {
3650
+ const serverItem = response.items[itemIndex];
3651
+ if (serverItem === void 0) {
3652
+ throw new BitfabError(
3653
+ `Replay resume for experiment ${response.experimentId} named item ${itemIndex}, but the server returned only ${response.items.length} item(s).`
3654
+ );
3655
+ }
3656
+ return { serverItem, attempt };
3657
+ });
3658
+ if (response.skippedOriginalTraceIds.length > 0) {
3659
+ try {
3660
+ console.warn(
3661
+ `Bitfab: ${response.skippedOriginalTraceIds.length} trace(s) selected when experiment ${response.experimentId} started no longer exist, so resume skips them: ${response.skippedOriginalTraceIds.join(", ")}`
3662
+ );
3663
+ } catch {
3664
+ }
3665
+ }
3666
+ if (requestedDbBranch !== void 0 && dbBranchEnabled(requestedDbBranch) !== response.includeDbBranchLease) {
3667
+ try {
3668
+ console.warn(
3669
+ `Bitfab: experiment ${response.experimentId} started with database branching ${response.includeDbBranchLease ? "on" : "off"}, so resume keeps it ${response.includeDbBranchLease ? "on" : "off"} and ignores the dbBranch option passed here.`
3670
+ );
3671
+ } catch {
3672
+ }
3673
+ }
3674
+ return {
3675
+ experimentId: response.experimentId,
3676
+ experimentUrl: response.experimentUrl,
3677
+ attempts: response.attempts,
3678
+ work,
3679
+ finished: response.finished,
3680
+ includeDbBranchLease: response.includeDbBranchLease,
3681
+ dbBranchSettings: response.dbBranchSettings ?? void 0
3682
+ };
3683
+ }
3684
+ function carriedOverReplayItem(finished) {
3685
+ return {
3686
+ traceId: finished.traceId,
3687
+ originalTraceId: finished.originalTraceId,
3688
+ originalSpanId: "",
3689
+ sourceTraceId: finished.originalTraceId,
3690
+ sourceSpanId: "",
3691
+ attempt: finished.attempt,
3692
+ input: [],
3693
+ result: void 0,
3694
+ originalOutput: void 0,
3695
+ error: finished.hasError ? CARRIED_OVER_ERROR : null,
3696
+ traceError: null,
3697
+ replayError: null,
3698
+ durationMs: null,
3699
+ originalDurationMs: null,
3700
+ originalTokens: null,
3701
+ originalModel: null,
3702
+ tokens: null,
3703
+ model: null,
3704
+ dbSnapshotRef: null,
3705
+ dbBranchTimings: null,
3706
+ traceOutline: null,
3707
+ originalTraceOutline: null,
3708
+ carriedOver: true
3709
+ };
3710
+ }
3589
3711
  function resolveAttempts(attempts) {
3590
3712
  if (attempts === void 0) {
3591
3713
  return 1;
@@ -4038,6 +4160,13 @@ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted)
4038
4160
  return results;
4039
4161
  }
4040
4162
  async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
4163
+ if (options?.resume !== void 0) {
4164
+ validateResume(options.resume, options);
4165
+ } else if (options?.force === true) {
4166
+ throw new BitfabError(
4167
+ "force only applies when resuming an experiment. Pass it together with resume."
4168
+ );
4169
+ }
4041
4170
  if (options?.traceIds !== void 0) {
4042
4171
  if (options.traceIds.length === 0) {
4043
4172
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -4057,7 +4186,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4057
4186
  } catch {
4058
4187
  }
4059
4188
  }
4060
- const attempts = resolveAttempts(options?.attempts);
4189
+ const requestedAttempts = resolveAttempts(options?.attempts);
4061
4190
  const maxConcurrency = options?.maxConcurrency ?? 10;
4062
4191
  if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
4063
4192
  throw new BitfabError(
@@ -4076,40 +4205,59 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4076
4205
  }
4077
4206
  }
4078
4207
  }
4079
- const {
4080
- experimentId,
4081
- experimentUrl,
4082
- items: serverItems
4083
- } = await httpClient.startReplay(
4084
- traceFunctionKey,
4085
- // limit is meaningless with explicit traceIds (the ID list determines
4086
- // the count), so it's omitted from the request entirely.
4087
- options?.traceIds ? void 0 : options?.limit ?? 5,
4088
- options?.traceIds,
4089
- options?.name,
4090
- codeChangeDescription,
4091
- codeChangeFiles,
4092
- // A dry run executes nothing, so a branch would be provisioned (and billed)
4093
- // for code that never runs, and a seeded source's refusal would fail the item
4094
- // before it could report its resolved inputs.
4095
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4096
- options?.experimentGroupId,
4097
- datasetIds,
4098
- options?.graderIds,
4099
- resolveDbBranchSettings(options?.dbBranch),
4100
- attempts,
4101
- options?.adaptInputs !== void 0,
4102
- options?.onlyWithAssertions,
4103
- options?.judgeAssertions
4104
- );
4105
- if (serverItems.length === 0) {
4106
- try {
4107
- console.warn(
4108
- `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4109
- );
4110
- } catch {
4208
+ const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4209
+ let session;
4210
+ if (options?.resume === void 0) {
4211
+ const started2 = await httpClient.startReplay(
4212
+ traceFunctionKey,
4213
+ // limit is meaningless with explicit traceIds (the ID list determines
4214
+ // the count), so it's omitted from the request entirely.
4215
+ options?.traceIds ? void 0 : options?.limit ?? 5,
4216
+ options?.traceIds,
4217
+ options?.name,
4218
+ codeChangeDescription,
4219
+ codeChangeFiles,
4220
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
4221
+ // for code that never runs, and a seeded source's refusal would fail the item
4222
+ // before it could report its resolved inputs.
4223
+ includeDbBranchLease,
4224
+ options?.experimentGroupId,
4225
+ datasetIds,
4226
+ options?.graderIds,
4227
+ resolveDbBranchSettings(options?.dbBranch),
4228
+ requestedAttempts,
4229
+ options?.adaptInputs !== void 0,
4230
+ options?.onlyWithAssertions,
4231
+ options?.judgeAssertions
4232
+ );
4233
+ if (started2.items.length === 0) {
4234
+ try {
4235
+ console.warn(
4236
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
4237
+ );
4238
+ } catch {
4239
+ }
4111
4240
  }
4241
+ session = {
4242
+ experimentId: started2.experimentId,
4243
+ experimentUrl: started2.experimentUrl,
4244
+ attempts: requestedAttempts,
4245
+ work: expandReplayWork(started2.items, requestedAttempts),
4246
+ finished: [],
4247
+ includeDbBranchLease,
4248
+ dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
4249
+ };
4250
+ } else {
4251
+ session = await resumeReplaySession(
4252
+ httpClient,
4253
+ options.resume,
4254
+ traceFunctionKey,
4255
+ codeChangeFiles,
4256
+ options.force,
4257
+ options.dbBranch
4258
+ );
4112
4259
  }
4260
+ const { experimentId, experimentUrl, attempts } = session;
4113
4261
  const mockStrategy = options?.mock ?? "marked";
4114
4262
  const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4115
4263
  try {
@@ -4123,7 +4271,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4123
4271
  ...normalizeMockOverrides(options?.mockOverride),
4124
4272
  ...registeredOverrides
4125
4273
  ];
4126
- const workItems = expandReplayWork(serverItems, attempts);
4274
+ const workItems = session.work;
4275
+ const carriedOverItems = session.finished.map(
4276
+ (finished) => carriedOverReplayItem(finished)
4277
+ );
4127
4278
  const replayedTraceIds = workItems.map(() => randomUuid());
4128
4279
  httpClient.trackTraceDeliveries(replayedTraceIds);
4129
4280
  const tasks = workItems.map(
@@ -4136,18 +4287,18 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4136
4287
  resolvedOverrides,
4137
4288
  replayedTraceIds[index],
4138
4289
  attempt,
4139
- dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
4140
- resolveDbBranchSettings(options?.dbBranch),
4290
+ session.includeDbBranchLease,
4291
+ session.dbBranchSettings,
4141
4292
  options?.adaptInputs,
4142
4293
  options?.dryRun === true
4143
4294
  )
4144
4295
  );
4145
- const total = tasks.length;
4296
+ const total = tasks.length + carriedOverItems.length;
4146
4297
  const onItemFinish = options?.onItemFinish ?? options?.onProgress;
4147
- let completed = 0;
4148
- let started = 0;
4149
- let succeeded = 0;
4150
- let errored = 0;
4298
+ let completed = carriedOverItems.length;
4299
+ let started = carriedOverItems.length;
4300
+ let errored = carriedOverItems.filter((item) => item.error !== null).length;
4301
+ let succeeded = completed - errored;
4151
4302
  let flushInFlight = null;
4152
4303
  let flushPending = false;
4153
4304
  const flushFinishedItemTrace = () => {
@@ -4167,7 +4318,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4167
4318
  }
4168
4319
  return flushInFlight;
4169
4320
  };
4170
- const resultItems = await mapWithConcurrency2(
4321
+ const ranItems = await mapWithConcurrency2(
4171
4322
  tasks,
4172
4323
  maxConcurrency,
4173
4324
  async (item, index) => {
@@ -4254,6 +4405,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4254
4405
  }
4255
4406
  } : void 0
4256
4407
  );
4408
+ const resultItems = [...carriedOverItems, ...ranItems];
4257
4409
  if (options?.dryRun === true) {
4258
4410
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4259
4411
  const dryResult = {
@@ -4273,11 +4425,11 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4273
4425
  experimentId,
4274
4426
  fullExperimentUrl
4275
4427
  );
4276
- for (let index = 0; index < resultItems.length; index += 1) {
4428
+ for (let index = 0; index < ranItems.length; index += 1) {
4277
4429
  const localId = replayedTraceIds[index];
4278
4430
  const readBack = localId ? deliveredTraceIds[localId] : void 0;
4279
4431
  if (readBack !== void 0) {
4280
- resultItems[index].traceId = readBack;
4432
+ ranItems[index].traceId = readBack;
4281
4433
  }
4282
4434
  }
4283
4435
  const completeResult = await preserveReplayFailure(
@@ -4291,8 +4443,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4291
4443
  if (serverTraceIds !== void 0) {
4292
4444
  const missing = [];
4293
4445
  let completedCount = 0;
4294
- for (let index = 0; index < resultItems.length; index += 1) {
4295
- const item = resultItems[index];
4446
+ for (let index = 0; index < ranItems.length; index += 1) {
4447
+ const item = ranItems[index];
4296
4448
  const localId = replayedTraceIds[index];
4297
4449
  const mapped = localId ? serverTraceIds[localId] : void 0;
4298
4450
  item.traceId = item.traceId ?? mapped ?? null;
@@ -4378,7 +4530,7 @@ async function writeReplayResultFile(result) {
4378
4530
  }
4379
4531
  }
4380
4532
  }
4381
- var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4533
+ var REPLAY_PERSISTENCE_TIMEOUT_MS, MAX_REPLAY_ATTEMPTS, RESUME_CONFLICTING_OPTIONS, CARRIED_OVER_ERROR, BITFAB_PROGRESS_PREFIX, ReplayError, DbBranchReplayError;
4382
4534
  var init_replay = __esm({
4383
4535
  "src/replay.ts"() {
4384
4536
  "use strict";
@@ -4392,6 +4544,17 @@ var init_replay = __esm({
4392
4544
  init_serialize();
4393
4545
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
4394
4546
  MAX_REPLAY_ATTEMPTS = 100;
4547
+ RESUME_CONFLICTING_OPTIONS = [
4548
+ "traceIds",
4549
+ "limit",
4550
+ "datasetId",
4551
+ "datasetIds",
4552
+ "attempts",
4553
+ "graderIds",
4554
+ "onlyWithAssertions",
4555
+ "dryRun"
4556
+ ];
4557
+ CARRIED_OVER_ERROR = "This item errored in an earlier run of this experiment, before it was resumed.";
4395
4558
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
4396
4559
  ReplayError = class extends BitfabError {
4397
4560
  constructor(message, items, experimentId, experimentUrl, cause) {