@prismatic-io/lux 0.0.2-preview.36 → 0.0.2-preview.38

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 (106) hide show
  1. package/lib/assertions/core/command-exits-zero.d.ts +2 -2
  2. package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
  3. package/lib/assertions/core/command-exits-zero.js +12 -10
  4. package/lib/assertions/core/command-exits-zero.js.map +1 -1
  5. package/lib/assertions/rubric/evidence.d.ts +88 -0
  6. package/lib/assertions/rubric/evidence.d.ts.map +1 -0
  7. package/lib/assertions/rubric/evidence.js +66 -0
  8. package/lib/assertions/rubric/evidence.js.map +1 -0
  9. package/lib/assertions/rubric/index.d.ts +72 -43
  10. package/lib/assertions/rubric/index.d.ts.map +1 -1
  11. package/lib/assertions/rubric/index.js +10 -49
  12. package/lib/assertions/rubric/index.js.map +1 -1
  13. package/lib/assertions/rubric/internal.d.ts +1 -1
  14. package/lib/assertions/rubric/internal.d.ts.map +1 -1
  15. package/lib/assertions/rubric/internal.js +4 -4
  16. package/lib/assertions/rubric/internal.js.map +1 -1
  17. package/lib/authoring.d.ts +1 -0
  18. package/lib/authoring.d.ts.map +1 -1
  19. package/lib/authoring.js.map +1 -1
  20. package/lib/cli/harness-plan.d.ts +1 -0
  21. package/lib/cli/harness-plan.d.ts.map +1 -1
  22. package/lib/cli/harness-plan.js +1 -1
  23. package/lib/cli/harness-plan.js.map +1 -1
  24. package/lib/cli/output-schemas.d.ts +1 -1
  25. package/lib/cli/program.d.ts.map +1 -1
  26. package/lib/cli/program.js +8 -4
  27. package/lib/cli/program.js.map +1 -1
  28. package/lib/cli/render/reporter.d.ts.map +1 -1
  29. package/lib/cli/render/reporter.js +1 -1
  30. package/lib/cli/render/reporter.js.map +1 -1
  31. package/lib/core/assertion.d.ts +2 -2
  32. package/lib/core/assertion.d.ts.map +1 -1
  33. package/lib/core/assertion.js +2 -1
  34. package/lib/core/assertion.js.map +1 -1
  35. package/lib/core/case-defaults.d.ts +16 -0
  36. package/lib/core/case-defaults.d.ts.map +1 -0
  37. package/lib/core/case-defaults.js +42 -0
  38. package/lib/core/case-defaults.js.map +1 -0
  39. package/lib/core/case.d.ts +8 -3
  40. package/lib/core/case.d.ts.map +1 -1
  41. package/lib/core/case.js +33 -4
  42. package/lib/core/case.js.map +1 -1
  43. package/lib/core/reference-defaults.d.ts +5 -0
  44. package/lib/core/reference-defaults.d.ts.map +1 -0
  45. package/lib/core/reference-defaults.js +42 -0
  46. package/lib/core/reference-defaults.js.map +1 -0
  47. package/lib/index.d.ts +2 -1
  48. package/lib/index.d.ts.map +1 -1
  49. package/lib/index.js.map +1 -1
  50. package/lib/orchestrator/config.d.ts +1 -0
  51. package/lib/orchestrator/config.d.ts.map +1 -1
  52. package/lib/orchestrator/config.js +3 -0
  53. package/lib/orchestrator/config.js.map +1 -1
  54. package/lib/orchestrator/doctor.d.ts.map +1 -1
  55. package/lib/orchestrator/doctor.js +19 -42
  56. package/lib/orchestrator/doctor.js.map +1 -1
  57. package/lib/orchestrator/experiment-context.d.ts.map +1 -1
  58. package/lib/orchestrator/experiment-context.js +2 -1
  59. package/lib/orchestrator/experiment-context.js.map +1 -1
  60. package/lib/orchestrator/experiment-corpus.d.ts.map +1 -1
  61. package/lib/orchestrator/experiment-corpus.js +1 -1
  62. package/lib/orchestrator/experiment-corpus.js.map +1 -1
  63. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  64. package/lib/orchestrator/experiment-evaluation.js +1 -0
  65. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  66. package/lib/orchestrator/experiment-identity.d.ts.map +1 -1
  67. package/lib/orchestrator/experiment-identity.js +3 -3
  68. package/lib/orchestrator/experiment-identity.js.map +1 -1
  69. package/lib/orchestrator/grader-experiment.d.ts +2 -2
  70. package/lib/orchestrator/grading.d.ts.map +1 -1
  71. package/lib/orchestrator/grading.js +2 -4
  72. package/lib/orchestrator/grading.js.map +1 -1
  73. package/lib/orchestrator/orchestrator.d.ts +3 -1
  74. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  75. package/lib/orchestrator/orchestrator.js +18 -19
  76. package/lib/orchestrator/orchestrator.js.map +1 -1
  77. package/lib/orchestrator/profile.d.ts +4 -0
  78. package/lib/orchestrator/profile.d.ts.map +1 -0
  79. package/lib/orchestrator/profile.js +10 -0
  80. package/lib/orchestrator/profile.js.map +1 -0
  81. package/lib/orchestrator/regrade.d.ts +1 -1
  82. package/package.json +1 -1
  83. package/skills/lux-answerer/SKILL.md +1 -1
  84. package/src/assertions/core/command-exits-zero.ts +33 -22
  85. package/src/assertions/rubric/evidence.ts +76 -0
  86. package/src/assertions/rubric/index.ts +10 -57
  87. package/src/assertions/rubric/internal.ts +7 -6
  88. package/src/authoring.ts +1 -0
  89. package/src/cli/harness-plan.ts +7 -1
  90. package/src/cli/program.ts +8 -4
  91. package/src/cli/render/reporter.ts +2 -1
  92. package/src/core/assertion.ts +2 -1
  93. package/src/core/case-defaults.ts +65 -0
  94. package/src/core/case.ts +53 -6
  95. package/src/core/reference-defaults.ts +47 -0
  96. package/src/index.ts +2 -1
  97. package/src/orchestrator/config.ts +3 -0
  98. package/src/orchestrator/doctor.ts +30 -45
  99. package/src/orchestrator/experiment-context.ts +2 -1
  100. package/src/orchestrator/experiment-corpus.ts +6 -1
  101. package/src/orchestrator/experiment-evaluation.ts +1 -0
  102. package/src/orchestrator/experiment-identity.ts +18 -3
  103. package/src/orchestrator/grading.ts +2 -4
  104. package/src/orchestrator/orchestrator.ts +28 -17
  105. package/src/orchestrator/profile.ts +24 -0
  106. package/viewer/detail.js +16 -5
@@ -0,0 +1,47 @@
1
+ import type { AnswererRef, DriverRef } from "./case.js";
2
+ import { mergePluginConfig } from "./harness-matrix.js";
3
+
4
+ export const withDriverDefaults = (
5
+ reference: DriverRef | undefined,
6
+ defaults: Record<string, unknown> | undefined,
7
+ ): DriverRef | undefined => {
8
+ if (!reference || !defaults) return reference;
9
+ const selected = typeof reference === "string" ? { name: reference } : reference;
10
+ return { ...selected, config: mergePluginConfig(defaults, selected.config) };
11
+ };
12
+
13
+ export const mergeDriverReference = (
14
+ base: DriverRef | undefined,
15
+ local: DriverRef | undefined,
16
+ ): DriverRef | undefined => {
17
+ if (local === undefined) return base;
18
+ if (base === undefined) return local;
19
+ const first = typeof base === "string" ? { name: base } : base;
20
+ const second = typeof local === "string" ? { name: local } : local;
21
+ if (first.name !== second.name) return local;
22
+ return {
23
+ name: second.name,
24
+ config: mergePluginConfig(first.config, second.config),
25
+ ...((second.execution ?? first.execution)
26
+ ? { execution: second.execution ?? first.execution }
27
+ : {}),
28
+ };
29
+ };
30
+
31
+ export const mergeAnswererReference = (
32
+ base: AnswererRef | undefined,
33
+ local: AnswererRef | undefined,
34
+ ): AnswererRef | undefined => {
35
+ const merged = mergeDriverReference(base, local);
36
+ if (typeof merged !== "object" || merged.name !== "persona") return merged;
37
+ const first = typeof base === "object" ? base.config : undefined;
38
+ const second = typeof local === "object" ? local.config : undefined;
39
+ if (!first || !second || typeof first !== "object" || typeof second !== "object") return merged;
40
+ if (!("provider" in second) || ("provider" in first && second.provider === first.provider))
41
+ return merged;
42
+ const config = { ...(merged.config as Record<string, unknown>) };
43
+ for (const key of ["model", "command", "reasoningEffort"]) {
44
+ if (!(key in second)) delete config[key];
45
+ }
46
+ return { name: merged.name, config };
47
+ };
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ export { DurationSchema, durationMs } from "./core/duration.js";
4
4
  // lifecycles. Explicit exports keep implementation and test helpers private.
5
5
 
6
6
  export { z } from "zod";
7
-
8
7
  // Bundled answerers and drivers. Test injection and protocol-normalization
9
8
  // helpers stay in their implementation modules.
10
9
  export {
@@ -140,6 +139,7 @@ export {
140
139
  type RubricConfig,
141
140
  RubricConfigSchema,
142
141
  type RubricEvidence,
142
+ type RubricEvidenceInput,
143
143
  RubricEvidenceSchema,
144
144
  RubricModelAccountingDetailSchema,
145
145
  rubricAssertion,
@@ -237,6 +237,7 @@ export {
237
237
  type PersistedCase,
238
238
  PersistedCaseSchema,
239
239
  } from "./core/case.js";
240
+ export type { AssertionDefaults, EvalCaseDefaults } from "./core/case-defaults.js";
240
241
  export {
241
242
  CONVERSATION_TURN_REQUEST,
242
243
  type ConversationTurnRequest,
@@ -84,6 +84,8 @@ export const LuxConfigSchema = z
84
84
  subjectRoot: z.string().optional(),
85
85
  defaultAnswerer: AnswererRefSchema.default("persona"),
86
86
  defaultDriver: DriverRefSchema.optional(),
87
+ /** Configuration inherited by every selected driver, then validated by that plugin. */
88
+ driverDefaults: z.record(z.string(), z.unknown()).optional(),
87
89
  /** Driver, model, and effort permutations every case runs through by default. */
88
90
  matrix: HarnessMatrixSchema.optional(),
89
91
  /** Shared provider/model defaults for persona answering and rubric judging. */
@@ -172,6 +174,7 @@ export const loadLuxConfig = async (cwd: string = process.cwd()): Promise<Resolv
172
174
  ...(validated.fixturesRoot ? { fixturesRoot: resolve(root, validated.fixturesRoot) } : {}),
173
175
  defaultAnswerer: validated.defaultAnswerer,
174
176
  ...(validated.defaultDriver ? { defaultDriver: validated.defaultDriver } : {}),
177
+ ...(validated.driverDefaults ? { driverDefaults: validated.driverDefaults } : {}),
175
178
  ...(validated.matrix ? { matrix: validated.matrix } : {}),
176
179
  ...(validated.harness ? { harness: validated.harness } : {}),
177
180
  concurrency: validated.concurrency,
@@ -7,6 +7,7 @@ import { RubricConfigSchema } from "../assertions/rubric/index.js";
7
7
  import { driverExecutionOf } from "../core/driver-execution.js";
8
8
  import {
9
9
  casePermutations,
10
+ type DriverRef,
10
11
  type EvalCase,
11
12
  expandHarnessMatrix,
12
13
  type HarnessPermutation,
@@ -14,6 +15,7 @@ import {
14
15
  permutationId,
15
16
  type Registry,
16
17
  } from "../core/index.js";
18
+ import { mergeDriverReference, withDriverDefaults } from "../core/reference-defaults.js";
17
19
  import { registryForEvalCase } from "./case-extensions.js";
18
20
  import { buildRegistryFromConfig, loadLuxConfig, type ResolvedLuxConfig } from "./config.js";
19
21
  import { type DiscoveredCase, type DiscoveryResult, discoverCases } from "./discover.js";
@@ -33,6 +35,8 @@ import {
33
35
  hasHarnessCatalogDrift,
34
36
  } from "./harness-catalog-drift.js";
35
37
  import { loadEvalCase } from "./loader.js";
38
+ import { resolveAnswererReference } from "./orchestrator.js";
39
+ import { resolveProfile } from "./profile.js";
36
40
  import { validatePluginConfig, validatePluginConfigAsync } from "./validate-plugin-config.js";
37
41
 
38
42
  export const DoctorDiagnosticSchema = z
@@ -120,42 +124,17 @@ const rawRefConfig = (reference: unknown): unknown =>
120
124
  ? (reference as { config?: unknown }).config
121
125
  : undefined;
122
126
 
123
- const mergeRecords = (base: unknown, local: unknown): unknown => {
124
- if (
125
- !base ||
126
- typeof base !== "object" ||
127
- Array.isArray(base) ||
128
- !local ||
129
- typeof local !== "object" ||
130
- Array.isArray(local)
131
- ) {
132
- return local ?? base;
133
- }
134
- const merged: Record<string, unknown> = { ...(base as Record<string, unknown>) };
135
- for (const [key, value] of Object.entries(local as Record<string, unknown>)) {
136
- merged[key] = key in merged ? mergeRecords(merged[key], value) : value;
137
- }
138
- return merged;
139
- };
140
-
141
- const effectiveDriverReference = (local: unknown, fallback: unknown): unknown => {
142
- if (!local) return fallback;
143
- if (!fallback || refName(local) !== refName(fallback)) return local;
144
- return {
145
- name: refName(local),
146
- config: mergeRecords(rawRefConfig(fallback), rawRefConfig(local)),
147
- execution: driverExecutionOf(local) ?? driverExecutionOf(fallback),
148
- };
149
- };
150
-
151
127
  const boundPermutation = (
152
128
  permutation: HarnessPermutation,
153
129
  config: ResolvedLuxConfig,
154
130
  registry: Registry,
155
- ): unknown => {
131
+ ): DriverRef => {
156
132
  const plugin = registry.getDriver(permutation.driver);
157
133
  if (!plugin) throw new Error(`unknown driver '${permutation.driver}'`);
158
- return permutationDriverRef(permutation, config.defaultDriver, plugin);
134
+ return withDriverDefaults(
135
+ permutationDriverRef(permutation, config.defaultDriver, plugin),
136
+ config.driverDefaults,
137
+ ) as DriverRef;
159
138
  };
160
139
 
161
140
  const caseDriverReferences = (
@@ -167,16 +146,20 @@ const caseDriverReferences = (
167
146
  const permutations = casePermutations(authored, config.matrix);
168
147
  if (permutations.length > 0) {
169
148
  return permutations.map((permutation) =>
170
- effectiveDriverReference(local, boundPermutation(permutation, config, registry)),
149
+ mergeDriverReference(boundPermutation(permutation, config, registry), local),
171
150
  );
172
151
  }
173
152
  const fallbacks = [
174
- config.defaultDriver,
175
- ...Object.values(config.profiles ?? {}).map((profile) => profile.driver),
153
+ resolveProfile(config).driver,
154
+ ...Object.values(config.profiles ?? {}).map(
155
+ (profile) => resolveProfile(config, profile).driver,
156
+ ),
176
157
  ].filter((reference) => reference !== undefined);
177
158
  if (!local) return [...new Set(fallbacks)];
178
- if (fallbacks.length === 0) return [local];
179
- const references = fallbacks.map((fallback) => effectiveDriverReference(local, fallback));
159
+ if (fallbacks.length === 0) return [withDriverDefaults(local, config.driverDefaults)];
160
+ const references = fallbacks.map((fallback) =>
161
+ withDriverDefaults(mergeDriverReference(fallback, local), config.driverDefaults),
162
+ );
180
163
  return [
181
164
  ...new Map(references.map((reference) => [JSON.stringify(reference), reference])).values(),
182
165
  ];
@@ -210,7 +193,9 @@ const rubricCommands = (assertion: unknown, config: ResolvedLuxConfig): string[]
210
193
  const rubric = RubricConfigSchema.parse(assertion);
211
194
  const harnesses = [
212
195
  config.harness,
213
- ...Object.values(config.profiles ?? {}).map((profile) => profile.harness ?? config.harness),
196
+ ...Object.values(config.profiles ?? {}).map(
197
+ (profile) => resolveProfile(config, profile).harness,
198
+ ),
214
199
  ];
215
200
  const commands = harnesses.map((harness) => {
216
201
  const provider = rubric.provider ?? harness?.provider ?? "claude-code";
@@ -227,7 +212,7 @@ const configuredCliTargets = (config: ResolvedLuxConfig, registry: Registry): Cl
227
212
  const add = (label: string, command: string | undefined): void => {
228
213
  if (command) targets.push({ label, command });
229
214
  };
230
- for (const command of driverCommands(config.defaultDriver, registry))
215
+ for (const command of driverCommands(resolveProfile(config).driver, registry))
231
216
  add("default driver", command);
232
217
  for (const permutation of expandHarnessMatrix(config.matrix ?? [])) {
233
218
  try {
@@ -250,11 +235,11 @@ const configuredCliTargets = (config: ResolvedLuxConfig, registry: Registry): Cl
250
235
  for (const [id, profile] of Object.entries(config.profiles ?? {}).sort(([a], [b]) =>
251
236
  a.localeCompare(b),
252
237
  )) {
253
- for (const command of driverCommands(profile.driver ?? config.defaultDriver, registry)) {
238
+ for (const command of driverCommands(resolveProfile(config, profile).driver, registry)) {
254
239
  add(`profile '${id}' driver`, command);
255
240
  }
256
- add(`profile '${id}' answerer`, answererCommand(profile.answerer ?? config.defaultAnswerer));
257
- const harness = profile.harness ?? config.harness;
241
+ add(`profile '${id}' answerer`, answererCommand(resolveProfile(config, profile).answerer));
242
+ const harness = resolveProfile(config, profile).harness;
258
243
  if (harness) {
259
244
  add(
260
245
  `profile '${id}' harness`,
@@ -356,18 +341,18 @@ const loadDoctorRegistry = (
356
341
  const configuredDriverReferences = (
357
342
  config: ResolvedLuxConfig,
358
343
  ): Array<readonly [string, unknown]> => [
359
- ["default", config.defaultDriver],
344
+ ["default", resolveProfile(config).driver],
360
345
  ...Object.entries(config.profiles ?? {}).map(
361
- ([id, profile]) => [`profile '${id}'`, profile.driver] as const,
346
+ ([id, profile]) => [`profile '${id}'`, resolveProfile(config, profile).driver] as const,
362
347
  ),
363
348
  ];
364
349
 
365
350
  const configuredAnswererReferences = (
366
351
  config: ResolvedLuxConfig,
367
352
  ): Array<readonly [string, unknown]> => [
368
- ["default", config.defaultAnswerer],
353
+ ["default", resolveProfile(config).answerer],
369
354
  ...Object.entries(config.profiles ?? {}).map(
370
- ([id, profile]) => [`profile '${id}'`, profile.answerer] as const,
355
+ ([id, profile]) => [`profile '${id}'`, resolveProfile(config, profile).answerer] as const,
371
356
  ),
372
357
  ];
373
358
 
@@ -651,7 +636,7 @@ const inspectCase = async (
651
636
  const authored = await loadEvalCase(discovered.path);
652
637
  const caseRegistry = registryForEvalCase(registry, authored);
653
638
  const drivers = caseDriverReferences(authored, config, caseRegistry);
654
- const answerer = authored.answerer ?? config.defaultAnswerer;
639
+ const answerer = resolveAnswererReference(authored, config.defaultAnswerer, config.harness);
655
640
  validateCaseDrivers(discovered.id, drivers, caseRegistry, recorder, authored.requires);
656
641
  validateCaseAnswerer(discovered.id, answerer, caseRegistry, recorder);
657
642
  return [
@@ -2,6 +2,7 @@ import type { Registry } from "../core/index.js";
2
2
  import type { ResolvedLuxConfig } from "./config.js";
3
3
  import type { discoverCases } from "./discover.js";
4
4
  import type { LoadedExperimentCampaign } from "./experiment-loader.js";
5
+ import { resolveProfile } from "./profile.js";
5
6
 
6
7
  export type CorpusSelection =
7
8
  | {
@@ -38,5 +39,5 @@ export const profileOrThrow = (config: ResolvedLuxConfig, profileId: string) =>
38
39
  `experiment profile '${profileId}' is not configured${available.length > 0 ? `; available: ${available.join(", ")}` : ""}`,
39
40
  );
40
41
  }
41
- return profile;
42
+ return resolveProfile(config, profile);
42
43
  };
@@ -131,7 +131,12 @@ export const agentCorpusHash = async (
131
131
  const profile = profileOrThrow(execution.config, profileId);
132
132
  const harness = profile.harness ?? execution.config.harness;
133
133
  const driver = bindSubjectRoot(
134
- resolveDriverReference(bound, profile.driver ?? execution.config.defaultDriver),
134
+ resolveDriverReference(
135
+ bound,
136
+ profile.driver ?? execution.config.defaultDriver,
137
+ undefined,
138
+ execution.config.driverDefaults,
139
+ ),
135
140
  execution.loaded.subjectRoot,
136
141
  );
137
142
  const answerer = bindSubjectRoot(
@@ -512,6 +512,7 @@ export const evaluateAgentCandidate = async (options: {
512
512
  const defaultDriver = profile.driver ?? execution.config.defaultDriver;
513
513
  const registry = registryWithHarness(execution.registry, execution.config, harness);
514
514
  const orchestrator = new Orchestrator({
515
+ driverDefaults: execution.config.driverDefaults,
515
516
  environment: profile.environment ?? execution.config.environment,
516
517
  registry,
517
518
  defaultAnswerer: profile.answerer ?? execution.config.defaultAnswerer,
@@ -132,7 +132,12 @@ const trustedExecutableEntrypoints = async (
132
132
  for (const profileId of execution.loaded.campaign.profiles) {
133
133
  const profile = profileOrThrow(execution.config, profileId);
134
134
  const driver = bindSubjectRoot(
135
- resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
135
+ resolveDriverReference(
136
+ authored,
137
+ profile.driver ?? execution.config.defaultDriver,
138
+ undefined,
139
+ execution.config.driverDefaults,
140
+ ),
136
141
  execution.loaded.subjectRoot,
137
142
  );
138
143
  for (const command of referencedDriverRuntime(driver, execution.registry).commands)
@@ -198,7 +203,12 @@ export const runtimeCliReferences = async (
198
203
  const profile = profileOrThrow(execution.config, profileId);
199
204
  const harness = profile.harness ?? execution.config.harness;
200
205
  const driver = bindSubjectRoot(
201
- resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
206
+ resolveDriverReference(
207
+ authored,
208
+ profile.driver ?? execution.config.defaultDriver,
209
+ undefined,
210
+ execution.config.driverDefaults,
211
+ ),
202
212
  execution.loaded.subjectRoot,
203
213
  );
204
214
  const answerer = bindSubjectRoot(
@@ -275,7 +285,12 @@ export const validateAgentCaseProfileDrivers = async (
275
285
  for (const profileId of execution.loaded.campaign.profiles) {
276
286
  const profile = profileOrThrow(execution.config, profileId);
277
287
  const reference = bindSubjectRoot(
278
- resolveDriverReference(authored, profile.driver ?? execution.config.defaultDriver),
288
+ resolveDriverReference(
289
+ authored,
290
+ profile.driver ?? execution.config.defaultDriver,
291
+ undefined,
292
+ execution.config.driverDefaults,
293
+ ),
279
294
  execution.loaded.subjectRoot,
280
295
  );
281
296
  const name = typeof reference === "string" ? reference : reference.name;
@@ -273,10 +273,9 @@ export const gradeRun = async (
273
273
  type: a.type,
274
274
  name,
275
275
  passed: a.informational ? null : passed,
276
- ...(a.informational ? { informational: true } : {}),
276
+ ...(a.informational ? { informational: true } : { threshold }),
277
277
  score: result.score,
278
278
  weight,
279
- threshold,
280
279
  durationMs: Date.now() - startedAt,
281
280
  ...(result.comment !== undefined ? { comment: result.comment } : {}),
282
281
  detail: result.detail,
@@ -291,10 +290,9 @@ export const gradeRun = async (
291
290
  type: a.type,
292
291
  name,
293
292
  passed: a.informational ? null : false,
294
- ...(a.informational ? { informational: true } : {}),
293
+ ...(a.informational ? { informational: true } : { threshold }),
295
294
  score: 0,
296
295
  weight,
297
- threshold,
298
296
  durationMs: Date.now() - startedAt,
299
297
  errorMessage: message,
300
298
  };
@@ -17,13 +17,17 @@ import {
17
17
  type HarnessModelConfig,
18
18
  type LifecycleHooksPlugin,
19
19
  mergeHarnessModelConfig,
20
- mergePluginConfig,
21
20
  type Registry,
22
21
  type RunMetadata,
23
22
  type RunObserver,
24
23
  type SuiteLifecycleHookContext,
25
24
  valueHash,
26
25
  } from "../core/index.js";
26
+ import {
27
+ mergeAnswererReference,
28
+ mergeDriverReference,
29
+ withDriverDefaults,
30
+ } from "../core/reference-defaults.js";
27
31
  import {
28
32
  fixturesForEvalCase,
29
33
  mergeLifecycleFixtures,
@@ -116,6 +120,7 @@ export type OrchestratorOptions = {
116
120
  defaultAnswerer?: AnswererRef;
117
121
  /** Driver used when a case names none. */
118
122
  defaultDriver?: DriverRef;
123
+ driverDefaults?: Record<string, unknown> | undefined;
119
124
  /** Shared provider/model defaults for built-in harness components. */
120
125
  harness?: HarnessModelConfig;
121
126
  /** Versioned project resource fixtures. */
@@ -233,6 +238,7 @@ export const resolveDriverReference = (
233
238
  evalCase: EvalCase,
234
239
  fallback: DriverRef | undefined,
235
240
  permutation?: DriverRef | undefined,
241
+ defaults?: Record<string, unknown> | undefined,
236
242
  ): DriverRef => {
237
243
  const local = evalCase.driver;
238
244
  if (permutation && local && driverNameOf(local) !== driverNameOf(permutation)) {
@@ -240,20 +246,12 @@ export const resolveDriverReference = (
240
246
  `case is pinned to driver '${driverNameOf(local)}' but the run selected '${driverNameOf(permutation)}'`,
241
247
  );
242
248
  }
243
- const base = permutation ?? fallback;
244
- if (!local) {
245
- if (!base) throw new Error("case names no driver and no defaultDriver is configured");
246
- return base;
247
- }
248
- if (!base || driverNameOf(local) !== driverNameOf(base)) return local;
249
-
250
- // Naming the same driver opts into project defaults. A case object only
251
- // needs to carry the fields it wants to override (for example a timeout).
252
- return {
253
- name: driverNameOf(local),
254
- config: mergePluginConfig(driverConfigOf(base), driverConfigOf(local)),
255
- execution: driverExecutionOf(local) ?? driverExecutionOf(base),
256
- };
249
+ const reference = withDriverDefaults(
250
+ mergeDriverReference(permutation ?? fallback, local),
251
+ defaults,
252
+ );
253
+ if (!reference) throw new Error("case names no driver and no defaultDriver is configured");
254
+ return reference;
257
255
  };
258
256
 
259
257
  export const resolveAnswererReference = (
@@ -262,7 +260,18 @@ export const resolveAnswererReference = (
262
260
  harness: HarnessModelConfig | undefined,
263
261
  override?: string,
264
262
  ): AnswererRef => {
265
- const ref = override ?? evalCase.answerer ?? fallback;
263
+ const fallbackName = typeof fallback === "string" ? fallback : fallback.name;
264
+ const inherited =
265
+ harness && fallbackName === "persona"
266
+ ? {
267
+ name: "persona",
268
+ config: mergeHarnessModelConfig(
269
+ harness,
270
+ typeof fallback === "object" ? fallback.config : undefined,
271
+ ),
272
+ }
273
+ : fallback;
274
+ const ref = mergeAnswererReference(inherited, override ?? evalCase.answerer) as AnswererRef;
266
275
  const name = typeof ref === "string" ? ref : ref.name;
267
276
  if (!harness || name !== "persona") return ref;
268
277
  return {
@@ -310,6 +319,7 @@ export class Orchestrator {
310
319
  readonly registry: Registry;
311
320
  private readonly defaultAnswerer: AnswererRef;
312
321
  private readonly defaultDriver: DriverRef | undefined;
322
+ private readonly driverDefaults: Record<string, unknown> | undefined;
313
323
  private readonly harness: HarnessModelConfig | undefined;
314
324
  private readonly lifecycleFixtures: readonly LifecycleHooksPlugin[];
315
325
 
@@ -320,6 +330,7 @@ export class Orchestrator {
320
330
  this.registry = options.registry;
321
331
  this.defaultAnswerer = options.defaultAnswerer ?? "persona";
322
332
  this.defaultDriver = options.defaultDriver;
333
+ this.driverDefaults = options.driverDefaults;
323
334
  this.harness = options.harness;
324
335
  this.lifecycleFixtures = [...(options.lifecycleFixtures ?? [])];
325
336
  }
@@ -359,7 +370,7 @@ export class Orchestrator {
359
370
  );
360
371
 
361
372
  const driverRef = bindSubjectRoot(
362
- resolveDriverReference(evalCase, this.defaultDriver, request.driver),
373
+ resolveDriverReference(evalCase, this.defaultDriver, request.driver, this.driverDefaults),
363
374
  request.subjectRoot,
364
375
  );
365
376
  const driverName = driverNameOf(driverRef);
@@ -0,0 +1,24 @@
1
+ import {
2
+ mergeAnswererReference,
3
+ mergeDriverReference,
4
+ withDriverDefaults,
5
+ } from "../core/reference-defaults.js";
6
+ import type { ExperimentProfile, ResolvedLuxConfig } from "./config.js";
7
+
8
+ /** Used by ordinary runs, doctor, and experiment execution and identity alike. */
9
+ export const resolveProfile = (
10
+ config: Pick<
11
+ ResolvedLuxConfig,
12
+ "defaultDriver" | "defaultAnswerer" | "harness" | "environment" | "driverDefaults"
13
+ >,
14
+ profile: ExperimentProfile = {},
15
+ ): ExperimentProfile => ({
16
+ ...profile,
17
+ driver: withDriverDefaults(
18
+ mergeDriverReference(config.defaultDriver, profile.driver),
19
+ config.driverDefaults,
20
+ ),
21
+ answerer: mergeAnswererReference(config.defaultAnswerer, profile.answerer),
22
+ harness: profile.harness ?? config.harness,
23
+ environment: profile.environment ?? config.environment,
24
+ });
package/viewer/detail.js CHANGED
@@ -96,10 +96,21 @@ const eventTime = (event, startedAt) => {
96
96
  return `+${duration(Math.max(0, event.ts - start))}`;
97
97
  };
98
98
 
99
+ const assertionScoreLabel = (assertion) => {
100
+ const label = `${assertion.name} scored ${percentage(assertionScore(assertion))}`;
101
+ if (assertion.informational || assertion.passed === null) return `${label} (informational)`;
102
+ if (typeof assertion.threshold === "number") {
103
+ return `${label}; threshold ${percentage(assertion.threshold)}`;
104
+ }
105
+ return label;
106
+ };
107
+
99
108
  const assertionMeta = (assertion) =>
100
109
  [
101
110
  assertion.type,
102
- `threshold ${finite(assertion.threshold)}`,
111
+ !assertion.informational && assertion.passed !== null && typeof assertion.threshold === "number"
112
+ ? `threshold ${finite(assertion.threshold)}`
113
+ : null,
103
114
  `weight ${finite(assertion.weight)}`,
104
115
  duration(assertion.durationMs),
105
116
  assertion.usage ? `${compactNumber(totalTokens(assertion.usage))} judge tokens` : null,
@@ -334,10 +345,10 @@ export const renderRunDetail = (item, detail) => {
334
345
  ${scoreBar(
335
346
  assertionScore(assertion),
336
347
  assertionPresentation(assertion).tone,
337
- `${assertion.name} scored ${percentage(
338
- assertionScore(assertion),
339
- )}${assertion.passed === null ? " (informational)" : `; threshold ${percentage(assertion.threshold)}`}`,
340
- assertion.passed === null ? null : assertion.threshold,
348
+ assertionScoreLabel(assertion),
349
+ assertion.informational || assertion.passed === null
350
+ ? null
351
+ : assertion.threshold,
341
352
  )}
342
353
  ${assertionDiagnostics(assertion)}
343
354
  </span>