@leostera/evalkit 0.10.0 → 0.11.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/CHANGELOG.md +67 -1
- package/dist/cli.mjs +182 -73
- package/dist/dashboard/assets/index-CFJzcw3o.css +2 -0
- package/dist/dashboard/assets/{index-DNR4shrM.js → index-DgoX2x_M.js} +34 -32
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-B1FeXzmS.mjs → index-db-DI7xKMSX.mjs} +237 -47
- package/dist/index.d.mts +837 -220
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +241 -52
- package/dist/runner.d.mts +568 -227
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +1 -1
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.d.mts +9 -6
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +2 -2
- package/dist/skills/evalkit-agents/SKILL.md +3 -3
- package/dist/skills/evalkit-datasets/SKILL.md +1 -1
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +1 -1
- package/dist/skills/evalkit-results/SKILL.md +1 -1
- package/dist/{src-Bccv9igQ.mjs → src-CXzbHAKN.mjs} +1 -1
- package/dist/{src-B14fXAGp.mjs → src-D6Ax2t9b.mjs} +379 -206
- package/dist/src-Dc3GXacB.mjs +3637 -0
- package/dist/upgrade-rMsGzUEJ.mjs +230 -0
- package/dist/upgrade-transcript-C8DUykLd.mjs +402 -0
- package/package.json +2 -1
- package/dist/dashboard/assets/index-TPYprg75.css +0 -2
- package/dist/src-BfSQKOJP.mjs +0 -1877
- package/dist/upgrade-G-uFPJKg.mjs +0 -111
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>EvalKit</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DgoX2x_M.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CFJzcw3o.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as upgradeManifest, i as upgradeEvent, o as upgradeStatus, r as upgradeCatalog } from "./upgrade-
|
|
1
|
+
import { a as upgradeManifest, i as upgradeEvent, o as upgradeStatus, r as upgradeCatalog } from "./upgrade-rMsGzUEJ.mjs";
|
|
2
|
+
import { n as upgradeScoring, t as convertTrajectory } from "./upgrade-transcript-C8DUykLd.mjs";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
//#region ../share/src/bundle.ts
|
|
4
5
|
/**
|
|
@@ -82,40 +83,40 @@ function escapeAttr(value) {
|
|
|
82
83
|
}
|
|
83
84
|
//#endregion
|
|
84
85
|
//#region ../share/src/legacy.ts
|
|
85
|
-
const decoder = new TextDecoder();
|
|
86
|
-
const encoder = new TextEncoder();
|
|
87
|
-
const parse = (data) => data ? JSON.parse(decoder.decode(data)) : void 0;
|
|
88
|
-
const encode = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
|
|
89
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
const decoder$1 = new TextDecoder();
|
|
87
|
+
const encoder$1 = new TextEncoder();
|
|
88
|
+
const parse$1 = (data) => data ? JSON.parse(decoder$1.decode(data)) : void 0;
|
|
89
|
+
const encode$1 = (value) => encoder$1.encode(`${JSON.stringify(value)}\n`);
|
|
90
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90
91
|
/** True for a version 1 run.json: no `snapshotVersion`. */
|
|
91
92
|
function isLegacyRun(runJson) {
|
|
92
|
-
return isRecord(runJson) && runJson.snapshotVersion === void 0;
|
|
93
|
+
return isRecord$1(runJson) && runJson.snapshotVersion === void 0;
|
|
93
94
|
}
|
|
94
95
|
function upgradeSnapshotRun(files) {
|
|
95
|
-
const run = parse(files.get("run.json"));
|
|
96
|
-
if (!isRecord(run)) throw new Error("run.json must be an object");
|
|
97
|
-
const listed = parse(files.get("trials.json"));
|
|
98
|
-
const trials = (isRecord(listed) && Array.isArray(listed.trials) ? listed.trials : []).filter(isRecord);
|
|
96
|
+
const run = parse$1(files.get("run.json"));
|
|
97
|
+
if (!isRecord$1(run)) throw new Error("run.json must be an object");
|
|
98
|
+
const listed = parse$1(files.get("trials.json"));
|
|
99
|
+
const trials = (isRecord$1(listed) && Array.isArray(listed.trials) ? listed.trials : []).filter(isRecord$1);
|
|
99
100
|
const size = trials.length;
|
|
100
101
|
return trials.map((trial) => {
|
|
101
102
|
const id = size === 1 ? String(run.id) : String(trial.id);
|
|
102
103
|
const prefix = `trials/${String(trial.id)}/`;
|
|
103
|
-
const detail = parse(files.get(`${prefix}detail.json`));
|
|
104
|
-
const trialManifest = isRecord(detail) && isRecord(detail.manifest) ? detail.manifest : {};
|
|
105
|
-
const trialSummary = isRecord(detail) && isRecord(detail.summary) ? detail.summary : {};
|
|
104
|
+
const detail = parse$1(files.get(`${prefix}detail.json`));
|
|
105
|
+
const trialManifest = isRecord$1(detail) && isRecord$1(detail.manifest) ? detail.manifest : {};
|
|
106
|
+
const trialSummary = isRecord$1(detail) && isRecord$1(detail.summary) ? detail.summary : {};
|
|
106
107
|
const index = typeof trial.index === "number" ? trial.index : trials.indexOf(trial);
|
|
107
108
|
const batch = size > 1 ? {
|
|
108
109
|
id: String(run.id),
|
|
109
110
|
index,
|
|
110
111
|
size
|
|
111
112
|
} : void 0;
|
|
112
|
-
const scores = Array.isArray(trial.scores) ? trial.scores.filter(isRecord) : [];
|
|
113
|
-
const metrics = Array.isArray(trial.metrics) ? trial.metrics.filter(isRecord) : [];
|
|
114
|
-
const datasets = Array.isArray(trialManifest.datasets) ? trialManifest.datasets.filter(isRecord) : [];
|
|
113
|
+
const scores = Array.isArray(trial.scores) ? trial.scores.filter(isRecord$1) : [];
|
|
114
|
+
const metrics = Array.isArray(trial.metrics) ? trial.metrics.filter(isRecord$1) : [];
|
|
115
|
+
const datasets = Array.isArray(trialManifest.datasets) ? trialManifest.datasets.filter(isRecord$1) : [];
|
|
115
116
|
const status = String(trial.status);
|
|
116
117
|
const pick = (keys) => Object.fromEntries(keys.flatMap((key) => run[key] === void 0 ? [] : [[key, run[key]]]));
|
|
117
118
|
const out = /* @__PURE__ */ new Map();
|
|
118
|
-
out.set("run.json", encode({
|
|
119
|
+
out.set("run.json", encode$1({
|
|
119
120
|
snapshotVersion: 2,
|
|
120
121
|
id,
|
|
121
122
|
...pick([
|
|
@@ -153,7 +154,7 @@ function upgradeSnapshotRun(files) {
|
|
|
153
154
|
})) } : {}
|
|
154
155
|
}));
|
|
155
156
|
const passed = trialSummary.scoring?.passed;
|
|
156
|
-
out.set("detail.json", encode({
|
|
157
|
+
out.set("detail.json", encode$1({
|
|
157
158
|
scores,
|
|
158
159
|
...metrics.length ? { metrics } : {},
|
|
159
160
|
checkpoints: Array.isArray(trial.checkpoints) ? trial.checkpoints : [],
|
|
@@ -181,8 +182,8 @@ function upgradeSnapshotRun(files) {
|
|
|
181
182
|
const rest = path.slice(prefix.length);
|
|
182
183
|
if (rest === "detail.json") continue;
|
|
183
184
|
if (rest === "events.json") {
|
|
184
|
-
const events = parse(data);
|
|
185
|
-
out.set(rest, encode({ events: isRecord(events) && Array.isArray(events.events) ? events.events.map(upgradeEvent) : [] }));
|
|
185
|
+
const events = parse$1(data);
|
|
186
|
+
out.set(rest, encode$1({ events: isRecord$1(events) && Array.isArray(events.events) ? events.events.map(upgradeEvent) : [] }));
|
|
186
187
|
} else out.set(rest, data);
|
|
187
188
|
}
|
|
188
189
|
return {
|
|
@@ -193,12 +194,107 @@ function upgradeSnapshotRun(files) {
|
|
|
193
194
|
}
|
|
194
195
|
/** A stored or uploaded version 1 catalog, as version 2 stores it. */
|
|
195
196
|
function upgradeCatalogFile(data) {
|
|
196
|
-
return encode(upgradeCatalog(parse(data)));
|
|
197
|
+
return encode$1(upgradeCatalog(parse$1(data)));
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region ../share/src/transcripts.ts
|
|
201
|
+
/**
|
|
202
|
+
* Converts one run in snapshot format version 2 (EvalKit 0.9 and 0.10) to
|
|
203
|
+
* version 3 (RFD0009): `events.json`, its trajectory, becomes
|
|
204
|
+
* `transcript.json`, an ATXF transcript with EvalKit's events placed in it;
|
|
205
|
+
* `detail.json` points into it, and token metrics no longer count cache
|
|
206
|
+
* reads, in `detail.json` and `run.json` alike. Pure: the Worker runs it on
|
|
207
|
+
* upload, in `POST /api/v1/upgrade`, and on read for runs not upgraded yet,
|
|
208
|
+
* so all three give the same files.
|
|
209
|
+
*
|
|
210
|
+
* Throws when the run cannot be converted, such as tokens that do not add
|
|
211
|
+
* up; the caller keeps the run as it was.
|
|
212
|
+
*/
|
|
213
|
+
function upgradeV2Run(files) {
|
|
214
|
+
const run = record$1(parse(files.get("run.json")), "run.json");
|
|
215
|
+
const detail = record$1(parse(files.get("detail.json")) ?? {}, "detail.json");
|
|
216
|
+
const events = parse(files.get("events.json"));
|
|
217
|
+
const trajectory = isRecord(events) && Array.isArray(events.events) ? events.events : [];
|
|
218
|
+
const manifest = isRecord(detail.manifest) ? detail.manifest : {};
|
|
219
|
+
const runId = String(run.id);
|
|
220
|
+
const startedAt = typeof run.startedAt === "string" ? run.startedAt : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
221
|
+
const aut = isRecord(manifest.aut) ? manifest.aut : void 0;
|
|
222
|
+
const converted = convertTrajectory(trajectory, {
|
|
223
|
+
runId,
|
|
224
|
+
...typeof run.evalId === "string" ? { evalId: run.evalId } : {},
|
|
225
|
+
startedAt,
|
|
226
|
+
...aut ? { agent: {
|
|
227
|
+
...typeof aut.id === "string" ? { id: aut.id } : {},
|
|
228
|
+
...typeof aut.version === "string" ? { version: aut.version } : {}
|
|
229
|
+
} } : {}
|
|
230
|
+
});
|
|
231
|
+
const upgrade = (scoring) => upgradeScoring(scoring, {
|
|
232
|
+
runId,
|
|
233
|
+
startedAt
|
|
234
|
+
}, converted.recordOfEvent, converted.transcript);
|
|
235
|
+
const scoring = upgrade({
|
|
236
|
+
results: Array.isArray(detail.scores) ? detail.scores : [],
|
|
237
|
+
...Array.isArray(detail.metrics) ? { metrics: detail.metrics } : {},
|
|
238
|
+
...Array.isArray(detail.checkpoints) ? { checkpoints: detail.checkpoints } : {},
|
|
239
|
+
passed: false
|
|
240
|
+
});
|
|
241
|
+
const summary = isRecord(detail.summary) ? detail.summary : void 0;
|
|
242
|
+
const upgraded = new Map(files);
|
|
243
|
+
upgraded.delete("events.json");
|
|
244
|
+
upgraded.set("transcript.json", encode({
|
|
245
|
+
transcript: converted.transcript,
|
|
246
|
+
subagents: {},
|
|
247
|
+
runEvents: converted.runEvents
|
|
248
|
+
}));
|
|
249
|
+
upgraded.set("detail.json", encode({
|
|
250
|
+
...detail,
|
|
251
|
+
scores: scoring.results,
|
|
252
|
+
...scoring.metrics ? { metrics: scoring.metrics } : {},
|
|
253
|
+
...scoring.checkpoints ? { checkpoints: scoring.checkpoints } : {},
|
|
254
|
+
manifest: {
|
|
255
|
+
...manifest,
|
|
256
|
+
schemaVersion: 7
|
|
257
|
+
},
|
|
258
|
+
...summary ? { summary: {
|
|
259
|
+
...summary,
|
|
260
|
+
...isRecord(summary.scoring) ? { scoring: upgrade(summary.scoring) } : {}
|
|
261
|
+
} } : {}
|
|
262
|
+
}));
|
|
263
|
+
upgraded.set("run.json", encode({
|
|
264
|
+
...run,
|
|
265
|
+
snapshotVersion: 3,
|
|
266
|
+
...Array.isArray(scoring.metrics) && scoring.metrics.length ? { metrics: runMetrics(scoring.metrics) } : {}
|
|
267
|
+
}));
|
|
268
|
+
return upgraded;
|
|
269
|
+
}
|
|
270
|
+
/** True when the stored run is in snapshot format version 2. */
|
|
271
|
+
function isV2Run(runJson) {
|
|
272
|
+
return isRecord(runJson) && runJson.snapshotVersion === 2;
|
|
273
|
+
}
|
|
274
|
+
/** `run.json`'s metrics, as `evalkit share` derives them from the scoring's. */
|
|
275
|
+
function runMetrics(scoring) {
|
|
276
|
+
return scoring.filter(isRecord).map((metric) => ({
|
|
277
|
+
name: metric.name,
|
|
278
|
+
...metric.unit ? { unit: metric.unit } : {},
|
|
279
|
+
...metric.better ? { better: metric.better } : {},
|
|
280
|
+
...metric.builtIn ? { builtIn: true } : {},
|
|
281
|
+
...metric.value === void 0 ? {} : { value: metric.value }
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
const decoder = new TextDecoder();
|
|
285
|
+
const encoder = new TextEncoder();
|
|
286
|
+
const parse = (data) => data ? JSON.parse(decoder.decode(data)) : void 0;
|
|
287
|
+
const encode = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
|
|
288
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
289
|
+
function record$1(value, name) {
|
|
290
|
+
if (!isRecord(value)) throw new Error(`${name} must be an object`);
|
|
291
|
+
return value;
|
|
197
292
|
}
|
|
198
293
|
//#endregion
|
|
199
294
|
//#region ../share/src/paths.ts
|
|
200
295
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
201
|
-
const RUN_JSON = /^(detail|
|
|
296
|
+
const RUN_JSON = /^(detail|transcript|artifacts|workspace)\.json$/;
|
|
297
|
+
const V2_RUN_JSON = /^(detail|events|artifacts|workspace)\.json$/;
|
|
202
298
|
const RUN_FILE$1 = /^(?:workspace|attachments)\/(.+)$/;
|
|
203
299
|
const LEGACY_TRIAL_JSON = /^trials\/([^/]+)\/(detail|events|artifacts|workspace)\.json$/;
|
|
204
300
|
const LEGACY_TRIAL_FILE = /^trials\/([^/]+)\/(?:workspace|attachments)\/(.+)$/;
|
|
@@ -209,12 +305,13 @@ function safeSegment(segment) {
|
|
|
209
305
|
return segment.length > 0 && segment.length <= 255 && segment !== "." && segment !== ".." && !/[\\/\0]/.test(segment);
|
|
210
306
|
}
|
|
211
307
|
/**
|
|
212
|
-
* The only files a run bundle may contain (snapshot format version
|
|
213
|
-
* `evalkit
|
|
214
|
-
* attachments. Never evaluator workspaces.
|
|
308
|
+
* The only files a run bundle may contain (snapshot format version 3, from
|
|
309
|
+
* `evalkit share`, for one run): its JSON, candidate workspace files and
|
|
310
|
+
* attachments. Never evaluator workspaces. Version 2 (EvalKit 0.9 and
|
|
311
|
+
* 0.10) has `events.json` where version 3 has `transcript.json`.
|
|
215
312
|
*/
|
|
216
|
-
function allowedRunPath(path) {
|
|
217
|
-
if (path === "run.json" || path === "catalog.json" || RUN_JSON.test(path)) return true;
|
|
313
|
+
function allowedRunPath(path, version = 3) {
|
|
314
|
+
if (path === "run.json" || path === "catalog.json" || (version === 3 ? RUN_JSON : V2_RUN_JSON).test(path)) return true;
|
|
218
315
|
const file = RUN_FILE$1.exec(path);
|
|
219
316
|
return Boolean(file && file[1].split("/").every(safeSegment));
|
|
220
317
|
}
|
|
@@ -255,18 +352,30 @@ function parseUpload(bundle) {
|
|
|
255
352
|
}
|
|
256
353
|
const runJson = byPath.get("run.json");
|
|
257
354
|
if (!runJson) throw new Rejected("Bundle has no run.json");
|
|
258
|
-
const
|
|
259
|
-
const
|
|
355
|
+
const runValue = parseJson(runJson, "run.json");
|
|
356
|
+
const legacy = isLegacyRun(runValue);
|
|
357
|
+
const v2 = isV2Run(runValue);
|
|
358
|
+
const allowed = legacy ? allowedLegacyRunPath : (path) => allowedRunPath(path, v2 ? 2 : 3);
|
|
260
359
|
for (const path of byPath.keys()) if (!allowed(path)) throw new Rejected(`File not allowed in a run bundle: ${path}`);
|
|
261
360
|
const storedCatalog = byPath.get("catalog.json");
|
|
262
361
|
if (storedCatalog) parseCatalog(storedCatalog);
|
|
263
362
|
const catalog = storedCatalog && legacy ? upgradeCatalogFile(storedCatalog) : storedCatalog;
|
|
363
|
+
if (v2) return [runUpload(toV3(byPath), catalog)];
|
|
264
364
|
if (!legacy) return [runUpload(byPath, catalog)];
|
|
265
365
|
parseRun(runJson, { legacy: true });
|
|
266
366
|
return upgradeSnapshotRun(byPath).filter(({ files }) => {
|
|
267
367
|
const status = JSON.parse(new TextDecoder().decode(files.get("run.json"))).status;
|
|
268
368
|
return status === "passed" || status === "failed" || status === "errored";
|
|
269
|
-
}).map(({ files }) => runUpload(files, catalog));
|
|
369
|
+
}).map(({ files }) => runUpload(toV3(files), catalog));
|
|
370
|
+
}
|
|
371
|
+
/** A version 2 run as version 3; an upload that cannot be converted is refused. */
|
|
372
|
+
function toV3(files) {
|
|
373
|
+
parseRun(files.get("run.json"), { version: 2 });
|
|
374
|
+
try {
|
|
375
|
+
return upgradeV2Run(files);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw new Rejected(`This run, from EvalKit 0.10 or older, could not be converted: ${error instanceof Error ? error.message : String(error)}. Upgrade the CLI and run \`evalkit upgrade\`, then share again.`);
|
|
378
|
+
}
|
|
270
379
|
}
|
|
271
380
|
function runUpload(files, catalog) {
|
|
272
381
|
const runJson = files.get("run.json");
|
|
@@ -282,7 +391,8 @@ function runUpload(files, catalog) {
|
|
|
282
391
|
}
|
|
283
392
|
function parseRun(data, options = {}) {
|
|
284
393
|
const value = record(parseJson(data, "run.json"), "run.json");
|
|
285
|
-
|
|
394
|
+
const version = options.version ?? 3;
|
|
395
|
+
if (!options.legacy && value.snapshotVersion !== version) throw new Rejected(`run.json must be snapshot format version ${version}`);
|
|
286
396
|
const source = record(value.source, "run.json source");
|
|
287
397
|
const id = text(value.id, "id");
|
|
288
398
|
if (!isUuid(id)) throw new Rejected("run.json id must be a UUID");
|
|
@@ -290,7 +400,7 @@ function parseRun(data, options = {}) {
|
|
|
290
400
|
if (!isSha(commit)) throw new Rejected("source.commit must be a full 40-character SHA");
|
|
291
401
|
if (typeof source.dirty !== "boolean") throw new Rejected("source.dirty must be a boolean");
|
|
292
402
|
const status = value.status;
|
|
293
|
-
if (status !== "passed" && status !== "failed" && status !== "errored") throw new Rejected(`Only finished runs can be shared (status ${JSON.stringify(status)})`);
|
|
403
|
+
if (status !== "passed" && status !== "failed" && status !== "errored" && status !== "limited") throw new Rejected(`Only finished runs can be shared (status ${JSON.stringify(status)})`);
|
|
294
404
|
if (value.score !== void 0 && typeof value.score !== "number") throw new Rejected("score must be a number");
|
|
295
405
|
const shared = {
|
|
296
406
|
repo: text(source.repo, "source.repo"),
|
|
@@ -374,7 +484,7 @@ const jsonLine = (event) => console.log(JSON.stringify(event));
|
|
|
374
484
|
*
|
|
375
485
|
* POST /api/v1/uploads one run bundle
|
|
376
486
|
* POST /api/v1/reindex rebuild the index from stored run.json files
|
|
377
|
-
* POST /api/v1/upgrade convert stored runs from
|
|
487
|
+
* POST /api/v1/upgrade convert stored runs from older EvalKit versions (RFD0008, RFD0009)
|
|
378
488
|
* GET /api/v1/repos indexed repositories
|
|
379
489
|
* GET /api/v1/repos/<repo>/-/commits commits, newest first
|
|
380
490
|
* GET /api/v1/repos/<repo>/-/<sha>/data/… snapshot data
|
|
@@ -450,15 +560,20 @@ function createShareApp(options) {
|
|
|
450
560
|
}, created ? 201 : 200);
|
|
451
561
|
});
|
|
452
562
|
/**
|
|
453
|
-
* Converts stored runs from
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
563
|
+
* Converts stored runs from older EvalKit versions, `limit` runs per
|
|
564
|
+
* request (default 20), so it fits a Worker's limits. Call it until
|
|
565
|
+
* `remaining` is 0. Runs from before 0.9 (snapshot format version 1, a
|
|
566
|
+
* run of trials) become one run per trial (RFD0008); runs from 0.9 and
|
|
567
|
+
* 0.10 (version 2) get an ATXF transcript in place of their trajectory
|
|
568
|
+
* (RFD0009). New objects and index rows are written before old ones are
|
|
569
|
+
* removed, and run.json last, so an interrupted upgrade is finished by
|
|
570
|
+
* calling it again. A run that cannot be converted is left as it was and
|
|
571
|
+
* listed in `failed`.
|
|
458
572
|
*/
|
|
459
573
|
app.post("/api/v1/upgrade", async (context) => {
|
|
460
574
|
const limit = Math.min(Math.max(Number(context.req.query("limit") ?? 20) || 20, 1), 200);
|
|
461
575
|
const legacy = [];
|
|
576
|
+
const v2 = [];
|
|
462
577
|
for await (const key of files.list("repos/")) {
|
|
463
578
|
if (!RUN_FILE.test(key)) continue;
|
|
464
579
|
const data = await readBytes(await files.get(key));
|
|
@@ -470,21 +585,66 @@ function createShareApp(options) {
|
|
|
470
585
|
continue;
|
|
471
586
|
}
|
|
472
587
|
if (isLegacyRun(value)) legacy.push(key);
|
|
588
|
+
else if (isV2Run(value)) v2.push(key);
|
|
473
589
|
}
|
|
474
590
|
let runs = 0;
|
|
591
|
+
let transcripts = 0;
|
|
592
|
+
const failed = [];
|
|
475
593
|
const batch = legacy.slice(0, limit);
|
|
476
594
|
for (const key of batch) runs += await upgradeStoredRun(key);
|
|
595
|
+
for (const key of v2.slice(0, limit - batch.length)) try {
|
|
596
|
+
await upgradeV2StoredRun(key);
|
|
597
|
+
transcripts += 1;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
failed.push({
|
|
600
|
+
run: key,
|
|
601
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const done = batch.length + transcripts + failed.length;
|
|
477
605
|
const result = {
|
|
478
606
|
upgraded: batch.length,
|
|
479
607
|
runs,
|
|
480
|
-
|
|
608
|
+
transcripts,
|
|
609
|
+
failed: failed.length,
|
|
610
|
+
remaining: legacy.length + v2.length - done
|
|
481
611
|
};
|
|
482
612
|
log({
|
|
483
613
|
event: "share.upgrade",
|
|
484
614
|
...result
|
|
485
615
|
});
|
|
486
|
-
return context.json(
|
|
616
|
+
return context.json({
|
|
617
|
+
...result,
|
|
618
|
+
failures: failed
|
|
619
|
+
});
|
|
487
620
|
});
|
|
621
|
+
/** A stored version 2 run, converted to version 3 in place. */
|
|
622
|
+
async function upgradeV2StoredRun(runJsonKey) {
|
|
623
|
+
const runBase = runJsonKey.slice(0, -9);
|
|
624
|
+
const upgraded = upgradeV2Run(await v2Files(runBase));
|
|
625
|
+
for (const name of ["transcript.json", "detail.json"]) await files.put(`${runBase}/${name}`, upgraded.get(name));
|
|
626
|
+
const runJson = upgraded.get("run.json");
|
|
627
|
+
const run = storedRun(runJson);
|
|
628
|
+
if (run) {
|
|
629
|
+
await index.forgetRun(run.id);
|
|
630
|
+
await index.recordRun(run);
|
|
631
|
+
}
|
|
632
|
+
await files.delete(`${runBase}/events.json`);
|
|
633
|
+
await files.put(runJsonKey, runJson);
|
|
634
|
+
}
|
|
635
|
+
/** The files `upgradeV2Run` reads, from a stored run. */
|
|
636
|
+
async function v2Files(runBase) {
|
|
637
|
+
const stored = /* @__PURE__ */ new Map();
|
|
638
|
+
for (const name of [
|
|
639
|
+
"run.json",
|
|
640
|
+
"detail.json",
|
|
641
|
+
"events.json"
|
|
642
|
+
]) {
|
|
643
|
+
const data = await readBytes(await files.get(`${runBase}/${name}`));
|
|
644
|
+
if (data) stored.set(name, data);
|
|
645
|
+
}
|
|
646
|
+
return stored;
|
|
647
|
+
}
|
|
488
648
|
/** One stored version 1 run → version 2 runs; returns how many. */
|
|
489
649
|
async function upgradeStoredRun(runJsonKey) {
|
|
490
650
|
const [, repo, sha, oldRunId] = RUN_FILE.exec(runJsonKey);
|
|
@@ -501,6 +661,9 @@ function createShareApp(options) {
|
|
|
501
661
|
const written = /* @__PURE__ */ new Set();
|
|
502
662
|
let newRunJson;
|
|
503
663
|
for (const upgraded of upgradeSnapshotRun(stored)) {
|
|
664
|
+
try {
|
|
665
|
+
upgraded.files = upgradeV2Run(upgraded.files);
|
|
666
|
+
} catch {}
|
|
504
667
|
const runJson = upgraded.files.get("run.json");
|
|
505
668
|
const run = storedRun(runJson);
|
|
506
669
|
if (!run) continue;
|
|
@@ -666,9 +829,9 @@ function createShareApp(options) {
|
|
|
666
829
|
const catalog = data ? parseCatalog(data) : EMPTY_CATALOG;
|
|
667
830
|
return context.json(path === "catalog.json" ? { evals: catalog.catalog } : path === "evals.json" ? { evals: catalog.evals } : { suites: catalog.suites }, 200, NO_CACHE);
|
|
668
831
|
}
|
|
669
|
-
const runFile = /^runs\/([^/]+)\/((?:detail|
|
|
832
|
+
const runFile = /^runs\/([^/]+)\/((?:detail|transcript|artifacts|workspace)\.json|(?:workspace|attachments)\/.+)$/.exec(path);
|
|
670
833
|
if (!runFile || !isUuid(runFile[1])) return context.notFound();
|
|
671
|
-
const body = await files.get(`${commitBase}/${path}`);
|
|
834
|
+
const body = runFile[2] === "transcript.json" || runFile[2] === "detail.json" ? await currentFile(`${commitBase}/runs/${runFile[1]}`, runFile[2]) : await files.get(`${commitBase}/${path}`);
|
|
672
835
|
if (!body) return context.notFound();
|
|
673
836
|
const json = path.endsWith(".json") && !path.includes("/workspace/") && !path.includes("/attachments/");
|
|
674
837
|
return new Response(body, { headers: json ? {
|
|
@@ -681,14 +844,35 @@ function createShareApp(options) {
|
|
|
681
844
|
"cache-control": IMMUTABLE
|
|
682
845
|
} });
|
|
683
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* A run's detail or transcript as version 3 has it: a version 2 run not
|
|
849
|
+
* upgraded yet is converted as it is read, the same way the upgrade does,
|
|
850
|
+
* so its cached files do not change when it is upgraded.
|
|
851
|
+
*/
|
|
852
|
+
async function currentFile(runBase, name) {
|
|
853
|
+
const runJson = await readBytes(await files.get(`${runBase}/run.json`));
|
|
854
|
+
let value;
|
|
855
|
+
try {
|
|
856
|
+
value = runJson ? JSON.parse(new TextDecoder().decode(runJson)) : void 0;
|
|
857
|
+
} catch {
|
|
858
|
+
value = void 0;
|
|
859
|
+
}
|
|
860
|
+
if (!isV2Run(value)) return files.get(`${runBase}/${name}`);
|
|
861
|
+
try {
|
|
862
|
+
return upgradeV2Run(await v2Files(runBase)).get(name) ?? null;
|
|
863
|
+
} catch {
|
|
864
|
+
return name === "detail.json" ? files.get(`${runBase}/${name}`) : null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
684
867
|
return app;
|
|
685
868
|
}
|
|
686
|
-
/** A stored run.json, or undefined if it is not a shareable run. */
|
|
869
|
+
/** A stored run.json (version 3, or 2 until upgraded), or undefined if it is not a shareable run. */
|
|
687
870
|
function storedRun(data) {
|
|
688
871
|
if (!data) return void 0;
|
|
689
872
|
try {
|
|
690
|
-
return parseRun(data);
|
|
873
|
+
return parseRun(data, { version: JSON.parse(new TextDecoder().decode(data)).snapshotVersion === 2 ? 2 : 3 });
|
|
691
874
|
} catch (error) {
|
|
875
|
+
if (error instanceof SyntaxError) return void 0;
|
|
692
876
|
if (error instanceof Rejected) return void 0;
|
|
693
877
|
throw error;
|
|
694
878
|
}
|
|
@@ -738,6 +922,12 @@ const MIGRATIONS = [
|
|
|
738
922
|
`CREATE INDEX IF NOT EXISTS runs_by_eval ON runs (repo, eval_id, started_at)`
|
|
739
923
|
];
|
|
740
924
|
/** Newest first; commits without a commit time order by first upload. */
|
|
925
|
+
/** Statuses a commit counts as failed: a limited run is a failure too. */
|
|
926
|
+
const FAILED = /* @__PURE__ */ new Set([
|
|
927
|
+
"failed",
|
|
928
|
+
"errored",
|
|
929
|
+
"limited"
|
|
930
|
+
]);
|
|
741
931
|
const BY_TIME = `ORDER BY COALESCE(committed_at, first_seen_at) DESC, first_seen_at DESC`;
|
|
742
932
|
var ShareIndexDb = class {
|
|
743
933
|
sql;
|
|
@@ -768,7 +958,7 @@ var ShareIndexDb = class {
|
|
|
768
958
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, run.id, run.repoKey, run.source.commit, run.evalId, run.suiteId ?? null, run.status, run.score ?? null, run.startedAt, run.source.dirty ? 1 : 0, JSON.stringify(run.summary), at);
|
|
769
959
|
if (!run.source.dirty) this.sql.run(`UPDATE commits SET runs = runs + 1,
|
|
770
960
|
passed = passed + ?, failed = failed + ?
|
|
771
|
-
WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0,
|
|
961
|
+
WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0, FAILED.has(run.status) ? 1 : 0, run.repoKey, run.source.commit);
|
|
772
962
|
});
|
|
773
963
|
}
|
|
774
964
|
forgetRun(runId) {
|
|
@@ -778,7 +968,7 @@ var ShareIndexDb = class {
|
|
|
778
968
|
this.sql.run("DELETE FROM runs WHERE id = ?", runId);
|
|
779
969
|
if (!row.dirty) this.sql.run(`UPDATE commits SET runs = runs - 1,
|
|
780
970
|
passed = passed - ?, failed = failed - ?
|
|
781
|
-
WHERE repo = ? AND sha = ?`, row.status === "passed" ? 1 : 0,
|
|
971
|
+
WHERE repo = ? AND sha = ?`, row.status === "passed" ? 1 : 0, FAILED.has(row.status) ? 1 : 0, row.repo, row.sha);
|
|
782
972
|
});
|
|
783
973
|
}
|
|
784
974
|
repos() {
|