Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 17 additions & 36 deletions apps/cli/src/commands/eval/run-eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,20 +571,14 @@ export function resolveExperimentNamespace(params: {
resultGroupName: string;
}): {
experiment: string;
source: RunRuntimeSourceMetadata['experiment_namespace_source'];
} {
if (params.cliExperiment) {
return { experiment: params.cliExperiment, source: 'cli' };
return { experiment: params.cliExperiment };
}
if (params.tagsExperiment) {
return { experiment: params.tagsExperiment, source: 'tags' };
return { experiment: params.tagsExperiment };
}
const source: RunRuntimeSourceMetadata['experiment_namespace_source'] = params.isMultiEval
? 'multi_eval'
: params.suiteName
? 'eval_metadata'
: 'eval_filename';
return { experiment: params.resultGroupName, source };
return { experiment: params.resultGroupName };
}

/**
Expand Down Expand Up @@ -902,11 +896,9 @@ function buildRuntimeSourceMetadata(params: {
readonly activeTestFiles: readonly string[];
readonly sourceTests: readonly EvalTest[];
readonly fileMetadata: ReadonlyMap<string, { readonly options: NormalizedOptions }>;
readonly experimentNamespace: string;
readonly experimentNamespaceSource: RunRuntimeSourceMetadata['experiment_namespace_source'];
readonly hasCliRuntimeConfig: boolean;
}): RunRuntimeSourceMetadata {
const evalFiles = uniqueRuntimeSourcePaths(
const activeEvalFiles = uniqueRuntimeSourcePaths(
params.activeTestFiles.map((filePath) => toRuntimeSourcePath(params.cwd, filePath)),
);
const activeResolvedFiles = new Set(
Expand All @@ -920,30 +912,22 @@ function buildRuntimeSourceMetadata(params: {
const sourceFile = testSourceEvalPathForComparison(test);
return sourceFile ? !activeResolvedFiles.has(sourceFile) : false;
});
const kind =
params.activeTestFiles.length > 1
? 'multi_eval'
: hasImportedSuite || hasNonActiveSourceFile
? 'wrapper_eval'
: 'direct_suite';
const wrapperEvalFile =
kind === 'wrapper_eval'
? toRuntimeSourcePath(params.cwd, params.activeTestFiles[0])
: undefined;
const isWrapperEval =
params.activeTestFiles.length === 1 && (hasImportedSuite || hasNonActiveSourceFile);
const wrapperEvalFile = isWrapperEval
? toRuntimeSourcePath(params.cwd, params.activeTestFiles[0])
: undefined;
const evalFiles = sourceEvalFiles.length > 0 ? sourceEvalFiles : activeEvalFiles;

return {
schema_version: 'agentv.runtime_source.v1',
kind,
config_source: buildRuntimeConfigSource({
activeTestFiles: params.activeTestFiles,
fileMetadata: params.fileMetadata,
hasCliRuntimeConfig: params.hasCliRuntimeConfig,
}),
experiment_namespace: params.experimentNamespace,
experiment_namespace_source: params.experimentNamespaceSource,
eval_files: evalFiles,
...(wrapperEvalFile && { wrapper_eval_file: wrapperEvalFile }),
...(sourceEvalFiles.length > 0 && { source_eval_files: sourceEvalFiles }),
};
}

Expand Down Expand Up @@ -1848,14 +1832,13 @@ export async function runEvalCommand(
configTags: yamlConfig?.tags,
cliTags: options.tagMap,
});
const { experiment: resolvedExperimentNamespace, source: experimentNamespaceSource } =
resolveExperimentNamespace({
cliExperiment: resolvedExperiment.name,
tagsExperiment: normalizeString(resolvedTags?.experiment),
isMultiEval: resolvedTestFiles.length > 1,
suiteName: primarySuite?.metadata?.name,
resultGroupName,
});
const { experiment: resolvedExperimentNamespace } = resolveExperimentNamespace({
cliExperiment: resolvedExperiment.name,
tagsExperiment: normalizeString(resolvedTags?.experiment),
isMultiEval: resolvedTestFiles.length > 1,
suiteName: primarySuite?.metadata?.name,
resultGroupName,
});
// Normalize once so the row `experiment` field, AGENTV_EXPERIMENT, and the
// emitted tags map all agree (and any invalid-name error surfaces in one place).
const normalizedExperiment = normalizeExperimentName(resolvedExperimentNamespace);
Expand Down Expand Up @@ -2266,8 +2249,6 @@ export async function runEvalCommand(
activeTestFiles,
sourceTests: activeSourceTests,
fileMetadata,
experimentNamespace: normalizeExperimentName(options.experiment),
experimentNamespaceSource,
hasCliRuntimeConfig,
});
const hasPerFileRuntimeThresholds =
Expand Down
54 changes: 3 additions & 51 deletions apps/cli/src/commands/results/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,7 @@ function serializeReportResult(
const runtimeSource = manifestRecord?.runtime_source ?? summaryMetadata?.runtimeSource;
const resultExperiment = (result as EvaluationResult & { experiment?: string }).experiment;
const experimentNamespace =
runtimeSource?.experiment_namespace ??
manifestRecord?.experiment ??
summaryMetadata?.experiment ??
resultExperiment;
manifestRecord?.experiment ?? summaryMetadata?.experiment ?? resultExperiment;
const fallbackEvalFile =
normalizeEvalFileLabel(manifestRecord?.eval_file) ??
summaryMetadata?.evalFile ??
Expand All @@ -113,27 +110,13 @@ function serializeReportResult(
output: result.output,
assertions: result.assertions,
experiment: experimentNamespace,
experiment_namespace: experimentNamespace,
runtime_source: runtimeSource,
runtime_source_label: formatRuntimeSourceLabel(runtimeSource),
runtime_config_source_label: formatRuntimeConfigSourceLabel(runtimeSource?.config_source),
eval_file: fallbackEvalFile,
};
}

function formatRuntimeKindLabel(kind: RunRuntimeSourceMetadata['kind'] | undefined): string {
switch (kind) {
case 'direct_suite':
return 'Direct suite';
case 'wrapper_eval':
return 'Wrapper eval';
case 'multi_eval':
return 'Multi-eval';
default:
return 'Unknown source';
}
}

function formatRuntimeConfigSourceLabel(
source: RunRuntimeSourceMetadata['config_source'] | undefined,
): string {
Expand All @@ -151,36 +134,11 @@ function formatRuntimeConfigSourceLabel(
}
}

function formatNamespaceSourceLabel(
source: RunRuntimeSourceMetadata['experiment_namespace_source'] | undefined,
): string {
switch (source) {
case 'cli':
return 'CLI namespace';
case 'tags':
return 'Tags namespace';
case 'eval_metadata':
return 'Eval metadata namespace';
case 'eval_filename':
return 'Eval filename namespace';
case 'multi_eval':
return 'Multi-eval namespace';
default:
return '';
}
}

function formatRuntimeSourceLabel(runtimeSource: RunRuntimeSourceMetadata | undefined): string {
if (!runtimeSource) {
return '';
}
return [
formatRuntimeKindLabel(runtimeSource.kind),
formatNamespaceSourceLabel(runtimeSource.experiment_namespace_source),
formatRuntimeConfigSourceLabel(runtimeSource.config_source),
]
.filter(Boolean)
.join(' · ');
return formatRuntimeConfigSourceLabel(runtimeSource.config_source);
}

function uniqueStrings(values: readonly (string | undefined)[]): string[] {
Expand All @@ -201,13 +159,7 @@ function escapeHtml(value: string): string {

function formatReportHeaderContext(rows: readonly Record<string, unknown>[]): string {
const experiments = uniqueStrings(
rows.map((row) =>
typeof row.experiment_namespace === 'string'
? row.experiment_namespace
: typeof row.experiment === 'string'
? row.experiment
: undefined,
),
rows.map((row) => (typeof row.experiment === 'string' ? row.experiment : undefined)),
);
const runtimeSources = uniqueStrings(
rows.map((row) =>
Expand Down
15 changes: 1 addition & 14 deletions apps/cli/src/commands/results/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,6 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
runtimeSource = deriveDashboardRuntimeSource({
summaryMetadata,
records,
inferredExperiment: experiment,
});
} else {
// Run is in-progress with 0 results written yet — fall back to the
Expand All @@ -1456,7 +1455,6 @@ async function handleRuns(c: C, { searchDir, agentvDir, projectId }: DataContext
runtimeSource = deriveDashboardRuntimeSource({
summaryMetadata,
records: [],
inferredExperiment: experiment,
});
}
} catch {
Expand Down Expand Up @@ -1524,7 +1522,6 @@ async function handleRunDetail(c: C, { searchDir, projectId }: DataContext) {
const runtimeSource = deriveDashboardRuntimeSource({
summaryMetadata,
records,
inferredExperiment: records[0]?.experiment,
});
// Surface run_dir + suite_filter for local runs so the UI can launch a
// Dashboard-side resume against this exact run. Remote runs live in the
Expand Down Expand Up @@ -1646,7 +1643,6 @@ function deriveDashboardRuntimeSource(params: {
runtimeSource?: RunRuntimeSourceMetadata;
runtime_source?: RunRuntimeSourceMetadata;
}[];
readonly inferredExperiment?: string;
}): RunRuntimeSourceMetadata | undefined {
const recordWithRuntimeSource = params.records.find(
(record) => record.runtimeSource ?? record.runtime_source,
Expand All @@ -1659,25 +1655,17 @@ function deriveDashboardRuntimeSource(params: {
return explicit;
}

const experimentNamespace =
params.summaryMetadata.experiment ??
params.inferredExperiment ??
params.records.find((record) => record.experiment)?.experiment ??
'default';
const evalFiles = uniqueRuntimeSourceValues([
params.summaryMetadata.evalFile,
...params.records.map((record) => record.evalPath ?? record.eval_path),
]);
if (evalFiles.length === 0 && !experimentNamespace) {
if (evalFiles.length === 0) {
return undefined;
}

return {
schema_version: 'agentv.runtime_source.v1',
kind: evalFiles.length > 1 ? 'multi_eval' : 'direct_suite',
config_source: 'defaults',
experiment_namespace: experimentNamespace,
experiment_namespace_source: 'unknown',
eval_files: evalFiles,
};
}
Expand Down Expand Up @@ -3173,7 +3161,6 @@ export function createApp(
runtimeSource = deriveDashboardRuntimeSource({
summaryMetadata,
records,
inferredExperiment: experiment,
});
}
} catch {
Expand Down
21 changes: 6 additions & 15 deletions apps/cli/test/commands/eval/artifact-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
type GradingArtifact,
type IndexArtifactEntry,
RESULT_INDEX_FILENAME,
RUN_CONFIG_FILENAME,
type RunSummaryArtifact,
type TimingArtifact,
buildAggregateGradingArtifact,
Expand Down Expand Up @@ -1177,10 +1176,7 @@ describe('writeArtifactsFromResults', () => {
it('writes optional runtime source metadata to summary only', async () => {
const runtimeSource = {
schema_version: 'agentv.runtime_source.v1' as const,
kind: 'direct_suite' as const,
config_source: 'cli_flags' as const,
experiment_namespace: 'cli-smoke',
experiment_namespace_source: 'cli' as const,
eval_files: ['evals/smoke.eval.yaml'],
};
const paths = await writeArtifactsFromResults([makeResult({ testId: 'alpha' })], testDir, {
Expand All @@ -1199,7 +1195,7 @@ describe('writeArtifactsFromResults', () => {
expect(indexLine.runtime_source).toBeUndefined();
});

it('moves experiment config metadata to an internal run config sidecar', async () => {
it('does not write experiment config metadata into public run artifacts', async () => {
const experimentMetadata = {
name: 'native-exp',
target: 'codex-target',
Expand All @@ -1214,21 +1210,16 @@ describe('writeArtifactsFromResults', () => {

const summary: RunSummaryArtifact = JSON.parse(await readFile(paths.summaryPath, 'utf8'));
expect(summary.metadata).not.toHaveProperty('experiment_config');
expect(summary.metadata.run_config_path).toBe(`.internal/${RUN_CONFIG_FILENAME}`);

const runConfig = JSON.parse(
await readFile(path.join(paths.testArtifactDir, '.internal', RUN_CONFIG_FILENAME), 'utf8'),
);
expect(runConfig).toEqual({
schema_version: 'agentv.run_config.v1',
experiment_config: experimentMetadata,
});
expect(summary.metadata).not.toHaveProperty('run_config_path');
await expect(
readFile(path.join(paths.testArtifactDir, '.internal', 'run-config.json'), 'utf8'),
).rejects.toThrow();

await aggregateRunDir(paths.testArtifactDir);
const rewrittenSummary: RunSummaryArtifact = JSON.parse(
await readFile(paths.summaryPath, 'utf8'),
);
expect(rewrittenSummary.metadata.run_config_path).toBe(`.internal/${RUN_CONFIG_FILENAME}`);
expect(rewrittenSummary.metadata).not.toHaveProperty('run_config_path');
});

it('omits duplicated root instances from run summary', () => {
Expand Down
10 changes: 4 additions & 6 deletions apps/cli/test/commands/eval/tag-filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,20 +144,18 @@ describe('resolveExperimentNamespace', () => {
it('prefers an explicit --experiment over tags.experiment and the default', () => {
expect(
resolveExperimentNamespace({ ...base, cliExperiment: 'cli-exp', tagsExperiment: 'tag-exp' }),
).toEqual({ experiment: 'cli-exp', source: 'cli' });
).toEqual({ experiment: 'cli-exp' });
});

it('uses tags.experiment when no --experiment is given', () => {
expect(resolveExperimentNamespace({ ...base, tagsExperiment: 'tag-exp' })).toEqual({
experiment: 'tag-exp',
source: 'tags',
});
});

it('falls back to the eval-metadata default when neither is set (e.g. --tag experiment=)', () => {
expect(resolveExperimentNamespace({ ...base })).toEqual({
experiment: 'my-suite',
source: 'eval_metadata',
});
});

Expand All @@ -167,13 +165,13 @@ describe('resolveExperimentNamespace', () => {
isMultiEval: false,
resultGroupName: 'dataset',
}),
).toEqual({ experiment: 'dataset', source: 'eval_filename' });
).toEqual({ experiment: 'dataset' });
});

it('labels multi-eval runs when no CLI/tags experiment is set', () => {
expect(
resolveExperimentNamespace({ isMultiEval: true, resultGroupName: 'multi-eval' }),
).toEqual({ experiment: 'multi-eval', source: 'multi_eval' });
).toEqual({ experiment: 'multi-eval' });
});

it('lets tags.experiment win over the multi-eval default', () => {
Expand All @@ -183,7 +181,7 @@ describe('resolveExperimentNamespace', () => {
tagsExperiment: 'tag-exp',
resultGroupName: 'multi-eval',
}),
).toEqual({ experiment: 'tag-exp', source: 'tags' });
).toEqual({ experiment: 'tag-exp' });
});
});

Expand Down
10 changes: 2 additions & 8 deletions apps/cli/test/commands/results/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,23 +160,17 @@ describe('results report', () => {
experiment: 'named-smoke',
runtimeSource: {
schema_version: 'agentv.runtime_source.v1',
kind: 'wrapper_eval',
config_source: 'inline_experiment',
experiment_namespace: 'named-smoke',
experiment_namespace_source: 'eval_metadata',
eval_files: ['evals/wrapper.eval.yaml'],
eval_files: ['evals/child.eval.yaml'],
wrapper_eval_file: 'evals/wrapper.eval.yaml',
source_eval_files: ['evals/child.eval.yaml'],
},
});

const { outputPath } = await writeResultsReport(runDir, undefined, tempDir);
const html = readFileSync(outputPath, 'utf8');

expect(html).toContain('Experiment namespace: named-smoke');
expect(html).toContain(
'Runtime source: Wrapper eval · Eval metadata namespace · Inline experiment config',
);
expect(html).toContain('Runtime source: Inline experiment config');
});

it('embeds result text containing replacement tokens without corrupting the inline script', async () => {
Expand Down
Loading
Loading