@prismatic-io/lux 0.0.1 → 0.0.2-preview.4

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.
Files changed (142) hide show
  1. package/lib/answerers/terminal/index.js +1 -1
  2. package/lib/answerers/terminal/index.js.map +1 -1
  3. package/lib/assertions/authoring.d.ts +14 -0
  4. package/lib/assertions/authoring.d.ts.map +1 -0
  5. package/lib/assertions/authoring.js +3 -0
  6. package/lib/assertions/authoring.js.map +1 -0
  7. package/lib/assertions/core/helpers.d.ts +2 -7
  8. package/lib/assertions/core/helpers.d.ts.map +1 -1
  9. package/lib/assertions/core/helpers.js +2 -6
  10. package/lib/assertions/core/helpers.js.map +1 -1
  11. package/lib/authoring.d.ts +1 -0
  12. package/lib/authoring.d.ts.map +1 -1
  13. package/lib/authoring.js +1 -0
  14. package/lib/authoring.js.map +1 -1
  15. package/lib/cli/program.d.ts.map +1 -1
  16. package/lib/cli/program.js +6 -2
  17. package/lib/cli/program.js.map +1 -1
  18. package/lib/cli/render/reporter.d.ts.map +1 -1
  19. package/lib/cli/render/reporter.js +16 -3
  20. package/lib/cli/render/reporter.js.map +1 -1
  21. package/lib/cli/run-options.d.ts +1 -0
  22. package/lib/cli/run-options.d.ts.map +1 -1
  23. package/lib/cli/run-options.js.map +1 -1
  24. package/lib/core/artifact-evidence.d.ts +8 -0
  25. package/lib/core/artifact-evidence.d.ts.map +1 -1
  26. package/lib/core/artifact-evidence.js +36 -4
  27. package/lib/core/artifact-evidence.js.map +1 -1
  28. package/lib/core/assertion.d.ts +19 -9
  29. package/lib/core/assertion.d.ts.map +1 -1
  30. package/lib/core/assertion.js +26 -0
  31. package/lib/core/assertion.js.map +1 -1
  32. package/lib/core/case-authoring-state.d.ts +12 -0
  33. package/lib/core/case-authoring-state.d.ts.map +1 -0
  34. package/lib/core/case-authoring-state.js +43 -0
  35. package/lib/core/case-authoring-state.js.map +1 -0
  36. package/lib/core/case.d.ts +29 -1
  37. package/lib/core/case.d.ts.map +1 -1
  38. package/lib/core/case.js +14 -6
  39. package/lib/core/case.js.map +1 -1
  40. package/lib/core/driver.d.ts +3 -0
  41. package/lib/core/driver.d.ts.map +1 -1
  42. package/lib/core/driver.js.map +1 -1
  43. package/lib/core/events.d.ts +2 -0
  44. package/lib/core/events.d.ts.map +1 -1
  45. package/lib/core/events.js +1 -0
  46. package/lib/core/events.js.map +1 -1
  47. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  48. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  49. package/lib/core/lifecycle-fixtures.js +4 -2
  50. package/lib/core/lifecycle-fixtures.js.map +1 -1
  51. package/lib/core/lifecycle-hooks.d.ts +9 -0
  52. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  53. package/lib/core/lifecycle-hooks.js +8 -2
  54. package/lib/core/lifecycle-hooks.js.map +1 -1
  55. package/lib/core/run.d.ts +1 -0
  56. package/lib/core/run.d.ts.map +1 -1
  57. package/lib/core/wire.d.ts +1 -0
  58. package/lib/core/wire.d.ts.map +1 -1
  59. package/lib/core/wire.js +5 -1
  60. package/lib/core/wire.js.map +1 -1
  61. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  62. package/lib/drivers/subprocess/index.js +6 -0
  63. package/lib/drivers/subprocess/index.js.map +1 -1
  64. package/lib/index.d.ts +4 -4
  65. package/lib/index.d.ts.map +1 -1
  66. package/lib/index.js +4 -4
  67. package/lib/index.js.map +1 -1
  68. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  69. package/lib/orchestrator/annotation-store.js +6 -2
  70. package/lib/orchestrator/annotation-store.js.map +1 -1
  71. package/lib/orchestrator/case-extensions.d.ts +6 -0
  72. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  73. package/lib/orchestrator/case-extensions.js +28 -0
  74. package/lib/orchestrator/case-extensions.js.map +1 -0
  75. package/lib/orchestrator/config.d.ts.map +1 -1
  76. package/lib/orchestrator/config.js +0 -2
  77. package/lib/orchestrator/config.js.map +1 -1
  78. package/lib/orchestrator/doctor.d.ts.map +1 -1
  79. package/lib/orchestrator/doctor.js +5 -3
  80. package/lib/orchestrator/doctor.js.map +1 -1
  81. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  82. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  83. package/lib/orchestrator/grading.d.ts +2 -1
  84. package/lib/orchestrator/grading.d.ts.map +1 -1
  85. package/lib/orchestrator/grading.js +16 -4
  86. package/lib/orchestrator/grading.js.map +1 -1
  87. package/lib/orchestrator/loader.d.ts.map +1 -1
  88. package/lib/orchestrator/loader.js +2 -0
  89. package/lib/orchestrator/loader.js.map +1 -1
  90. package/lib/orchestrator/orchestrator.d.ts +3 -0
  91. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  92. package/lib/orchestrator/orchestrator.js +42 -14
  93. package/lib/orchestrator/orchestrator.js.map +1 -1
  94. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  95. package/lib/orchestrator/owned-lock.js +52 -38
  96. package/lib/orchestrator/owned-lock.js.map +1 -1
  97. package/lib/orchestrator/regrade.d.ts +6 -0
  98. package/lib/orchestrator/regrade.d.ts.map +1 -1
  99. package/lib/orchestrator/regrade.js +8 -4
  100. package/lib/orchestrator/regrade.js.map +1 -1
  101. package/lib/orchestrator/run-execution.d.ts +2 -1
  102. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  103. package/lib/orchestrator/run-execution.js +3 -2
  104. package/lib/orchestrator/run-execution.js.map +1 -1
  105. package/package.json +1 -2
  106. package/skills/lux/SKILL.md +7 -3
  107. package/skills/lux/references/cli.md +25 -2
  108. package/skills/lux/references/eval-authoring.md +156 -4
  109. package/skills/lux-answerer/SKILL.md +1 -1
  110. package/src/answerers/terminal/index.ts +1 -1
  111. package/src/assertions/authoring.ts +79 -0
  112. package/src/assertions/core/helpers.ts +3 -6
  113. package/src/authoring.ts +1 -0
  114. package/src/cli/program.ts +9 -2
  115. package/src/cli/render/reporter.ts +18 -3
  116. package/src/cli/run-options.ts +1 -0
  117. package/src/core/artifact-evidence.ts +47 -4
  118. package/src/core/assertion.ts +41 -22
  119. package/src/core/case-authoring-state.ts +65 -0
  120. package/src/core/case.ts +57 -6
  121. package/src/core/driver.ts +3 -0
  122. package/src/core/events.ts +1 -0
  123. package/src/core/lifecycle-fixtures.ts +7 -2
  124. package/src/core/lifecycle-hooks.ts +16 -2
  125. package/src/core/wire.ts +5 -1
  126. package/src/drivers/subprocess/index.ts +6 -0
  127. package/src/index.ts +10 -6
  128. package/src/orchestrator/annotation-store.ts +5 -1
  129. package/src/orchestrator/case-extensions.ts +44 -0
  130. package/src/orchestrator/config.ts +0 -2
  131. package/src/orchestrator/doctor.ts +5 -3
  132. package/src/orchestrator/grading.ts +21 -2
  133. package/src/orchestrator/loader.ts +2 -0
  134. package/src/orchestrator/orchestrator.ts +86 -40
  135. package/src/orchestrator/owned-lock.ts +54 -34
  136. package/src/orchestrator/regrade.ts +8 -7
  137. package/src/orchestrator/run-execution.ts +5 -1
  138. package/lib/answerers/scripted/index.d.ts +0 -8
  139. package/lib/answerers/scripted/index.d.ts.map +0 -1
  140. package/lib/answerers/scripted/index.js +0 -163
  141. package/lib/answerers/scripted/index.js.map +0 -1
  142. package/src/answerers/scripted/index.ts +0 -186
@@ -5,6 +5,7 @@ import { glob } from "tinyglobby";
5
5
  import { z } from "zod";
6
6
  import { RubricConfigSchema } from "../assertions/rubric/index.js";
7
7
  import type { EvalCase, Registry } from "../core/index.js";
8
+ import { registryForEvalCase } from "./case-extensions.js";
8
9
  import { buildRegistryFromConfig, loadLuxConfig, type ResolvedLuxConfig } from "./config.js";
9
10
  import { type DiscoveredCase, type DiscoveryResult, discoverCases } from "./discover.js";
10
11
  import { cliIdentity, ExperimentCampaignPlanSchema, planExperimentCampaign } from "./experiment.js";
@@ -559,12 +560,13 @@ const inspectCase = async (
559
560
  ): Promise<CliTarget[]> => {
560
561
  try {
561
562
  const authored = await loadEvalCase(discovered.path);
563
+ const caseRegistry = registryForEvalCase(registry, authored);
562
564
  const drivers = caseDriverReferences(authored.driver, config);
563
565
  const answerer = authored.answerer ?? config.defaultAnswerer;
564
- validateCaseDrivers(discovered.id, drivers, registry, recorder);
565
- validateCaseAnswerer(discovered.id, answerer, registry, recorder);
566
+ validateCaseDrivers(discovered.id, drivers, caseRegistry, recorder);
567
+ validateCaseAnswerer(discovered.id, answerer, caseRegistry, recorder);
566
568
  return [
567
- ...(await validateCaseAssertions(discovered.id, authored, config, registry, recorder)),
569
+ ...(await validateCaseAssertions(discovered.id, authored, config, caseRegistry, recorder)),
568
570
  ...executionTargetsForCase(discovered.id, drivers, answerer),
569
571
  ];
570
572
  } catch (error) {
@@ -76,6 +76,7 @@ export const graderConfigHash = (input: {
76
76
  assertions: readonly AssertionEvaluatorIdentity[];
77
77
  validatedAssertionsHash: string;
78
78
  graderContext?: unknown;
79
+ skipJudge?: boolean;
79
80
  }): string =>
80
81
  valueHash({
81
82
  luxVersion: input.luxVersion,
@@ -83,6 +84,7 @@ export const graderConfigHash = (input: {
83
84
  assertions: input.assertions,
84
85
  validatedAssertionsHash: input.validatedAssertionsHash,
85
86
  graderContext: input.graderContext ?? null,
87
+ skipJudge: input.skipJudge ?? false,
86
88
  });
87
89
 
88
90
  export const validateAssertionPlan = async (
@@ -133,6 +135,7 @@ export const gradeRun = async (
133
135
  complete: (attemptIds: readonly string[]) => void | Promise<void>;
134
136
  },
135
137
  validatedConfigs?: readonly unknown[],
138
+ skipJudge = false,
136
139
  ): Promise<GradingReport> => {
137
140
  const guardedObserver = guardRunObserver(observer);
138
141
  const assertions = run.case.assertions ?? [];
@@ -141,12 +144,27 @@ export const gradeRun = async (
141
144
  }
142
145
 
143
146
  const entries: GradingAssertionEntry[] = [];
147
+ const skippedAssertions: Array<{
148
+ id: string;
149
+ type: string;
150
+ name: string;
151
+ reason: "judge-disabled";
152
+ }> = [];
144
153
  let weightedScoreSum = 0;
145
154
  let weightSum = 0;
146
155
  let allPassed = true;
147
156
  let gradingIncomplete = false;
148
157
 
149
158
  for (const [index, a] of assertions.entries()) {
159
+ if (skipJudge && a.type === "rubric") {
160
+ skippedAssertions.push({
161
+ id: a.id ?? `${a.type}:${index + 1}`,
162
+ type: a.type,
163
+ name: describeAssertion(a),
164
+ reason: "judge-disabled",
165
+ });
166
+ continue;
167
+ }
150
168
  // Grading is the last cancellation point — an LLM judge can run for
151
169
  // minutes per assertion. An aborted grade can't claim everything passed.
152
170
  if (abortSignal?.aborted) {
@@ -247,12 +265,13 @@ export const gradeRun = async (
247
265
  }
248
266
 
249
267
  let caseScore: number | null = null;
250
- if (!gradingIncomplete) caseScore = weightSum > 0 ? weightedScoreSum / weightSum : 0;
268
+ if (!gradingIncomplete && weightSum > 0) caseScore = weightedScoreSum / weightSum;
251
269
  return {
252
270
  schemaVersion: 1,
253
- casePassed: gradingIncomplete ? null : allPassed,
271
+ casePassed: gradingIncomplete || weightSum === 0 ? null : allPassed,
254
272
  caseScore,
255
273
  assertions: entries,
274
+ ...(skippedAssertions.length > 0 ? { skippedAssertions } : {}),
256
275
  ...(gradingIncomplete
257
276
  ? { incomplete: { exitReason: "error" as const, phase: "grading" as const } }
258
277
  : {}),
@@ -1,5 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
+ import { transferEvalCaseExtensions } from "../core/case-authoring-state.js";
3
4
  import { type EvalCase, EvalCaseSchema, formatZodIssues } from "../core/index.js";
4
5
 
5
6
  export const loadEvalCase = async (filePath: string): Promise<EvalCase> => {
@@ -15,5 +16,6 @@ export const loadEvalCase = async (filePath: string): Promise<EvalCase> => {
15
16
  if (!result.success) {
16
17
  throw new Error(`Invalid eval case at ${absolute}: ${formatZodIssues(result.error)}`);
17
18
  }
19
+ transferEvalCaseExtensions(mod.default as EvalCase, result.data);
18
20
  return result.data;
19
21
  };
@@ -18,6 +18,12 @@ import {
18
18
  type SuiteLifecycleHookContext,
19
19
  valueHash,
20
20
  } from "../core/index.js";
21
+ import {
22
+ fixturesForEvalCase,
23
+ mergeLifecycleFixtures,
24
+ registryForEvalCase,
25
+ valuesForFixtures,
26
+ } from "./case-extensions.js";
21
27
  import { evaluatorImplementationHash } from "./evaluator-identity.js";
22
28
  import {
23
29
  assertionEvaluatorIdentities,
@@ -122,6 +128,8 @@ export type RunRequest = {
122
128
  /** Root that relative `fixtures` dir paths resolve against. */
123
129
  fixturesRoot?: string | undefined;
124
130
  answererOverride?: string | undefined;
131
+ /** Skip built-in rubric judge assertions without changing subject execution. */
132
+ skipJudge?: boolean | undefined;
125
133
  startedAt?: Date | undefined;
126
134
  /** The `runSuite` invocation this case attempt belongs to. */
127
135
  suiteId?: string | undefined;
@@ -476,11 +484,6 @@ export class Orchestrator {
476
484
  }
477
485
 
478
486
  async run(request: RunRequest): Promise<RunResult> {
479
- if (this.lifecycleFixtures.length > 0 && !request.suiteId) {
480
- throw new Error(
481
- "Orchestrator.run() cannot execute configured lifecycle fixtures; use runSuite() for a single case",
482
- );
483
- }
484
487
  if (!request.case && !request.casePath) {
485
488
  throw new Error("RunRequest requires either 'case' or 'casePath'");
486
489
  }
@@ -489,11 +492,24 @@ export class Orchestrator {
489
492
  }
490
493
  const casePath = request.casePath ? resolve(request.casePath) : INLINE_CASE_PATH;
491
494
  const authoredCase = request.case ?? (await loadEvalCase(casePath));
495
+ const lifecycleFixtures = mergeLifecycleFixtures(
496
+ this.lifecycleFixtures,
497
+ fixturesForEvalCase(authoredCase),
498
+ );
499
+ if (
500
+ lifecycleFixtures.some(({ setupSuite, setupCase }) => setupSuite || setupCase) &&
501
+ !request.suiteId
502
+ ) {
503
+ throw new Error(
504
+ "Orchestrator.run() cannot execute lifecycle fixtures; use runSuite() for a single case",
505
+ );
506
+ }
507
+ const registry = registryForEvalCase(this.registry, authoredCase);
492
508
  const evalCase = bindSubjectRoot(authoredCase, request.subjectRoot);
493
509
  const caseId = resolveCaseId(casePath, evalCase, request.caseId);
494
510
  const validatedAssertionConfigs = await validateAssertionPlan(
495
511
  evalCase.assertions ?? [],
496
- this.registry,
512
+ registry,
497
513
  );
498
514
 
499
515
  const driverRef = resolveDriverReference(evalCase, this.defaultDriver);
@@ -506,11 +522,11 @@ export class Orchestrator {
506
522
  );
507
523
  const answererName = answererNameOf(answererRef);
508
524
 
509
- const driverPlugin = this.registry.getDriver(driverName);
525
+ const driverPlugin = registry.getDriver(driverName);
510
526
  if (!driverPlugin) {
511
527
  throw new Error(`no driver registered for '${driverName}'`);
512
528
  }
513
- const answererPlugin = this.registry.getAnswerer(answererName);
529
+ const answererPlugin = registry.getAnswerer(answererName);
514
530
  if (!answererPlugin) {
515
531
  throw new Error(`no answerer registered for '${answererName}'`);
516
532
  }
@@ -544,7 +560,7 @@ export class Orchestrator {
544
560
  try {
545
561
  const assertionImplementations = assertionEvaluatorIdentities(
546
562
  authoredCase.assertions ?? [],
547
- this.registry,
563
+ registry,
548
564
  );
549
565
  const validatedAssertionsHash = valueHash(validatedAssertionConfigs);
550
566
  const implementationHash = await this.evaluatorImplementationIdentity();
@@ -554,34 +570,40 @@ export class Orchestrator {
554
570
  assertions: assertionImplementations,
555
571
  validatedAssertionsHash,
556
572
  graderContext: this.harness,
573
+ skipJudge: request.skipJudge ?? false,
557
574
  });
558
- return await this.runWithPlugins({
559
- request,
560
- casePath,
561
- authoredCase,
562
- evalCase,
563
- validatedAssertionConfigs,
564
- caseId,
565
- driverName,
566
- answererName,
567
- answererConfig: answererCfg,
568
- driverConfigHash: valueHash(driverCfg),
569
- answererConfigHash: valueHash(answererCfg),
570
- validatedAssertionsHash,
571
- graderConfigHash: graderHash,
572
- evaluatorConfigHash: valueHash({
573
- luxVersion: LUX_VERSION,
574
- implementationHash,
575
- harness: this.harness,
576
- driver: { name: driverPlugin.name, create: driverPlugin.create },
577
- answerer: { name: answererPlugin.name, create: answererPlugin.create },
578
- assertions: assertionImplementations,
579
- lifecycleFixtures: this.lifecycleFixtures,
575
+ return await this.runWithPlugins(
576
+ {
577
+ request,
578
+ casePath,
579
+ authoredCase,
580
+ evalCase,
581
+ validatedAssertionConfigs,
582
+ caseId,
583
+ driverName,
584
+ answererName,
585
+ answererConfig: answererCfg,
586
+ driverConfigHash: valueHash(driverCfg),
587
+ answererConfigHash: valueHash(answererCfg),
580
588
  validatedAssertionsHash,
581
- }),
582
- driver,
583
- answerer,
584
- });
589
+ graderConfigHash: graderHash,
590
+ evaluatorConfigHash: valueHash({
591
+ luxVersion: LUX_VERSION,
592
+ implementationHash,
593
+ harness: this.harness,
594
+ driver: { name: driverPlugin.name, create: driverPlugin.create },
595
+ answerer: { name: answererPlugin.name, create: answererPlugin.create },
596
+ assertions: assertionImplementations,
597
+ lifecycleFixtures,
598
+ validatedAssertionsHash,
599
+ skipJudge: request.skipJudge ?? false,
600
+ }),
601
+ fixtureValues: valuesForFixtures(lifecycleFixtures),
602
+ driver,
603
+ answerer,
604
+ },
605
+ registry,
606
+ );
585
607
  } finally {
586
608
  await closeQuietly(() => driver.close(), `driver '${driverName}'`);
587
609
  await closeQuietly(() => answerer.close(), `answerer '${answererName}'`);
@@ -599,6 +621,7 @@ export class Orchestrator {
599
621
  runsRoot: string;
600
622
  fixturesRoot?: string | undefined;
601
623
  answererOverride?: string | undefined;
624
+ skipJudge?: boolean | undefined;
602
625
  observer?: SuiteObserver | undefined;
603
626
  /** Aborts the cases in flight and stops the pool pulling new ones. */
604
627
  abortSignal?: AbortSignal | undefined;
@@ -628,6 +651,20 @@ export class Orchestrator {
628
651
  const observer = opts.observer;
629
652
  const requestedConcurrency = validateSuiteConcurrency(opts.concurrency);
630
653
  const concurrency = Math.min(requestedConcurrency, Math.max(items.length, 1));
654
+ const authoredCases = await Promise.allSettled(items.map((item) => loadEvalCase(item.path)));
655
+ const localFixtures = authoredCases.map((result) =>
656
+ result.status === "fulfilled" ? fixturesForEvalCase(result.value) : [],
657
+ );
658
+ const lifecycleFixtures = mergeLifecycleFixtures(this.lifecycleFixtures, ...localFixtures);
659
+ const globalFixtureNames = new Set(this.lifecycleFixtures.map((fixture) => fixture.name));
660
+ const itemIndexesByFixture = new Map<string, number[]>();
661
+ for (const [index, fixtures] of localFixtures.entries()) {
662
+ for (const fixture of fixtures) {
663
+ const indexes = itemIndexesByFixture.get(fixture.name) ?? [];
664
+ indexes.push(index);
665
+ itemIndexesByFixture.set(fixture.name, indexes);
666
+ }
667
+ }
631
668
  const suite = await openSuiteRun({
632
669
  runsRoot: opts.runsRoot,
633
670
  concurrency,
@@ -639,7 +676,7 @@ export class Orchestrator {
639
676
  ...(opts.variantId ? { variantId: opts.variantId } : {}),
640
677
  ...(opts.candidateId ? { candidateId: opts.candidateId } : {}),
641
678
  ...(opts.subjectHash ? { subjectHash: opts.subjectHash } : {}),
642
- fixtures: this.lifecycleFixtures.map(({ name, pluginVersion }) => ({
679
+ fixtures: lifecycleFixtures.map(({ name, pluginVersion }) => ({
643
680
  name,
644
681
  pluginVersion,
645
682
  })),
@@ -677,9 +714,16 @@ export class Orchestrator {
677
714
  if (!opts.abortSignal?.aborted) {
678
715
  try {
679
716
  cleanupSuiteHooks = await setupLifecycleHooks(
680
- this.lifecycleFixtures,
717
+ lifecycleFixtures,
681
718
  "suite",
682
- (hook) => hook.setupSuite?.(hookContext),
719
+ (hook) => {
720
+ if (globalFixtureNames.has(hook.name)) return hook.setupSuite?.(hookContext);
721
+ const indexes = itemIndexesByFixture.get(hook.name) ?? [];
722
+ return hook.setupSuite?.({
723
+ ...hookContext,
724
+ items: indexes.flatMap((index) => hookContext.items[index] ?? []),
725
+ });
726
+ },
683
727
  opts.abortSignal,
684
728
  );
685
729
  } catch (error) {
@@ -718,7 +762,7 @@ export class Orchestrator {
718
762
  let cleanupCaseHooks: () => void | Promise<void>;
719
763
  try {
720
764
  cleanupCaseHooks = await setupLifecycleHooks(
721
- this.lifecycleFixtures,
765
+ mergeLifecycleFixtures(this.lifecycleFixtures, localFixtures[index] ?? []),
722
766
  "case",
723
767
  (hook) =>
724
768
  hook.setupCase?.({
@@ -749,6 +793,7 @@ export class Orchestrator {
749
793
  runsRoot: opts.runsRoot,
750
794
  fixturesRoot: opts.fixturesRoot,
751
795
  answererOverride: opts.answererOverride,
796
+ skipJudge: opts.skipJudge,
752
797
  observer: handle ?? observer,
753
798
  abortSignal: opts.abortSignal,
754
799
  suiteId: suite.manifest.suiteId,
@@ -906,11 +951,12 @@ export class Orchestrator {
906
951
 
907
952
  private runWithPlugins(
908
953
  args: Omit<RunWithPluginsArgs, "luxVersion" | "registry">,
954
+ registry: Registry = this.registry,
909
955
  ): Promise<RunResult> {
910
956
  return executeRunWithPlugins({
911
957
  ...args,
912
958
  luxVersion: LUX_VERSION,
913
- registry: this.registry,
959
+ registry,
914
960
  });
915
961
  }
916
962
  }
@@ -1,8 +1,6 @@
1
- import { randomInt, randomUUID } from "node:crypto";
2
- import { link, lstat, open, readdir, readFile, rm, unlink } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { link, lstat, open, readFile, rm, unlink } from "node:fs/promises";
3
3
  import { hostname } from "node:os";
4
- import { basename, dirname, join } from "node:path";
5
- import { setTimeout as delay } from "node:timers/promises";
6
4
 
7
5
  export type LockRecoveryClaim = {
8
6
  path: string;
@@ -60,36 +58,54 @@ const linkedClaim = (lockPath: string, claimPath: string): LockRecoveryClaim =>
60
58
  release: () => rm(claimPath, { force: true }),
61
59
  });
62
60
 
63
- const recoveryHost = Buffer.from(hostname()).toString("base64url");
61
+ type RecoveryMutexOwner = {
62
+ pid: number;
63
+ hostname: string;
64
+ lockToken: string;
65
+ };
64
66
 
65
- const localClaimProcessIsDead = (prefix: string, name: string): boolean => {
66
- const [claimHost, rawPid, id, extra] = name.slice(prefix.length).split(".");
67
- if (claimHost !== recoveryHost || !rawPid || !id || extra !== undefined) return false;
67
+ const recoveryOwnerIsDead = (owner: RecoveryMutexOwner): boolean => {
68
+ if (owner.hostname !== hostname()) return false;
68
69
  try {
69
- process.kill(Number(rawPid), 0);
70
+ process.kill(owner.pid, 0);
70
71
  return false;
71
72
  } catch (error) {
72
73
  return (error as NodeJS.ErrnoException).code === "ESRCH";
73
74
  }
74
75
  };
75
76
 
76
- const hasCompetingRecoveryClaim = async (lockPath: string, claimPath: string): Promise<boolean> => {
77
- const claim = await lstat(claimPath);
78
- const directory = dirname(lockPath);
79
- const claimName = basename(claimPath);
80
- const prefix = `${basename(lockPath)}.recovering-`;
81
- for (const name of await readdir(directory)) {
82
- if (name === claimName || !name.startsWith(prefix)) continue;
83
- const peerPath = join(directory, name);
84
- const peer = await missingAsNull(lstat(peerPath));
85
- if (!peer || peer.dev !== claim.dev || peer.ino !== claim.ino) continue;
86
- if (localClaimProcessIsDead(prefix, name)) {
87
- await unlinkIfExists(peerPath);
88
- continue;
77
+ const acquireRecoveryMutex = async (
78
+ lockPath: string,
79
+ ): Promise<{ path: string; lockToken: string } | null> => {
80
+ const path = `${lockPath}.recovery`;
81
+ while (true) {
82
+ try {
83
+ const owner = await publishOwnedLock(path, {
84
+ pid: process.pid,
85
+ hostname: hostname(),
86
+ });
87
+ return { path, lockToken: owner.lockToken };
88
+ } catch (error) {
89
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
89
90
  }
90
- return true;
91
+ let owner: RecoveryMutexOwner;
92
+ try {
93
+ const value = JSON.parse(await readFile(path, "utf8")) as Partial<RecoveryMutexOwner>;
94
+ if (
95
+ !Number.isSafeInteger(value.pid) ||
96
+ typeof value.hostname !== "string" ||
97
+ typeof value.lockToken !== "string"
98
+ ) {
99
+ return null;
100
+ }
101
+ owner = value as RecoveryMutexOwner;
102
+ } catch (error) {
103
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
104
+ return null;
105
+ }
106
+ if (!recoveryOwnerIsDead(owner)) return null;
107
+ await releaseOwnedLock(path, owner.lockToken);
91
108
  }
92
- return false;
93
109
  };
94
110
 
95
111
  /**
@@ -97,24 +113,28 @@ const hasCompetingRecoveryClaim = async (lockPath: string, claimPath: string): P
97
113
  * Foreign claims stay conservative because foreign lock owners do too.
98
114
  */
99
115
  export const claimLockForRecovery = async (lockPath: string): Promise<LockRecoveryClaim | null> => {
100
- const claimPath = `${lockPath}.recovering-${recoveryHost}.${process.pid}.${randomUUID()}`;
116
+ const mutex = await acquireRecoveryMutex(lockPath);
117
+ if (!mutex) return null;
118
+ const claimPath = `${lockPath}.recovering-${mutex.lockToken}`;
101
119
  try {
102
120
  await link(lockPath, claimPath);
103
121
  } catch (error) {
104
122
  const code = (error as NodeJS.ErrnoException).code;
123
+ await releaseOwnedLock(mutex.path, mutex.lockToken);
105
124
  if (code === "ENOENT") return null;
106
125
  throw error;
107
126
  }
108
127
  const claim = linkedClaim(lockPath, claimPath);
109
- try {
110
- if (!(await hasCompetingRecoveryClaim(lockPath, claimPath))) return claim;
111
- } catch (error) {
112
- await claim.release();
113
- throw error;
114
- }
115
- await claim.release();
116
- await delay(randomInt(1, 8));
117
- return null;
128
+ return {
129
+ ...claim,
130
+ release: async () => {
131
+ try {
132
+ await claim.release();
133
+ } finally {
134
+ await releaseOwnedLock(mutex.path, mutex.lockToken);
135
+ }
136
+ },
137
+ };
118
138
  };
119
139
 
120
140
  /** Remove only the canonical lock that still carries this acquisition token. */
@@ -33,6 +33,7 @@ import {
33
33
  readRunArtifactBytes,
34
34
  valueHash,
35
35
  } from "../core/index.js";
36
+ import { registryForEvalCase } from "./case-extensions.js";
36
37
  import { evaluatorImplementationHash } from "./evaluator-identity.js";
37
38
  import { parsePersistedJson, readBoundedHandle, safeReadJson } from "./fs-read.js";
38
39
  import {
@@ -526,18 +527,18 @@ export const regradeRun = async (options: {
526
527
  now?: Date;
527
528
  }): Promise<{ path: string; snapshot: GradingSnapshot }> => {
528
529
  const run = await loadFrozenRegradableRun(options.runDir, options.case);
530
+ const registry = options.case
531
+ ? registryForEvalCase(options.registry, options.case)
532
+ : options.registry;
529
533
  try {
530
534
  const evidenceHash = await gradingEvidenceHash(run);
531
535
  const validatedAssertionConfigs = await validateAssertionPlan(
532
536
  run.case.assertions ?? [],
533
- options.registry,
537
+ registry,
534
538
  );
535
539
  const validatedAssertionsHash = valueHash(validatedAssertionConfigs);
536
- const assertionEvaluators = assertionEvaluatorIdentities(
537
- run.case.assertions ?? [],
538
- options.registry,
539
- );
540
- const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], options.registry);
540
+ const assertionEvaluators = assertionEvaluatorIdentities(run.case.assertions ?? [], registry);
541
+ const assertionPlugins = assertionPluginIdentities(run.case.assertions ?? [], registry);
541
542
  const graderHash = graderConfigHash({
542
543
  luxVersion: LUX_VERSION,
543
544
  implementationHash: await currentEvaluatorImplementationHash(),
@@ -547,7 +548,7 @@ export const regradeRun = async (options: {
547
548
  });
548
549
  const report: GradingReport = await gradeRun(
549
550
  run,
550
- options.registry,
551
+ registry,
551
552
  undefined,
552
553
  options.abortSignal,
553
554
  undefined,
@@ -13,6 +13,7 @@ import {
13
13
  type EvalCase,
14
14
  emptyUsage,
15
15
  type FixturesRef,
16
+ type FixtureValueBinding,
16
17
  type GradingReport,
17
18
  guardRunObserver,
18
19
  inspectRunArtifact,
@@ -70,6 +71,7 @@ export type RunWithPluginsArgs = {
70
71
  validatedAssertionsHash: string;
71
72
  graderConfigHash: string;
72
73
  evaluatorConfigHash: string;
74
+ fixtureValues: readonly FixtureValueBinding[];
73
75
  luxVersion: string;
74
76
  registry: Registry;
75
77
  driver: AgentDriver;
@@ -360,6 +362,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
360
362
  caseId: this.args.caseId,
361
363
  prompt: evalCase.prompt,
362
364
  fixtures: staged?.fixtures,
365
+ ...(this.args.fixtureValues.length > 0 ? { fixtureValues: this.args.fixtureValues } : {}),
363
366
  runsRoot: resolve(request.runsRoot),
364
367
  runDir: this.runDir.path,
365
368
  artifactsDir: this.runDir.artifactsDir,
@@ -557,7 +560,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
557
560
  const outcome = this.require(this.loopOutcome, "driver outcome");
558
561
  const agentEndedAt = new Date();
559
562
  const exitReason = agentExitReason(outcome, this.collection.error);
560
- const summary = outcome.kind === "done" ? outcome.done.summary : undefined;
563
+ const summary = outcome.kind === "done" ? outcome.done.summary : outcome.error.summary;
561
564
  const driverCost = costFromSummary(summary);
562
565
  const reportedModelAccounting = modelAccountingFromSummary(summary);
563
566
  const answererAccounting = readAnswererModelAccounting(this.args.answerer);
@@ -654,6 +657,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
654
657
  request.recordGradingAssertion,
655
658
  modelAttempts,
656
659
  this.args.validatedAssertionConfigs,
660
+ request.skipJudge ?? false,
657
661
  );
658
662
  }
659
663
 
@@ -1,8 +0,0 @@
1
- import { z } from "zod";
2
- import { type AnswererPlugin } from "../../core/index.js";
3
- export declare const ScriptedAnswererConfigSchema: z.ZodObject<{
4
- fixturePath: z.ZodString;
5
- }, z.core.$strict>;
6
- export type ScriptedAnswererConfig = z.infer<typeof ScriptedAnswererConfigSchema>;
7
- export declare const scriptedAnswerer: AnswererPlugin<ScriptedAnswererConfig>;
8
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/answerers/scripted/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAGL,KAAK,cAAc,EAIpB,MAAM,qBAAqB,CAAC;AA8B7B,eAAO,MAAM,4BAA4B;;kBAAiD,CAAC;AAE3F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AA0IlF,eAAO,MAAM,gBAAgB,EAAE,cAAc,CAAC,sBAAsB,CAInE,CAAC"}