Skip to content
6 changes: 0 additions & 6 deletions apps/cli/src/commands/results/combine-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export interface CombineRunSource {
readonly displayName: string;
readonly manifestPath: string;
readonly experiment: string;
readonly tags?: readonly string[];
}

export interface DuplicateConflict {
Expand Down Expand Up @@ -111,7 +110,6 @@ export interface CombineRunResult {
readonly duplicateConflicts: readonly DuplicateConflict[];
readonly testCount: number;
readonly targetCount: number;
readonly tags: readonly string[];
}

function parseJsonlLine(line: string): ResultManifestRecord {
Expand Down Expand Up @@ -538,7 +536,6 @@ export function buildCombineRunSources(
options?: {
ids?: readonly string[];
displayNames?: readonly string[];
tags?: readonly string[][];
},
): CombineRunSource[] {
return sourcePaths.map((sourcePath, index) => {
Expand All @@ -561,7 +558,6 @@ export function buildCombineRunSources(
displayName: options?.displayNames?.[index] ?? path.basename(runDir),
manifestPath,
experiment: normalizeExperimentName(experiment),
tags: options?.tags?.[index],
};
});
}
Expand Down Expand Up @@ -632,7 +628,6 @@ export function combineRunSources(options: CombineRunOptions): CombineRunResult
const summaryPath = path.join(runDir, 'summary.json');
writeJson(summaryPath, summaryWithMetadata);

const tags = [...new Set(loadedSources.flatMap((source) => source.tags ?? []))].sort();
return {
runDir,
runId: toRunId(options.cwd, runDir),
Expand All @@ -644,6 +639,5 @@ export function combineRunSources(options: CombineRunOptions): CombineRunResult
duplicateConflicts: conflicts,
testCount: rows.length,
targetCount: new Set(results.map((result) => result.target ?? 'unknown')).size,
tags,
};
}
34 changes: 6 additions & 28 deletions apps/cli/src/commands/results/eval-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
normalizeExperimentName,
} from '../eval/result-layout.js';
import { findRepoRoot } from '../eval/shared.js';
import { normalizeTags, writeRunTags } from './run-tags.js';

// ── In-memory run tracker ────────────────────────────────────────────────

Expand Down Expand Up @@ -146,7 +145,6 @@ interface RunEvalRequest {
test_ids?: string[];
target?: string;
experiment?: string;
tags?: string[];
threshold?: number;
workers?: number;
/** Resume an interrupted run: skip already-completed tests and append results to `output`. */
Expand Down Expand Up @@ -176,24 +174,12 @@ function validateResumeOptions(req: RunEvalRequest): string | undefined {
return undefined;
}

function parseInitialTags(value: unknown): string[] {
if (value === undefined) return [];
if (!Array.isArray(value)) {
throw new Error('tags must be an array of strings');
}
return normalizeTags(value);
}

function normalizeRunMetadata(req: RunEvalRequest): { experiment: string; tags: string[] } {
function normalizeRunMetadata(req: RunEvalRequest): { experiment: string } {
const experiment = normalizeExperimentName(req.experiment);
const tags = parseInitialTags(req.tags);
if ((req.resume || req.rerun_failed) && req.experiment?.trim()) {
throw new Error('experiment cannot be changed when resuming an existing run');
}
if ((req.resume || req.rerun_failed) && tags.length > 0) {
throw new Error('initial tags can only be set when creating a new run');
}
return { experiment, tags };
return { experiment };
}

function buildCliArgs(req: RunEvalRequest, experiment?: string): string[] {
Expand Down Expand Up @@ -331,12 +317,6 @@ function openConsoleLogStream(outputDir: string): WriteStream | undefined {
}
}

function writeInitialRunTags(outputDir: string, tags: readonly string[]): void {
if (tags.length === 0) return;
mkdirSync(outputDir, { recursive: true });
writeRunTags(path.join(outputDir, RESULT_INDEX_FILENAME), tags);
}

// ── Route registration ───────────────────────────────────────────────────

// biome-ignore lint/suspicious/noExplicitAny: Hono Context generic varies by route
Expand Down Expand Up @@ -400,7 +380,7 @@ export function registerEvalRoutes(
return c.json({ error: resumeError }, 400);
}

let metadata: { experiment: string; tags: string[] };
let metadata: { experiment: string };
try {
metadata = normalizeRunMetadata(body);
} catch (err) {
Expand Down Expand Up @@ -440,7 +420,6 @@ export function registerEvalRoutes(
activeRuns.set(runId, run);

try {
writeInitialRunTags(outputDir, metadata.tags);
const child = spawn(cliPaths.binPath, [...cliPaths.args, ...args], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down Expand Up @@ -576,7 +555,7 @@ export function registerEvalRoutes(
return c.json({ error: 'Invalid JSON body' }, 400);
}

let metadata: { experiment: string; tags: string[] };
let metadata: { experiment: string };
try {
metadata = normalizeRunMetadata(body);
} catch (err) {
Expand Down Expand Up @@ -636,7 +615,7 @@ export function registerEvalRoutes(
return c.json({ error: resumeError }, 400);
}

let metadata: { experiment: string; tags: string[] };
let metadata: { experiment: string };
try {
metadata = normalizeRunMetadata(body);
} catch (err) {
Expand Down Expand Up @@ -671,7 +650,6 @@ export function registerEvalRoutes(
activeRuns.set(runId, run);

try {
writeInitialRunTags(outputDir, metadata.tags);
const child = spawn(cliPaths.binPath, [...cliPaths.args, ...args], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down Expand Up @@ -775,7 +753,7 @@ export function registerEvalRoutes(
} catch {
return c.json({ error: 'Invalid JSON body' }, 400);
}
let metadata: { experiment: string; tags: string[] };
let metadata: { experiment: string };
try {
metadata = normalizeRunMetadata(body);
} catch (err) {
Expand Down
23 changes: 23 additions & 0 deletions apps/cli/src/commands/results/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface ResultManifestRecord {
readonly suite?: string;
readonly category?: string;
readonly experiment?: string;
/** promptfoo-shaped tag map (`Record<string,string>`), e.g. `{experiment, team, env}`. */
readonly tags?: Record<string, string>;
readonly target?: string;
readonly variant?: string;
readonly score: number;
Expand Down Expand Up @@ -315,6 +317,8 @@ export interface LightweightResultRecord {
readonly target?: string;
readonly variant?: string;
readonly experiment?: string;
/** promptfoo-shaped tag map from the JSONL row's `tags` field. */
readonly tags?: Record<string, string>;
readonly score: number;
readonly scores?: readonly Record<string, unknown>[];
readonly executionStatus?: string;
Expand All @@ -324,6 +328,24 @@ export interface LightweightResultRecord {
readonly runtimeSource?: RunRuntimeSourceMetadata;
}

/**
* Coerce a raw JSONL `tags` value into a `Record<string,string>`, dropping
* non-string values. Returns undefined when the map is absent or empty so the
* lightweight record stays sparse for old runs that never wrote a tags map.
*/
export function normalizeTagMap(value: unknown): Record<string, string> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
const entries: [string, string][] = [];
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
if (typeof raw === 'string') {
entries.push([key, raw]);
}
}
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
}

export function loadLightweightResults(sourceFile: string): LightweightResultRecord[] {
const resolvedSourceFile = resolveRunManifestPath(sourceFile);
const content = readFileSync(resolvedSourceFile, 'utf8');
Expand All @@ -335,6 +357,7 @@ export function loadLightweightResults(sourceFile: string): LightweightResultRec
target: record.target,
variant: record.variant,
experiment: record.experiment,
tags: normalizeTagMap(record.tags),
score: record.score,
scores: record.scores,
executionStatus: record.execution_status,
Expand Down
Loading
Loading