@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.
- package/dist/{app-BD0D9-7k.mjs → app-Bpe6Monh.mjs} +4 -4
- package/dist/apps/web/dist/assets/index-CfSiAVmi.js +377 -0
- package/dist/apps/web/dist/assets/{index-2I-eWzVL.css → index-Xa_7PteQ.css} +1 -1
- package/dist/apps/web/dist/index.html +2 -2
- package/dist/bin.mjs +1 -1
- package/dist/caseChild.mjs +2 -1
- package/dist/{cli-BR3wMZMx.mjs → cli-DQO2Fpt2.mjs} +60 -13
- package/dist/index.d.mts +610 -62
- package/dist/index.mjs +3 -3
- package/dist/runChild.mjs +2 -2
- package/dist/{runExecution-Sw38bCaq.mjs → runExecution-6lrtj48K.mjs} +458 -121
- package/dist/{runOrchestration-DJsdLYeZ.mjs → runOrchestration-BYaN2mzS.mjs} +1 -1
- package/dist/{runner-dB69WsnM.mjs → runner-C3CiS2o7.mjs} +1 -1
- package/dist/{runner-72rsqJRq.mjs → runner-DYlwuAT3.mjs} +2 -2
- package/dist/{src-hBGtzWuA.mjs → src-DCGrFAmO.mjs} +2 -2
- package/package.json +3 -3
- package/skills/agent-eval/SKILL.md +29 -10
- package/dist/apps/web/dist/assets/index-CvsPmlHl.js +0 -377
|
@@ -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
|
-
|
|
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
|
|
4945
|
-
const
|
|
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
|
|
4960
|
-
|
|
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
|
|
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
|
-
|
|
4979
|
-
|
|
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
|
|
5003
|
-
|
|
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(
|
|
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
|
|
5107
|
+
await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
|
|
5042
5108
|
return;
|
|
5043
5109
|
}
|
|
5044
5110
|
await clearCacheEntries(cacheDir, filter);
|
|
5045
5111
|
await clearDebugEntries(debugDir, filter);
|
|
5046
|
-
await
|
|
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
|
-
|
|
5130
|
-
return (
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
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
|
|
5202
|
-
for (const
|
|
5203
|
-
const
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
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 ?
|
|
5424
|
+
const files = await listDebugEntryFiles(filter.namespace === void 0 ? debugDir : namespaceDirPath(debugDir, filter.namespace));
|
|
5213
5425
|
for (const filePath of files) {
|
|
5214
|
-
const
|
|
5215
|
-
|
|
5216
|
-
|
|
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
|
|
5225
|
-
const { cacheDir,
|
|
5226
|
-
|
|
5227
|
-
|
|
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
|
-
|
|
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(
|
|
5448
|
+
keptKeys.add(key);
|
|
5240
5449
|
}
|
|
5241
|
-
for (const
|
|
5242
|
-
|
|
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
|
|
5249
|
-
if (
|
|
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
|
|
5254
|
-
const
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
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
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
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(
|
|
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(
|
|
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
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
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
|
|
5321
|
-
if (!existsSync(blobDir)) return;
|
|
5579
|
+
async function pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs) {
|
|
5322
5580
|
const referenced = await collectReferencedExternalJsonBlobPaths(cacheDir);
|
|
5323
|
-
for (const
|
|
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
|
|
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
|
|
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")
|
|
5341
|
-
|
|
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
|
|
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
|
}
|