@ls-stack/agent-eval 0.55.1 → 0.56.0

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.
@@ -220,6 +220,19 @@ const traceSpanSchema = z.object({
220
220
  });
221
221
  //#endregion
222
222
  //#region ../shared/src/schemas/cache.ts
223
+ const outputColumnOverrideSchema = z.object({
224
+ label: z.string().optional(),
225
+ format: columnFormatSchema.optional(),
226
+ numberFormat: numberDisplayOptionsSchema.optional(),
227
+ hideInTable: z.boolean().optional(),
228
+ hideIfNoValue: z.boolean().optional(),
229
+ align: z.enum([
230
+ "left",
231
+ "center",
232
+ "right"
233
+ ]).optional(),
234
+ maxStars: z.number().int().min(2).optional()
235
+ });
223
236
  /**
224
237
  * Mode that controls how the cache is consulted for a given run.
225
238
  *
@@ -275,12 +288,15 @@ const traceCacheRefSchema = z.object({
275
288
  z.object({
276
289
  key: z.string(),
277
290
  namespace: z.string(),
278
- operationType: cacheOperationTypeSchema,
279
- operationName: z.string(),
280
- spanName: z.string().optional(),
281
- spanKind: traceSpanKindSchema.optional(),
282
291
  storedAt: z.string(),
283
- sizeBytes: z.number()
292
+ lastAccessedAt: z.string()
293
+ });
294
+ z.object({
295
+ removedCacheFiles: z.number(),
296
+ removedDebugFiles: z.number(),
297
+ removedBlobFiles: z.number(),
298
+ removedIndexRows: z.number(),
299
+ rewrittenIndexes: z.number()
284
300
  });
285
301
  /** Zod schema for `SerializedCacheSpan`, defined lazily for recursion. */
286
302
  const serializedCacheSpanSchema = z.object({
@@ -308,7 +324,8 @@ const cacheRecordingOpSchema = z.discriminatedUnion("kind", [
308
324
  z.object({
309
325
  kind: z.literal("setOutput"),
310
326
  key: z.string(),
311
- value: z.unknown()
327
+ value: z.unknown(),
328
+ column: outputColumnOverrideSchema.optional()
312
329
  }),
313
330
  z.object({
314
331
  kind: z.literal("appendOutput"),
@@ -789,6 +806,11 @@ const caseRowSchema = z.object({
789
806
  cacheOperations: z.number().optional(),
790
807
  costUsd: z.number().nullable().optional(),
791
808
  columns: z.record(z.string(), cellValueSchema),
809
+ /**
810
+ * Runtime column definitions authored by output helpers for this case.
811
+ * These complement eval-level `columns` without changing discovery metadata.
812
+ */
813
+ outputColumnDefs: z.array(columnDefSchema).optional(),
792
814
  /** Winning trial index for the persisted case result. */
793
815
  trial: z.number()
794
816
  });
@@ -895,6 +917,11 @@ const caseDetailSchema = z.object({
895
917
  */
896
918
  scoringTraces: z.record(z.string(), scoreTraceSchema).optional(),
897
919
  columns: z.record(z.string(), cellValueSchema),
920
+ /**
921
+ * Runtime column definitions authored by output helpers for this case.
922
+ * These complement eval-level `columns` without changing discovery metadata.
923
+ */
924
+ outputColumnDefs: z.array(columnDefSchema).optional(),
898
925
  assertionFailures: z.array(z.union([assertionFailureSchema, legacyAssertionFailureSchema])),
899
926
  /** Logs captured from manual `evalLog(...)` calls and enabled console calls. */
900
927
  logs: z.array(runLogEntrySchema).default([]),
@@ -1377,6 +1404,7 @@ const agentEvalsConfigSchema = z.object({
1377
1404
  dir: z.string().optional(),
1378
1405
  maxEntriesPerNamespace: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional()),
1379
1406
  maxEntriesByNamespace: z.record(z.string(), z.number()).optional(),
1407
+ pruneIdleDelayMs: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional()),
1380
1408
  maxEntriesPerEval: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional())
1381
1409
  }).optional()
1382
1410
  });
@@ -2136,17 +2164,34 @@ function computeTokensPerSecond({ outputTokens, durationMs }) {
2136
2164
  if (durationMs <= 0) return null;
2137
2165
  return outputTokens / (durationMs / 1e3);
2138
2166
  }
2139
- function readSteps(attributes, path) {
2167
+ function readSteps(attributes, path, childModelSteps) {
2140
2168
  const raw = getNestedAttribute(attributes, path);
2141
2169
  if (Array.isArray(raw)) return {
2142
2170
  stepCount: raw.length,
2143
2171
  stepDetails: raw
2144
2172
  };
2173
+ if (childModelSteps.length > 0) return {
2174
+ stepCount: childModelSteps.length,
2175
+ stepDetails: childModelSteps
2176
+ };
2145
2177
  return {
2146
2178
  stepCount: null,
2147
2179
  stepDetails: null
2148
2180
  };
2149
2181
  }
2182
+ function buildModelStepsByParent(spans) {
2183
+ const stepsByParent = /* @__PURE__ */ new Map();
2184
+ for (const span of spans) {
2185
+ if (span.kind !== "model_step" || span.parentId === null) continue;
2186
+ const current = stepsByParent.get(span.parentId);
2187
+ if (current === void 0) {
2188
+ stepsByParent.set(span.parentId, [span]);
2189
+ continue;
2190
+ }
2191
+ current.push(span);
2192
+ }
2193
+ return stepsByParent;
2194
+ }
2150
2195
  function collectWarnings$1(span) {
2151
2196
  const out = [];
2152
2197
  if (span.warning) out.push(span.warning);
@@ -2178,6 +2223,9 @@ function pickError$1(span) {
2178
2223
  * charged twice. Cache read/write costs still contribute to the total USD cost
2179
2224
  * at their configured rates. The `steps` attribute path may resolve to an array
2180
2225
  * of per-step detail objects, with `stepCount` derived from the array length.
2226
+ * When a matching LLM span does not expose that array, direct child spans with
2227
+ * `kind: 'model_step'` are used as the step details instead. This preserves
2228
+ * Mastra/OpenTelemetry traces where model steps are emitted as child spans.
2181
2229
  * `durationMs` and `tokensPerSecond` are `null` while the span is still
2182
2230
  * running. User-defined `metrics` whose path resolves to
2183
2231
  * `undefined` are dropped, but `null`, `0`, and `false` are preserved as
@@ -2186,6 +2234,7 @@ function pickError$1(span) {
2186
2234
  */
2187
2235
  function extractLlmCalls(spans, config) {
2188
2236
  const kindSet = new Set(config.kinds);
2237
+ const modelStepsByParent = buildModelStepsByParent(spans);
2189
2238
  const result = [];
2190
2239
  for (const span of spans) {
2191
2240
  if (!kindSet.has(span.kind)) continue;
@@ -2271,7 +2320,7 @@ function extractLlmCalls(spans, config) {
2271
2320
  cachedInputCostUsd,
2272
2321
  cacheCreationInputCostUsd,
2273
2322
  reasoningCostUsd,
2274
- ...readSteps(attrs, config.attributes.steps),
2323
+ ...readSteps(attrs, config.attributes.steps, modelStepsByParent.get(span.id) ?? []),
2275
2324
  finishReason: readString$2(attrs, config.attributes.finishReason),
2276
2325
  durationMs,
2277
2326
  input: getNestedAttribute(attrs, config.attributes.input),
@@ -3036,6 +3085,7 @@ async function runInEvalScope(caseId, fn, options = {}) {
3036
3085
  input: options.input,
3037
3086
  tags: options.tags ?? [],
3038
3087
  outputs: {},
3088
+ outputColumnOverrides: {},
3039
3089
  assertionFailures: [],
3040
3090
  logs: [],
3041
3091
  spans: [],
@@ -3085,6 +3135,11 @@ function recordOpIfActive(scope, op) {
3085
3135
  const top = scope.recordingStack.at(-1);
3086
3136
  if (top) top.ops.push(op);
3087
3137
  }
3138
+ function normalizeEvalOutputOptions(options) {
3139
+ if (options === void 0) return void 0;
3140
+ if (typeof options === "string") return { format: options };
3141
+ return options;
3142
+ }
3088
3143
  function toAssertionFailure$1(message, error = void 0) {
3089
3144
  const name = error?.name;
3090
3145
  const stack = error?.stack ? stripTerminalControlCodes$1(error.stack) : void 0;
@@ -3099,15 +3154,22 @@ function toAssertionFailure$1(message, error = void 0) {
3099
3154
  *
3100
3155
  * Supported values include scalars, JSON-safe objects/arrays, explicit file
3101
3156
  * refs, and native `Blob`/`File` instances for media or file columns.
3157
+ *
3158
+ * Pass the optional third argument to persist a display format or full column
3159
+ * override with this runtime output, for example `'markdown'` or
3160
+ * `{ label: 'Receipt', format: 'image', hideInTable: true }`.
3102
3161
  */
3103
- function setEvalOutput(key, value) {
3162
+ function setEvalOutput(key, value, options = void 0) {
3104
3163
  const scope = getCurrentScope();
3105
3164
  if (!scope) return;
3106
3165
  scope.outputs[key] = value;
3166
+ const column = normalizeEvalOutputOptions(options);
3167
+ if (column !== void 0) scope.outputColumnOverrides[key] = column;
3107
3168
  recordOpIfActive(scope, {
3108
3169
  kind: "setOutput",
3109
3170
  key,
3110
- value
3171
+ value,
3172
+ column
3111
3173
  });
3112
3174
  }
3113
3175
  /**
@@ -4106,6 +4168,7 @@ function replayRecording(scope, parentSpan, recording, options) {
4106
4168
  function applyRecordingOp(scope, parentSpan, op, options) {
4107
4169
  if (op.kind === "setOutput") {
4108
4170
  scope.outputs[op.key] = op.value;
4171
+ if (op.column !== void 0) scope.outputColumnOverrides[op.key] = op.column;
4109
4172
  return;
4110
4173
  }
4111
4174
  if (op.kind === "appendOutput") {
@@ -4926,8 +4989,10 @@ const cacheSerializationMarker = "__aecs";
4926
4989
  const supportedCacheSerializationPrefix = "v1:";
4927
4990
  const externalJsonCacheSerializationMarker = "v1:ExternalJson";
4928
4991
  const externalJsonBlobExtension = ".json.br";
4992
+ const externalJsonBlobDirName = "cache-blobs";
4929
4993
  const cacheEntryExtension = ".json.br";
4930
4994
  const debugEntryExtension = ".json";
4995
+ const cacheIndexFilePrefix = ".index-";
4931
4996
  async function commitPendingCacheWrites(params) {
4932
4997
  for (const pendingWrite of params.pendingWrites) await params.backingStore.write(pendingWrite.entry, pendingWrite.debugKey);
4933
4998
  }
@@ -4941,8 +5006,14 @@ async function commitPendingCacheWrites(params) {
4941
5006
  function createFsCacheStore(options) {
4942
5007
  const cacheDir = resolve(options.workspaceRoot, options.dir ?? ".agent-evals/cache");
4943
5008
  const debugDir = resolve(options.workspaceRoot, options.debugDir ?? ".agent-evals/cache-debug");
4944
- const blobDir = resolve(options.workspaceRoot, options.blobDir ?? ".agent-evals/cache-blobs");
4945
- const externalJsonStore = createExternalJsonBlobStore(blobDir);
5009
+ const blobDir = options.blobDir === void 0 ? join(cacheDir, externalJsonBlobDirName) : resolve(options.workspaceRoot, options.blobDir);
5010
+ const legacyBlobDir = resolve(options.workspaceRoot, ".agent-evals/cache-blobs");
5011
+ const fallbackBlobDirs = options.blobDir === void 0 && legacyBlobDir !== blobDir ? [legacyBlobDir] : [];
5012
+ const blobDirs = [blobDir, ...fallbackBlobDirs];
5013
+ const externalJsonStore = createExternalJsonBlobStore({
5014
+ fallbackDirs: fallbackBlobDirs,
5015
+ primaryDir: blobDir
5016
+ });
4946
5017
  const defaultMaxEntries = normalizeMaxEntries(options.maxEntriesPerNamespace);
4947
5018
  return {
4948
5019
  externalJsonStore,
@@ -4956,11 +5027,22 @@ function createFsCacheStore(options) {
4956
5027
  return blobDir;
4957
5028
  },
4958
5029
  async lookup(namespace, keyHash) {
4959
- const entry = await readCacheEntry(cacheDir, namespace, keyHash);
4960
- return entry === null ? null : await materializeExternalJsonCacheEntryOrNull(entry, externalJsonStore);
5030
+ const entry = await readIndexedCacheEntry({
5031
+ cacheDir,
5032
+ key: keyHash,
5033
+ namespace
5034
+ });
5035
+ if (entry === null) return null;
5036
+ const materialized = await materializeExternalJsonCacheEntryOrNull(entry, externalJsonStore);
5037
+ if (materialized !== null) await updateCacheIndexLastAccessedAt(cacheDir, namespace, keyHash);
5038
+ return materialized;
4961
5039
  },
4962
5040
  async lookupWithDebug(namespace, keyHash) {
4963
- const rawEntry = await readCacheEntry(cacheDir, namespace, keyHash);
5041
+ const rawEntry = await readIndexedCacheEntry({
5042
+ cacheDir,
5043
+ key: keyHash,
5044
+ namespace
5045
+ });
4964
5046
  if (rawEntry === null) return null;
4965
5047
  const entry = await materializeExternalJsonCacheEntryOrNull(rawEntry, externalJsonStore);
4966
5048
  if (entry === null) return null;
@@ -4975,8 +5057,17 @@ function createFsCacheStore(options) {
4975
5057
  };
4976
5058
  },
4977
5059
  async write(entry, debugKey) {
4978
- const maxEntries = maxEntriesForNamespace(entry.namespace, defaultMaxEntries, options.maxEntriesByNamespace);
4979
- await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), () => writeCompressedCacheEntry(cacheDir, entry));
5060
+ await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), async () => {
5061
+ await writeCompressedCacheEntry(cacheDir, entry);
5062
+ if (!usesSupportedCacheSerialization(entry.recording)) return;
5063
+ const index = await readNamespaceIndex(cacheDir, entry.namespace);
5064
+ index.entries[entry.key] = {
5065
+ storedAt: entry.storedAt,
5066
+ lastAccessedAt: entry.storedAt,
5067
+ blobRefs: await collectExternalJsonBlobRefs(entry, blobDirs)
5068
+ };
5069
+ await writeNamespaceIndex(cacheDir, index);
5070
+ });
4980
5071
  if (debugKey !== void 0) {
4981
5072
  if ((await resultify(() => writeDebugKeyEntry({
4982
5073
  debugDir,
@@ -4987,36 +5078,11 @@ function createFsCacheStore(options) {
4987
5078
  key: entry.key
4988
5079
  }));
4989
5080
  }
4990
- await pruneEntriesForNamespace({
4991
- cacheDir,
4992
- debugDir,
4993
- namespace: entry.namespace,
4994
- maxEntries,
4995
- protectedKey: entry.key
4996
- });
4997
- await pruneExternalJsonBlobs(cacheDir, blobDir);
4998
5081
  },
4999
5082
  async list() {
5000
- const files = await listCacheEntryFiles(cacheDir);
5001
5083
  const items = [];
5002
- for (const filePath of files) {
5003
- const fileEntry = await readCacheEntryFilePath(filePath);
5004
- if (fileEntry === null || !entryMatchesPath(filePath, fileEntry.entry)) continue;
5005
- const entry = fileEntry.entry;
5006
- const operationType = entry.operationType ?? "span";
5007
- const operationName = entry.operationName ?? entry.spanName ?? entry.namespace;
5008
- items.push({
5009
- key: entry.key,
5010
- namespace: entry.namespace,
5011
- operationType,
5012
- operationName,
5013
- spanName: entry.spanName,
5014
- spanKind: entry.spanKind,
5015
- storedAt: entry.storedAt,
5016
- sizeBytes: fileEntry.sizeBytes
5017
- });
5018
- }
5019
- items.sort((a, b) => a.storedAt < b.storedAt ? 1 : -1);
5084
+ for (const index of await listCacheIndexes(cacheDir)) for (const [key, entry] of Object.entries(index.entries)) items.push(toCacheListItem(index.namespace, key, entry));
5085
+ items.sort((a, b) => a.lastAccessedAt < b.lastAccessedAt ? 1 : -1);
5020
5086
  return items;
5021
5087
  },
5022
5088
  async clear(filter) {
@@ -5029,21 +5095,46 @@ function createFsCacheStore(options) {
5029
5095
  recursive: true,
5030
5096
  force: true
5031
5097
  });
5032
- await rm(blobDir, {
5098
+ await Promise.all(blobDirs.map((dir) => rm(dir, {
5033
5099
  recursive: true,
5034
5100
  force: true
5035
- });
5101
+ })));
5036
5102
  return;
5037
5103
  }
5038
5104
  if (filter.namespace !== void 0) {
5039
5105
  await clearCacheEntries(cacheDir, filter);
5040
5106
  await clearDebugEntries(debugDir, filter);
5041
- await pruneExternalJsonBlobs(cacheDir, blobDir);
5107
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5042
5108
  return;
5043
5109
  }
5044
5110
  await clearCacheEntries(cacheDir, filter);
5045
5111
  await clearDebugEntries(debugDir, filter);
5046
- await pruneExternalJsonBlobs(cacheDir, blobDir);
5112
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5113
+ },
5114
+ async pruneExternalJsonBlobs() {
5115
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5116
+ },
5117
+ async pruneRetention() {
5118
+ for (const index_ of await listCacheIndexes(cacheDir)) {
5119
+ const namespace = index_.namespace;
5120
+ const maxEntries = maxEntriesForNamespace(namespace, defaultMaxEntries, options.maxEntriesByNamespace);
5121
+ const keptKeys = await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5122
+ return pruneCacheEntriesForNamespace({
5123
+ cacheDir,
5124
+ index: await readNamespaceIndex(cacheDir, namespace),
5125
+ maxEntries
5126
+ });
5127
+ });
5128
+ await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys));
5129
+ }
5130
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5131
+ },
5132
+ async repair() {
5133
+ return repairIndexedCache({
5134
+ blobDirs,
5135
+ cacheDir,
5136
+ debugDir
5137
+ });
5047
5138
  }
5048
5139
  };
5049
5140
  }
@@ -5126,11 +5217,31 @@ function entryPath(params) {
5126
5217
  if (filePath !== namespaceDir && !filePath.startsWith(`${namespaceDir}${sep}`)) throw new Error(`Cache entry key escapes namespace directory: ${params.key}`);
5127
5218
  return filePath;
5128
5219
  }
5129
- async function readCacheEntry(cacheDir, namespace, key) {
5130
- return (await readCacheEntryFilePath(cacheEntryPath(cacheDir, namespace, key), {
5131
- namespace,
5132
- key
5133
- }))?.entry ?? null;
5220
+ function cacheIndexPath(cacheDir, namespace) {
5221
+ return join(namespaceDirPath(cacheDir, namespace), `${cacheIndexFilePrefix}${hashNamespace(namespace)}${debugEntryExtension}`);
5222
+ }
5223
+ async function readIndexedCacheEntry(params) {
5224
+ return withCacheFileLock(namespaceLockPath(params.cacheDir, params.namespace), async () => {
5225
+ if ((await readNamespaceIndex(params.cacheDir, params.namespace)).entries[params.key] === void 0) return null;
5226
+ const fileEntry = await readCacheEntryFilePath(cacheEntryPath(params.cacheDir, params.namespace, params.key), {
5227
+ namespace: params.namespace,
5228
+ key: params.key
5229
+ });
5230
+ if (fileEntry === null) return null;
5231
+ return fileEntry.entry;
5232
+ });
5233
+ }
5234
+ async function updateCacheIndexLastAccessedAt(cacheDir, namespace, key) {
5235
+ await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5236
+ const index = await readNamespaceIndex(cacheDir, namespace);
5237
+ const entry = index.entries[key];
5238
+ if (entry === void 0) return;
5239
+ index.entries[key] = {
5240
+ ...entry,
5241
+ lastAccessedAt: new Date(getRealDateNowMs()).toISOString()
5242
+ };
5243
+ await writeNamespaceIndex(cacheDir, index);
5244
+ });
5134
5245
  }
5135
5246
  async function readCacheEntryFilePath(filePath, expected) {
5136
5247
  if (!existsSync(filePath)) return null;
@@ -5145,10 +5256,7 @@ async function readCacheEntryFilePath(filePath, expected) {
5145
5256
  const entry = parsed.data;
5146
5257
  if (!usesSupportedCacheSerialization(entry.recording)) return null;
5147
5258
  if (expected !== void 0 && (entry.namespace !== expected.namespace || entry.key !== expected.key)) return null;
5148
- return {
5149
- entry,
5150
- sizeBytes: compressedResult.value.byteLength
5151
- };
5259
+ return { entry };
5152
5260
  }
5153
5261
  async function writeCompressedCacheEntry(cacheDir, entry) {
5154
5262
  const filePath = cacheEntryPath(cacheDir, entry.namespace, entry.key);
@@ -5197,23 +5305,132 @@ async function writeAtomicFile(filePath, contents) {
5197
5305
  await writeFile(tmpPath, contents);
5198
5306
  await rename(tmpPath, filePath);
5199
5307
  }
5308
+ const emptyCacheIndex = (namespace) => ({
5309
+ version: 1,
5310
+ namespace,
5311
+ entries: {}
5312
+ });
5313
+ async function readNamespaceIndex(cacheDir, namespace) {
5314
+ const indexPath = cacheIndexPath(cacheDir, namespace);
5315
+ if (!existsSync(indexPath)) return emptyCacheIndex(namespace);
5316
+ const rawResult = await resultify(() => readFile(indexPath, "utf8"));
5317
+ if (rawResult.error) return emptyCacheIndex(namespace);
5318
+ return parseCacheIndexFile(safeJsonParse(rawResult.value), namespace) ?? emptyCacheIndex(namespace);
5319
+ }
5320
+ async function writeNamespaceIndex(cacheDir, index) {
5321
+ const entries = Object.entries(index.entries);
5322
+ if (entries.length === 0) {
5323
+ await rm(cacheIndexPath(cacheDir, index.namespace), { force: true });
5324
+ await removeDirIfEmpty(namespaceDirPath(cacheDir, index.namespace));
5325
+ return;
5326
+ }
5327
+ const sortedEntries = entries.toSorted(([a], [b]) => a < b ? -1 : 1);
5328
+ const normalizedEntries = Object.fromEntries(sortedEntries.map(([key, entry]) => [key, entry]));
5329
+ await writeAtomicFile(cacheIndexPath(cacheDir, index.namespace), JSON.stringify({
5330
+ ...index,
5331
+ entries: normalizedEntries
5332
+ }, null, 2));
5333
+ }
5334
+ async function listCacheIndexes(cacheDir) {
5335
+ if (!existsSync(cacheDir)) return [];
5336
+ const entriesResult = await resultify(() => readdir(cacheDir, { withFileTypes: true }));
5337
+ if (entriesResult.error) return [];
5338
+ const records = [];
5339
+ for (const entry of entriesResult.value) {
5340
+ if (!entry.isDirectory()) continue;
5341
+ const namespaceDir = join(cacheDir, entry.name);
5342
+ for (const indexFilePath of await listCacheIndexFiles(namespaceDir)) {
5343
+ const rawResult = await resultify(() => readFile(indexFilePath, "utf8"));
5344
+ if (rawResult.error) continue;
5345
+ const parsed = parseCacheIndexFile(safeJsonParse(rawResult.value));
5346
+ if (parsed === null) continue;
5347
+ records.push(parsed);
5348
+ }
5349
+ }
5350
+ return records;
5351
+ }
5352
+ function hashNamespace(namespace) {
5353
+ return createHash("sha256").update(namespace).digest("hex");
5354
+ }
5355
+ function parseCacheIndexFile(value, expectedNamespace) {
5356
+ if (!isRecordLike(value)) return null;
5357
+ if (value.version !== 1 || typeof value.namespace !== "string") return null;
5358
+ if (expectedNamespace !== void 0 && value.namespace !== expectedNamespace) return null;
5359
+ if (!isRecordLike(value.entries)) return null;
5360
+ const entries = {};
5361
+ for (const [key, entryValue] of Object.entries(value.entries)) {
5362
+ const entry = parseCacheIndexEntry(entryValue);
5363
+ if (entry === null) return null;
5364
+ entries[key] = entry;
5365
+ }
5366
+ return {
5367
+ version: 1,
5368
+ namespace: value.namespace,
5369
+ entries
5370
+ };
5371
+ }
5372
+ function parseCacheIndexEntry(value) {
5373
+ if (!isRecordLike(value)) return null;
5374
+ if (typeof value.storedAt !== "string" || typeof value.lastAccessedAt !== "string") return null;
5375
+ if (!Array.isArray(value.blobRefs)) return null;
5376
+ const blobRefs = [];
5377
+ for (const blobRef of value.blobRefs) {
5378
+ if (typeof blobRef !== "string") return null;
5379
+ blobRefs.push(blobRef);
5380
+ }
5381
+ return {
5382
+ storedAt: value.storedAt,
5383
+ lastAccessedAt: value.lastAccessedAt,
5384
+ blobRefs
5385
+ };
5386
+ }
5387
+ function toCacheListItem(namespace, key, entry) {
5388
+ return {
5389
+ key,
5390
+ namespace,
5391
+ storedAt: entry.storedAt,
5392
+ lastAccessedAt: entry.lastAccessedAt
5393
+ };
5394
+ }
5395
+ function keyFromEntryFilePath(filePath, extension) {
5396
+ const name = basename(filePath);
5397
+ if (!name.endsWith(extension)) return null;
5398
+ return name.slice(0, -extension.length);
5399
+ }
5400
+ function debugNamespaceFromPath(debugDir, filePath) {
5401
+ return basename(dirname(resolve(debugDir, relative(debugDir, filePath))));
5402
+ }
5200
5403
  async function clearCacheEntries(cacheDir, filter) {
5201
- const files = filter.namespace === void 0 ? await listCacheEntryFiles(cacheDir) : await listCacheEntryFiles(namespaceDirPath(cacheDir, filter.namespace));
5202
- for (const filePath of files) {
5203
- const fileEntry = await readCacheEntryFilePath(filePath);
5204
- if (fileEntry === null) continue;
5205
- const entry = fileEntry.entry;
5206
- if (!entryMatchesFilter(entry, filter)) continue;
5207
- await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), () => rm(filePath, { force: true }));
5404
+ const indexes = filter.namespace === void 0 ? await listCacheIndexes(cacheDir) : [await readNamespaceIndex(cacheDir, filter.namespace)];
5405
+ for (const record of indexes) {
5406
+ const namespace = record.namespace;
5407
+ await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5408
+ const index = await readNamespaceIndex(cacheDir, namespace);
5409
+ const matchingKeys = Object.keys(index.entries).filter((key) => {
5410
+ return index.entries[key] !== void 0 && entryMatchesFilter({
5411
+ namespace,
5412
+ key
5413
+ }, filter);
5414
+ });
5415
+ for (const key of matchingKeys) {
5416
+ await rm(cacheEntryPath(cacheDir, namespace, key), { force: true });
5417
+ delete index.entries[key];
5418
+ }
5419
+ await writeNamespaceIndex(cacheDir, index);
5420
+ });
5208
5421
  }
5209
- if (filter.namespace !== void 0) await removeDirIfEmpty(namespaceDirPath(cacheDir, filter.namespace));
5210
5422
  }
5211
5423
  async function clearDebugEntries(debugDir, filter) {
5212
- const files = filter.namespace === void 0 ? await listDebugEntryFiles(debugDir) : await listDebugEntryFiles(namespaceDirPath(debugDir, filter.namespace));
5424
+ const files = await listDebugEntryFiles(filter.namespace === void 0 ? debugDir : namespaceDirPath(debugDir, filter.namespace));
5213
5425
  for (const filePath of files) {
5214
- const entry = await readDebugEntryFilePath(filePath);
5215
- if (entry === null || !entryMatchesFilter(entry, filter)) continue;
5216
- await withCacheFileLock(namespaceLockPath(debugDir, entry.namespace), () => rm(filePath, { force: true }));
5426
+ const namespace = filter.namespace === void 0 ? debugNamespaceFromPath(debugDir, filePath) : filter.namespace;
5427
+ const key = keyFromEntryFilePath(filePath, debugEntryExtension);
5428
+ if (key === null) continue;
5429
+ if (!entryMatchesFilter({
5430
+ namespace,
5431
+ key
5432
+ }, filter)) continue;
5433
+ await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => rm(filePath, { force: true }));
5217
5434
  }
5218
5435
  if (filter.namespace !== void 0) await removeDirIfEmpty(namespaceDirPath(debugDir, filter.namespace));
5219
5436
  }
@@ -5221,49 +5438,86 @@ function entryMatchesFilter(entry, filter) {
5221
5438
  if (filter.namespace !== void 0 && entry.namespace !== filter.namespace) return false;
5222
5439
  return filter.key === void 0 || entry.key === filter.key;
5223
5440
  }
5224
- async function pruneEntriesForNamespace(params) {
5225
- const { cacheDir, debugDir, namespace, maxEntries, protectedKey } = params;
5226
- await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5227
- const keptKeys = await pruneCacheEntriesForNamespace(cacheDir, namespace, maxEntries, protectedKey);
5228
- await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys));
5229
- });
5230
- }
5231
- async function pruneCacheEntriesForNamespace(cacheDir, namespace, maxEntries, protectedKey) {
5232
- const entries = await listCacheEntriesForNamespace(cacheDir, namespace);
5233
- const sorted = entries.toSorted((a, b) => a.entry.storedAt < b.entry.storedAt ? 1 : -1);
5441
+ async function pruneCacheEntriesForNamespace(params) {
5442
+ const { cacheDir, index, maxEntries } = params;
5443
+ const entries = Object.entries(index.entries);
5444
+ const sorted = entries.toSorted(([, a], [, b]) => a.lastAccessedAt < b.lastAccessedAt ? 1 : -1);
5234
5445
  const keptKeys = /* @__PURE__ */ new Set();
5235
- const protectedEntry = entries.find((item) => item.entry.key === protectedKey);
5236
- if (protectedEntry !== void 0) keptKeys.add(protectedEntry.entry.key);
5237
- for (const item of sorted) {
5446
+ for (const [key] of sorted) {
5238
5447
  if (keptKeys.size >= maxEntries) break;
5239
- keptKeys.add(item.entry.key);
5448
+ keptKeys.add(key);
5240
5449
  }
5241
- for (const item of entries) if (!keptKeys.has(item.entry.key)) await rm(item.filePath, { force: true });
5242
- await removeDirIfEmpty(namespaceDirPath(cacheDir, namespace));
5450
+ for (const [key] of entries) if (!keptKeys.has(key)) {
5451
+ await rm(cacheEntryPath(cacheDir, index.namespace, key), { force: true });
5452
+ delete index.entries[key];
5453
+ }
5454
+ await writeNamespaceIndex(cacheDir, index);
5243
5455
  return keptKeys;
5244
5456
  }
5245
5457
  async function pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys) {
5246
5458
  const files = await listDebugEntryFiles(namespaceDirPath(debugDir, namespace));
5247
5459
  for (const filePath of files) {
5248
- const entry = await readDebugEntryFilePath(filePath);
5249
- if (entry !== null && entry.namespace === namespace && !keptKeys.has(entry.key)) await rm(filePath, { force: true });
5460
+ const key = keyFromEntryFilePath(filePath, debugEntryExtension);
5461
+ if (key !== null && !keptKeys.has(key)) await rm(filePath, { force: true });
5250
5462
  }
5251
5463
  await removeDirIfEmpty(namespaceDirPath(debugDir, namespace));
5252
5464
  }
5253
- async function listCacheEntriesForNamespace(cacheDir, namespace) {
5254
- const files = await listCacheEntryFiles(namespaceDirPath(cacheDir, namespace));
5255
- const entries = [];
5256
- for (const filePath of files) {
5257
- const fileEntry = await readCacheEntryFilePath(filePath);
5258
- if (fileEntry !== null && fileEntry.entry.namespace === namespace && entryMatchesPath(filePath, fileEntry.entry)) entries.push({
5259
- filePath,
5260
- entry: fileEntry.entry
5465
+ async function repairIndexedCache(params) {
5466
+ const summary = {
5467
+ removedCacheFiles: 0,
5468
+ removedDebugFiles: 0,
5469
+ removedBlobFiles: 0,
5470
+ removedIndexRows: 0,
5471
+ rewrittenIndexes: 0
5472
+ };
5473
+ for (const index_ of await listCacheIndexes(params.cacheDir)) {
5474
+ const result = await withCacheFileLock(namespaceLockPath(params.cacheDir, index_.namespace), async () => {
5475
+ const index = await readNamespaceIndex(params.cacheDir, index_.namespace);
5476
+ let removedRows = 0;
5477
+ for (const key of Object.keys(index.entries)) if (!existsSync(cacheEntryPath(params.cacheDir, index.namespace, key))) {
5478
+ delete index.entries[key];
5479
+ removedRows++;
5480
+ }
5481
+ if (removedRows === 0) return {
5482
+ removedRows,
5483
+ rewritten: false
5484
+ };
5485
+ await writeNamespaceIndex(params.cacheDir, index);
5486
+ return {
5487
+ removedRows,
5488
+ rewritten: true
5489
+ };
5261
5490
  });
5491
+ summary.removedIndexRows += result.removedRows;
5492
+ if (result.rewritten) summary.rewrittenIndexes++;
5262
5493
  }
5263
- return entries;
5264
- }
5265
- function entryMatchesPath(filePath, entry) {
5266
- return basename(filePath) === `${entry.key}${cacheEntryExtension}` && basename(dirname(filePath)) === sanitizeSegment$1(entry.namespace);
5494
+ const indexes = await listCacheIndexes(params.cacheDir);
5495
+ const indexedCacheFiles = /* @__PURE__ */ new Set();
5496
+ const indexedDebugFiles = /* @__PURE__ */ new Set();
5497
+ const indexedBlobRefs = /* @__PURE__ */ new Set();
5498
+ for (const index_ of indexes) for (const [key, entry] of Object.entries(index_.entries)) {
5499
+ indexedCacheFiles.add(cacheEntryPath(params.cacheDir, index_.namespace, key));
5500
+ indexedDebugFiles.add(debugEntryPath(params.debugDir, index_.namespace, key));
5501
+ for (const blobRef of entry.blobRefs) indexedBlobRefs.add(blobRef);
5502
+ }
5503
+ for (const filePath of await listCacheEntryFiles(params.cacheDir, "allNamespaces")) if (!indexedCacheFiles.has(filePath)) {
5504
+ await rm(filePath, { force: true });
5505
+ summary.removedCacheFiles++;
5506
+ await removeDirIfEmpty(dirname(filePath));
5507
+ }
5508
+ for (const filePath of await listDebugEntryFiles(params.debugDir)) if (!indexedDebugFiles.has(filePath)) {
5509
+ await rm(filePath, { force: true });
5510
+ summary.removedDebugFiles++;
5511
+ await removeDirIfEmpty(dirname(filePath));
5512
+ }
5513
+ for (const blobDir of params.blobDirs) {
5514
+ if (!existsSync(blobDir)) continue;
5515
+ for (const blobRef of await listExternalJsonBlobPaths(blobDir)) if (!indexedBlobRefs.has(blobRef)) {
5516
+ await rm(resolveStorePath(blobDir, blobRef), { force: true });
5517
+ summary.removedBlobFiles++;
5518
+ }
5519
+ }
5520
+ return summary;
5267
5521
  }
5268
5522
  function usesSupportedCacheSerialization(value) {
5269
5523
  if (Array.isArray(value)) return value.every(usesSupportedCacheSerialization);
@@ -5271,14 +5525,14 @@ function usesSupportedCacheSerialization(value) {
5271
5525
  if (Object.hasOwn(value, cacheSerializationMarker) && (typeof value[cacheSerializationMarker] !== "string" || !value[cacheSerializationMarker].startsWith(supportedCacheSerializationPrefix))) return false;
5272
5526
  return Object.values(value).every(usesSupportedCacheSerialization);
5273
5527
  }
5274
- function createExternalJsonBlobStore(blobDir) {
5528
+ function createExternalJsonBlobStore(params) {
5275
5529
  return {
5276
5530
  async write(rawJson) {
5277
5531
  const rawBytes = Buffer.from(rawJson, "utf8");
5278
5532
  const hash = hashExternalJson(rawBytes);
5279
5533
  const path = externalJsonBlobPath(hash);
5280
5534
  const compressed = brotliCompressSync(rawBytes);
5281
- const filePath = resolveStorePath(blobDir, path);
5535
+ const filePath = resolveStorePath(params.primaryDir, path);
5282
5536
  if (!existsSync(filePath)) await writeAtomicFile(filePath, compressed);
5283
5537
  return {
5284
5538
  compressedLength: compressed.byteLength,
@@ -5288,10 +5542,15 @@ function createExternalJsonBlobStore(blobDir) {
5288
5542
  };
5289
5543
  },
5290
5544
  async read(ref) {
5291
- const rawBytes = brotliDecompressSync(await readFile(resolveStorePath(blobDir, ref.path)));
5292
- const rawJson = rawBytes.toString("utf8");
5293
- if (rawBytes.byteLength !== ref.length || hashExternalJson(rawBytes) !== ref.hash) throw new Error(`External cache blob failed integrity check: ${ref.hash}`);
5294
- return rawJson;
5545
+ for (const dir of [params.primaryDir, ...params.fallbackDirs]) {
5546
+ const compressedResult = await resultify(() => readFile(resolveStorePath(dir, ref.path)));
5547
+ if (compressedResult.error) continue;
5548
+ const rawBytesResult = resultify(() => brotliDecompressSync(compressedResult.value));
5549
+ if (rawBytesResult.error) continue;
5550
+ const rawBytes = rawBytesResult.value;
5551
+ if (rawBytes.byteLength === ref.length && hashExternalJson(rawBytes) === ref.hash) return rawBytes.toString("utf8");
5552
+ }
5553
+ throw new Error(`External cache blob failed integrity check: ${ref.hash}`);
5295
5554
  }
5296
5555
  };
5297
5556
  }
@@ -5317,28 +5576,55 @@ async function materializeExternalJsonCacheEntryOrNull(entry, store) {
5317
5576
  const result = await resultify(() => materializeExternalJsonCacheEntry(entry, store));
5318
5577
  return result.error ? null : result.value;
5319
5578
  }
5320
- async function pruneExternalJsonBlobs(cacheDir, blobDir) {
5321
- if (!existsSync(blobDir)) return;
5579
+ async function pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs) {
5322
5580
  const referenced = await collectReferencedExternalJsonBlobPaths(cacheDir);
5323
- for (const path of await listExternalJsonBlobPaths(blobDir)) if (!referenced.has(path)) await rm(resolveStorePath(blobDir, path), { force: true });
5581
+ for (const blobDir of blobDirs) {
5582
+ if (!existsSync(blobDir)) continue;
5583
+ for (const path of await listExternalJsonBlobPaths(blobDir)) if (!referenced.has(path)) await rm(resolveStorePath(blobDir, path), { force: true });
5584
+ }
5324
5585
  }
5325
5586
  async function collectReferencedExternalJsonBlobPaths(cacheDir) {
5326
5587
  const paths = /* @__PURE__ */ new Set();
5327
- for (const filePath of await listCacheEntryFiles(cacheDir)) {
5328
- const fileEntry = await readCacheEntryFilePath(filePath);
5329
- if (fileEntry === null || !entryMatchesPath(filePath, fileEntry.entry)) continue;
5330
- collectExternalJsonBlobPaths(fileEntry.entry, paths);
5331
- }
5588
+ for (const index_ of await listCacheIndexes(cacheDir)) for (const entry of Object.values(index_.entries)) for (const blobRef of entry.blobRefs) paths.add(blobRef);
5332
5589
  return paths;
5333
5590
  }
5334
- function collectExternalJsonBlobPaths(value, paths) {
5591
+ async function collectExternalJsonBlobRefs(value, blobDirs) {
5592
+ const paths = /* @__PURE__ */ new Set();
5593
+ const pendingBlobPaths = [];
5594
+ collectExternalJsonBlobPaths(value, paths, pendingBlobPaths);
5595
+ while (pendingBlobPaths.length > 0) {
5596
+ const blobPath = pendingBlobPaths.pop();
5597
+ if (blobPath === void 0) continue;
5598
+ const rawJson = await readExternalJsonBlobByPath(blobDirs, blobPath);
5599
+ if (rawJson === null) continue;
5600
+ const json = safeJsonParse(rawJson);
5601
+ if (json === null) continue;
5602
+ collectExternalJsonBlobPaths(json, paths, pendingBlobPaths);
5603
+ }
5604
+ return [...paths].sort();
5605
+ }
5606
+ function collectExternalJsonBlobPaths(value, paths, pendingBlobPaths) {
5335
5607
  if (Array.isArray(value)) {
5336
- for (const item of value) collectExternalJsonBlobPaths(item, paths);
5608
+ for (const item of value) collectExternalJsonBlobPaths(item, paths, pendingBlobPaths);
5337
5609
  return;
5338
5610
  }
5339
5611
  if (!isRecordLike(value)) return;
5340
- if (value[cacheSerializationMarker] === externalJsonCacheSerializationMarker && typeof value.path === "string") paths.add(value.path);
5341
- for (const entryValue of Object.values(value)) collectExternalJsonBlobPaths(entryValue, paths);
5612
+ if (value[cacheSerializationMarker] === externalJsonCacheSerializationMarker && typeof value.path === "string") {
5613
+ if (!paths.has(value.path)) {
5614
+ paths.add(value.path);
5615
+ pendingBlobPaths.push(value.path);
5616
+ }
5617
+ }
5618
+ for (const entryValue of Object.values(value)) collectExternalJsonBlobPaths(entryValue, paths, pendingBlobPaths);
5619
+ }
5620
+ async function readExternalJsonBlobByPath(blobDirs, path) {
5621
+ for (const blobDir of blobDirs) {
5622
+ const compressedResult = await resultify(() => readFile(resolveStorePath(blobDir, path)));
5623
+ if (compressedResult.error) continue;
5624
+ const rawResult = resultify(() => brotliDecompressSync(compressedResult.value).toString("utf8"));
5625
+ if (!rawResult.error) return rawResult.value;
5626
+ }
5627
+ return null;
5342
5628
  }
5343
5629
  async function listExternalJsonBlobPaths(blobDir) {
5344
5630
  const paths = [];
@@ -5357,12 +5643,33 @@ async function collectExternalJsonBlobFilePaths(root, dir, paths) {
5357
5643
  if (entry.isFile() && entry.name.endsWith(externalJsonBlobExtension)) paths.push(relative(root, path));
5358
5644
  }
5359
5645
  }
5360
- async function listCacheEntryFiles(rootDir) {
5361
- return listFilesWithExtension(rootDir, cacheEntryExtension);
5646
+ async function listCacheEntryFiles(rootDir, scope) {
5647
+ if (scope === "namespace") return listDirectFilesWithExtension(rootDir, cacheEntryExtension);
5648
+ if (!existsSync(rootDir)) return [];
5649
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5650
+ if (entriesResult.error) return [];
5651
+ const files = [];
5652
+ for (const entry of entriesResult.value) {
5653
+ if (!entry.isDirectory()) continue;
5654
+ files.push(...await listDirectFilesWithExtension(join(rootDir, entry.name), cacheEntryExtension));
5655
+ }
5656
+ return files;
5362
5657
  }
5363
5658
  async function listDebugEntryFiles(rootDir) {
5364
5659
  return listFilesWithExtension(rootDir, debugEntryExtension);
5365
5660
  }
5661
+ async function listCacheIndexFiles(rootDir) {
5662
+ if (!existsSync(rootDir)) return [];
5663
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5664
+ if (entriesResult.error) return [];
5665
+ return entriesResult.value.filter((entry) => entry.isFile() && entry.name.startsWith(cacheIndexFilePrefix) && entry.name.endsWith(debugEntryExtension)).map((entry) => join(rootDir, entry.name));
5666
+ }
5667
+ async function listDirectFilesWithExtension(rootDir, extension) {
5668
+ if (!existsSync(rootDir)) return [];
5669
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5670
+ if (entriesResult.error) return [];
5671
+ return entriesResult.value.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => join(rootDir, entry.name));
5672
+ }
5366
5673
  async function listFilesWithExtension(rootDir, extension) {
5367
5674
  if (!existsSync(rootDir)) return [];
5368
5675
  const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
@@ -5396,6 +5703,7 @@ async function withCacheFileLock(filePath, fn) {
5396
5703
  force: true
5397
5704
  });
5398
5705
  if (result.error) throw result.error;
5706
+ return result.value;
5399
5707
  }
5400
5708
  async function acquireLock(lockPath) {
5401
5709
  const startedAt = Date.now();
@@ -5509,6 +5817,27 @@ function buildDeclaredColumnDefs(overrides, scores, manualScores) {
5509
5817
  return [...declaredDefs.values()];
5510
5818
  }
5511
5819
  /**
5820
+ * Build runtime column definitions from output-level display overrides.
5821
+ *
5822
+ * These definitions are persisted on case rows/details so `setOutput(...)`
5823
+ * can format one-off outputs without adding them to eval discovery metadata.
5824
+ */
5825
+ function buildRuntimeOutputColumnDefs(columns, overrides, configuredColumnKeys = /* @__PURE__ */ new Set()) {
5826
+ return Object.entries(overrides).filter(([key]) => columns[key] !== void 0 && !configuredColumnKeys.has(key)).map(([key, override]) => createColumnDef({
5827
+ key,
5828
+ override,
5829
+ inferredKind: inferKindFromFormat(override.format) ?? (override.numberFormat === void 0 ? inferKind(columns[key]) : "number"),
5830
+ isScore: false,
5831
+ isManualScore: false
5832
+ }));
5833
+ }
5834
+ /** Infer a `ColumnKind` from a runtime value when no override is set. */
5835
+ function inferKind(value) {
5836
+ if (typeof value === "number") return "number";
5837
+ if (typeof value === "boolean") return "boolean";
5838
+ return "string";
5839
+ }
5840
+ /**
5512
5841
  * Coerce an arbitrary runtime value into a serializable `CellValue`.
5513
5842
  * Runtime values use the SDK's tagged serializer so saved run artifacts keep
5514
5843
  * structured data instead of storing JSON strings. Native binary/file root
@@ -6277,7 +6606,7 @@ async function runDeriveFromTracingConfig(params) {
6277
6606
  }
6278
6607
  }
6279
6608
  async function runCase(params) {
6280
- const { evalDef, evalId, evalKey = evalId, evalCase, globalTraceDisplay, globalDeriveFromTracing, llmCallsConfig = resolveLlmCallsConfig(void 0), apiCallsConfig = resolveApiCallsConfig(void 0), globalRemoveDefaultConfig, trial, startTime, cacheAdapter, cacheMode, moduleIsolation, evalFilePath, evalFileRelativePath = evalFilePath, workspaceRoot, artifactDir, runId } = params;
6609
+ const { evalDef, evalId, evalKey = evalId, evalCase, globalTraceDisplay, globalColumns, globalDeriveFromTracing, llmCallsConfig = resolveLlmCallsConfig(void 0), apiCallsConfig = resolveApiCallsConfig(void 0), globalRemoveDefaultConfig, trial, startTime, cacheAdapter, cacheMode, moduleIsolation, evalFilePath, evalFileRelativePath = evalFilePath, workspaceRoot, artifactDir, runId } = params;
6281
6610
  const scopedIdPrefix = buildScopedEvalIdPrefix({
6282
6611
  evalId,
6283
6612
  evalFilePath,
@@ -6445,6 +6774,12 @@ async function runCase(params) {
6445
6774
  if (cell !== void 0) columns[key] = cell;
6446
6775
  }
6447
6776
  for (const key of Object.keys(evalDef.manualScores ?? {})) columns[key] = null;
6777
+ const outputColumnDefs = buildRuntimeOutputColumnDefs(columns, scope.outputColumnOverrides, new Set(Object.keys(mergeDefaultColumns({
6778
+ globalColumns,
6779
+ columns: evalDef.columns,
6780
+ globalRemove: globalRemoveDefaultConfig,
6781
+ evalRemove: evalDef.removeDefaultConfig
6782
+ }) ?? {})));
6448
6783
  const errorInfo = nonAssertError ? {
6449
6784
  name: nonAssertError.name,
6450
6785
  message: nonAssertError.message,
@@ -6461,6 +6796,7 @@ async function runCase(params) {
6461
6796
  trace: displayTrace,
6462
6797
  traceDisplay,
6463
6798
  columns,
6799
+ ...outputColumnDefs.length > 0 ? { outputColumnDefs } : {},
6464
6800
  assertionFailures: scope.assertionFailures,
6465
6801
  logs: scope.logs,
6466
6802
  error: errorInfo,
@@ -6479,7 +6815,8 @@ async function runCase(params) {
6479
6815
  durationMs: elapsedMs,
6480
6816
  cacheHits: cacheHits.length,
6481
6817
  cacheOperations: cacheEntries.length,
6482
- columns
6818
+ columns,
6819
+ ...outputColumnDefs.length > 0 ? { outputColumnDefs } : {}
6483
6820
  }
6484
6821
  };
6485
6822
  }