@prismatic-io/lux 0.0.2-preview.37 → 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 (89) 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/program.d.ts.map +1 -1
  25. package/lib/cli/program.js +8 -4
  26. package/lib/cli/program.js.map +1 -1
  27. package/lib/core/case-defaults.d.ts +16 -0
  28. package/lib/core/case-defaults.d.ts.map +1 -0
  29. package/lib/core/case-defaults.js +42 -0
  30. package/lib/core/case-defaults.js.map +1 -0
  31. package/lib/core/case.d.ts +8 -3
  32. package/lib/core/case.d.ts.map +1 -1
  33. package/lib/core/case.js +33 -4
  34. package/lib/core/case.js.map +1 -1
  35. package/lib/core/reference-defaults.d.ts +5 -0
  36. package/lib/core/reference-defaults.d.ts.map +1 -0
  37. package/lib/core/reference-defaults.js +42 -0
  38. package/lib/core/reference-defaults.js.map +1 -0
  39. package/lib/index.d.ts +2 -1
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js.map +1 -1
  42. package/lib/orchestrator/config.d.ts +1 -0
  43. package/lib/orchestrator/config.d.ts.map +1 -1
  44. package/lib/orchestrator/config.js +3 -0
  45. package/lib/orchestrator/config.js.map +1 -1
  46. package/lib/orchestrator/doctor.d.ts.map +1 -1
  47. package/lib/orchestrator/doctor.js +19 -42
  48. package/lib/orchestrator/doctor.js.map +1 -1
  49. package/lib/orchestrator/experiment-context.d.ts.map +1 -1
  50. package/lib/orchestrator/experiment-context.js +2 -1
  51. package/lib/orchestrator/experiment-context.js.map +1 -1
  52. package/lib/orchestrator/experiment-corpus.d.ts.map +1 -1
  53. package/lib/orchestrator/experiment-corpus.js +1 -1
  54. package/lib/orchestrator/experiment-corpus.js.map +1 -1
  55. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  56. package/lib/orchestrator/experiment-evaluation.js +1 -0
  57. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  58. package/lib/orchestrator/experiment-identity.d.ts.map +1 -1
  59. package/lib/orchestrator/experiment-identity.js +3 -3
  60. package/lib/orchestrator/experiment-identity.js.map +1 -1
  61. package/lib/orchestrator/orchestrator.d.ts +3 -1
  62. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  63. package/lib/orchestrator/orchestrator.js +18 -19
  64. package/lib/orchestrator/orchestrator.js.map +1 -1
  65. package/lib/orchestrator/profile.d.ts +4 -0
  66. package/lib/orchestrator/profile.d.ts.map +1 -0
  67. package/lib/orchestrator/profile.js +10 -0
  68. package/lib/orchestrator/profile.js.map +1 -0
  69. package/package.json +1 -1
  70. package/skills/lux-answerer/SKILL.md +1 -1
  71. package/src/assertions/core/command-exits-zero.ts +33 -22
  72. package/src/assertions/rubric/evidence.ts +76 -0
  73. package/src/assertions/rubric/index.ts +10 -57
  74. package/src/assertions/rubric/internal.ts +7 -6
  75. package/src/authoring.ts +1 -0
  76. package/src/cli/harness-plan.ts +7 -1
  77. package/src/cli/program.ts +8 -4
  78. package/src/core/case-defaults.ts +65 -0
  79. package/src/core/case.ts +53 -6
  80. package/src/core/reference-defaults.ts +47 -0
  81. package/src/index.ts +2 -1
  82. package/src/orchestrator/config.ts +3 -0
  83. package/src/orchestrator/doctor.ts +30 -45
  84. package/src/orchestrator/experiment-context.ts +2 -1
  85. package/src/orchestrator/experiment-corpus.ts +6 -1
  86. package/src/orchestrator/experiment-evaluation.ts +1 -0
  87. package/src/orchestrator/experiment-identity.ts +18 -3
  88. package/src/orchestrator/orchestrator.ts +28 -17
  89. package/src/orchestrator/profile.ts +24 -0
@@ -0,0 +1,65 @@
1
+ import type { Assertion, EvalCase } from "./case.js";
2
+ import { mergePluginConfig } from "./harness-matrix.js";
3
+ import { mergeAnswererReference, mergeDriverReference } from "./reference-defaults.js";
4
+
5
+ type DeepPartial<T> = T extends readonly unknown[]
6
+ ? T
7
+ : T extends (...args: never[]) => unknown
8
+ ? T
9
+ : T extends object
10
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
11
+ : T;
12
+
13
+ export type AssertionDefaults<A extends Assertion = Assertion> = {
14
+ [Type in A["type"]]?: DeepPartial<Omit<Extract<A, { type: Type }>, "type" | "id">>;
15
+ };
16
+
17
+ export type EvalCaseDefaults<A extends Assertion = Assertion> = Omit<
18
+ Partial<EvalCase>,
19
+ "id" | "prompt" | "assertions"
20
+ > & { assertions?: A[] | undefined };
21
+
22
+ /** Named assertions replace inherited checks; unnamed checks append in declaration order. */
23
+ const mergeAssertions = (base: Assertion[], local: Assertion[]): Assertion[] => {
24
+ const seen = new Set<string>();
25
+ for (const assertion of local) {
26
+ if (!assertion.id) continue;
27
+ if (seen.has(assertion.id)) throw new Error(`duplicate assertion id '${assertion.id}'`);
28
+ seen.add(assertion.id);
29
+ }
30
+ const result = [...base];
31
+ for (const assertion of local) {
32
+ const index = assertion.id ? result.findIndex((entry) => entry.id === assertion.id) : -1;
33
+ if (index < 0) result.push(assertion);
34
+ else result[index] = assertion;
35
+ }
36
+ return result;
37
+ };
38
+
39
+ export const mergeCaseDefaults = (
40
+ base: EvalCaseDefaults,
41
+ local: EvalCaseDefaults,
42
+ ): EvalCaseDefaults => ({
43
+ ...base,
44
+ ...local,
45
+ ...(base.driver || local.driver
46
+ ? { driver: mergeDriverReference(base.driver, local.driver) }
47
+ : {}),
48
+ ...(base.answerer || local.answerer
49
+ ? { answerer: mergeAnswererReference(base.answerer, local.answerer) }
50
+ : {}),
51
+ ...(base.meta || local.meta
52
+ ? { meta: mergePluginConfig(base.meta, local.meta) as EvalCase["meta"] }
53
+ : {}),
54
+ ...(base.assertions || local.assertions
55
+ ? { assertions: mergeAssertions(base.assertions ?? [], local.assertions ?? []) }
56
+ : {}),
57
+ });
58
+
59
+ export const applyAssertionDefaults = (
60
+ assertions: Assertion[],
61
+ defaults: AssertionDefaults,
62
+ ): Assertion[] =>
63
+ assertions.map((assertion) => ({
64
+ ...(mergePluginConfig(defaults[assertion.type], assertion) as Assertion),
65
+ }));
package/src/core/case.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import type { AssertionPlugin } from "./assertion.js";
3
3
  import { collectEvalCaseExtensions, setEvalCaseExtensions } from "./case-authoring-state.js";
4
+ import {
5
+ type AssertionDefaults,
6
+ applyAssertionDefaults,
7
+ type EvalCaseDefaults,
8
+ mergeCaseDefaults,
9
+ } from "./case-defaults.js";
4
10
  import { DriverCapabilitySchema } from "./driver.js";
5
11
  import { DriverExecutionSchema } from "./driver-execution.js";
6
12
  import { EvaluationMetadataSchema } from "./evaluation-clusters.js";
7
- import { HarnessMatrixSchema } from "./harness-matrix.js";
13
+ import { HarnessMatrixSchema, mergePluginConfig } from "./harness-matrix.js";
8
14
  import type { LifecycleHooksPlugin } from "./lifecycle-hooks.js";
9
15
  import { formatZodIssues } from "./zod.js";
10
16
 
@@ -96,13 +102,18 @@ export type EvalCaseInput<AllowedAssertion extends Assertion = Assertion> = Omit
96
102
  "assertions"
97
103
  > & {
98
104
  assertions?: AllowedAssertion[];
105
+ /** Remove named inherited checks without replacing the rest of the suite plan. */
106
+ omitAssertions?: string[];
99
107
  };
100
108
 
101
109
  export type EvalCaseAuthoringOptions<
102
110
  Plugins extends readonly AssertionPlugin[] = readonly AssertionPlugin[],
111
+ AllowedAssertion extends Assertion = Assertion,
103
112
  > = {
104
113
  assertions?: Plugins;
105
114
  fixtures?: readonly LifecycleHooksPlugin[];
115
+ defaults?: EvalCaseDefaults<AllowedAssertion | AssertionInputOf<Plugins[number]>>;
116
+ assertionDefaults?: AssertionDefaults<AllowedAssertion | AssertionInputOf<Plugins[number]>>;
106
117
  };
107
118
 
108
119
  export type EvalCaseDefinition<AllowedAssertion extends Assertion = Assertion> = {
@@ -112,16 +123,44 @@ export type EvalCaseDefinition<AllowedAssertion extends Assertion = Assertion> =
112
123
  * Further calls to `extend` append fixtures in setup order.
113
124
  */
114
125
  extend<const Plugins extends readonly AssertionPlugin[] = readonly []>(
115
- options: EvalCaseAuthoringOptions<Plugins>,
126
+ options: EvalCaseAuthoringOptions<Plugins, AllowedAssertion>,
116
127
  ): EvalCaseDefinition<AllowedAssertion | AssertionInputOf<Plugins[number]>>;
117
128
  };
118
129
 
119
130
  export const createEvalCaseDefinition = <AllowedAssertion extends Assertion = Assertion>(
120
131
  inheritedAssertions: readonly AssertionPlugin[] = [],
121
132
  inheritedFixtures: readonly LifecycleHooksPlugin[] = [],
133
+ defaults: EvalCaseDefaults = {},
134
+ assertionDefaults: AssertionDefaults = {},
122
135
  ): EvalCaseDefinition<AllowedAssertion> => {
123
136
  const define = ((input: EvalCaseInput<AllowedAssertion>): EvalCase => {
124
- const parsed = EvalCaseSchema.safeParse(input);
137
+ const { omitAssertions = [], ...local } = input;
138
+ for (const id of omitAssertions) {
139
+ if (!defaults.assertions?.some((assertion) => assertion.id === id)) {
140
+ throw new Error(`cannot omit unknown inherited assertion '${id}'`);
141
+ }
142
+ }
143
+ const resolved = mergeCaseDefaults(
144
+ {
145
+ ...defaults,
146
+ ...(defaults.assertions
147
+ ? {
148
+ assertions: defaults.assertions.filter(
149
+ (assertion) => !omitAssertions.includes(assertion.id ?? ""),
150
+ ),
151
+ }
152
+ : {}),
153
+ },
154
+ local,
155
+ );
156
+ const parsed = EvalCaseSchema.safeParse({
157
+ ...resolved,
158
+ ...("id" in local ? { id: local.id } : {}),
159
+ prompt: local.prompt,
160
+ ...(resolved.assertions
161
+ ? { assertions: applyAssertionDefaults(resolved.assertions, assertionDefaults) }
162
+ : {}),
163
+ });
125
164
  if (!parsed.success) {
126
165
  throw new Error(`Invalid eval case: ${formatZodIssues(parsed.error)}`);
127
166
  }
@@ -133,11 +172,19 @@ export const createEvalCaseDefinition = <AllowedAssertion extends Assertion = As
133
172
  setEvalCaseExtensions(parsed.data, extensions);
134
173
  return parsed.data;
135
174
  }) as EvalCaseDefinition<AllowedAssertion>;
136
- define.extend = (({ assertions = [], fixtures = [] }: EvalCaseAuthoringOptions) =>
137
- createEvalCaseDefinition(
175
+ define.extend = ((options: EvalCaseAuthoringOptions) => {
176
+ const { assertions = [], fixtures = [] } = options;
177
+ const inherited = EvalCaseObjectSchema.omit({ id: true, prompt: true })
178
+ .partial()
179
+ .strict()
180
+ .parse(options.defaults ?? {});
181
+ return createEvalCaseDefinition(
138
182
  [...inheritedAssertions, ...assertions],
139
183
  [...inheritedFixtures, ...fixtures],
140
- )) as EvalCaseDefinition<AllowedAssertion>["extend"];
184
+ mergeCaseDefaults(defaults, inherited),
185
+ mergePluginConfig(assertionDefaults, options.assertionDefaults ?? {}) as AssertionDefaults,
186
+ );
187
+ }) as EvalCaseDefinition<AllowedAssertion>["extend"];
141
188
  return define;
142
189
  };
143
190
 
@@ -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;
@@ -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
+ });