@hackerrank/astra-cli 0.1.1 → 0.1.3
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/README.md +35 -2
- package/package.json +1 -1
- package/src/bench.js +26 -5
- package/src/cli.js +108 -24
- package/src/model.js +7 -1
- package/src/repl.js +69 -6
- package/src/report.html +580 -0
- package/src/report.js +3 -1
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ astra \
|
|
|
115
115
|
--repeat 3 \
|
|
116
116
|
--tar # write a bench/ tarball locally
|
|
117
117
|
|
|
118
|
-
# push results to S3 (tars bench/
|
|
118
|
+
# push results to S3 (tars bench/ and uploads it + report.html to the URI)
|
|
119
119
|
astra -m m1,m2 -p tasks/dummy-slugify --repeat 3 \
|
|
120
120
|
--push s3://hackerrank-astra-bench-results-dev/runs/2025-09-02/
|
|
121
121
|
```
|
|
@@ -126,6 +126,35 @@ price entry shows **`n/a`** (never `$0`), and estimated costs (native routes) ar
|
|
|
126
126
|
marked `~`. `--push` requires an `s3://` URI and the `aws` CLI on PATH; if the
|
|
127
127
|
upload fails the local tarball is kept.
|
|
128
128
|
|
|
129
|
+
Next to the tarball, `--push` also uploads the self-contained `report.html`
|
|
130
|
+
dashboard with `Content-Type: text/html` and prints a **7-day presigned
|
|
131
|
+
"view report" link** at the end of the run. The bucket is private (no public
|
|
132
|
+
read), so that presigned URL is the only way to open the dashboard — e.g.
|
|
133
|
+
clickable straight from a Jenkins console log.
|
|
134
|
+
|
|
135
|
+
### HTML report
|
|
136
|
+
|
|
137
|
+
Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
|
|
138
|
+
`bench/summary.json` and a self-contained `bench/report.html` dashboard —
|
|
139
|
+
no server, no build step, no external JS: open it straight from `file://` or
|
|
140
|
+
from inside the `bench-*.tgz` tarball. It has a leaderboard, a model × task
|
|
141
|
+
pass-rate heatmap, cost/token charts, per-step trend lines, and an
|
|
142
|
+
expandable command timeline for every run.
|
|
143
|
+
|
|
144
|
+
Regenerate it on demand (e.g. after manually editing/pruning `bench/`) with:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
astra --report # rebuild ./bench/{summary.json,report.html}
|
|
148
|
+
astra --report --bench-root ./other-bench
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`summary.json` follows the `astra-bench-1` schema: `kpis` (totals), a tidy
|
|
152
|
+
`leaderboard[]` (one row per model×reasoning), `matrix[]` (model×reasoning×task
|
|
153
|
+
pass@k), `runs[]` (every attempt, with a compact per-step `timeline`), and
|
|
154
|
+
`step_series[]` (token/cost distributions by step, for the trend charts). Each
|
|
155
|
+
run folder also gets a standalone `run.json` for drill-down without loading
|
|
156
|
+
the full `trajectory.json`.
|
|
157
|
+
|
|
129
158
|
### Sessions
|
|
130
159
|
|
|
131
160
|
Every run (either mode) is saved under `~/.astra/sessions/<id>.json`.
|
|
@@ -152,7 +181,11 @@ credits — that's a quota issue, not a bug.
|
|
|
152
181
|
--repeat <n> Attempts per (model,reasoning) in a matrix (default: 1)
|
|
153
182
|
--bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
154
183
|
--tar After a matrix, write a bench/ tarball locally
|
|
155
|
-
--push <s3-uri> After a matrix, tar bench/ and `aws s3 cp`
|
|
184
|
+
--push <s3-uri> After a matrix, tar bench/ and `aws s3 cp` it (plus
|
|
185
|
+
report.html) to the URI; prints presigned
|
|
186
|
+
download/view links
|
|
187
|
+
--report Rebuild bench/summary.json + bench/report.html from
|
|
188
|
+
whatever runs already exist on disk, then exit
|
|
156
189
|
-C, --cwd <path> Working directory for commands (default: cwd)
|
|
157
190
|
-o, --output <path> Also write trajectory JSON here (autonomous mode)
|
|
158
191
|
-s, --steps <n> Step limit (default: 40)
|
package/package.json
CHANGED
package/src/bench.js
CHANGED
|
@@ -455,17 +455,38 @@ function s3ConsoleUrl(s3Uri) {
|
|
|
455
455
|
return `https://s3.console.aws.amazon.com/s3/object/${bucket}?prefix=${encodeURIComponent(key)}`;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Generate a time-limited presigned URL for an S3 object via the AWS CLI
|
|
460
|
+
* (`aws s3 presign`). `expiresIn` defaults to 7 days (604800s). Returns null
|
|
461
|
+
* (rather than throwing) if the AWS CLI is missing or presigning fails, so
|
|
462
|
+
* a presign failure never blocks the run.
|
|
463
|
+
*/
|
|
464
|
+
export function presignS3Url(s3Uri, expiresIn = 604800) {
|
|
465
|
+
const res = spawnSync(
|
|
466
|
+
"aws",
|
|
467
|
+
["s3", "presign", s3Uri, "--expires-in", String(expiresIn)],
|
|
468
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
469
|
+
);
|
|
470
|
+
if (res.error && res.error.code === "ENOENT") return null;
|
|
471
|
+
if (res.status !== 0) return null;
|
|
472
|
+
return (res.stdout?.toString() || "").trim() || null;
|
|
473
|
+
}
|
|
474
|
+
|
|
458
475
|
/**
|
|
459
476
|
* Upload a file to S3 via the AWS CLI. Returns { uri, url } where `uri` is the
|
|
460
477
|
* final s3:// destination and `url` is a browsable https console link.
|
|
461
|
-
*
|
|
462
|
-
*
|
|
478
|
+
* Optionally sets the object's Content-Type (e.g. "text/html" so presigned
|
|
479
|
+
* URLs open in a browser instead of downloading). Fails gracefully (throws a
|
|
480
|
+
* descriptive error) when the AWS CLI is missing or the copy fails; callers
|
|
481
|
+
* should keep the local tarball on failure.
|
|
463
482
|
*/
|
|
464
|
-
export function pushToS3(file, s3Uri) {
|
|
483
|
+
export function pushToS3(file, s3Uri, { contentType } = {}) {
|
|
465
484
|
if (!/^s3:\/\//.test(s3Uri)) throw new Error(`--push destination must be an s3:// URI, got: ${s3Uri}`);
|
|
466
485
|
// If the URI ends with "/", upload into it under the file's basename.
|
|
467
486
|
const dest = s3Uri.endsWith("/") ? s3Uri + path.basename(file) : s3Uri;
|
|
468
|
-
const
|
|
487
|
+
const cmd = ["s3", "cp", file, dest];
|
|
488
|
+
if (contentType) cmd.push("--content-type", contentType);
|
|
489
|
+
const res = spawnSync("aws", cmd, {
|
|
469
490
|
stdio: ["ignore", "pipe", "pipe"],
|
|
470
491
|
});
|
|
471
492
|
if (res.error && res.error.code === "ENOENT") {
|
|
@@ -474,5 +495,5 @@ export function pushToS3(file, s3Uri) {
|
|
|
474
495
|
if (res.status !== 0) {
|
|
475
496
|
throw new Error(`aws s3 cp failed: ${res.stderr?.toString() || "unknown"}`);
|
|
476
497
|
}
|
|
477
|
-
return { uri: dest, url: s3ConsoleUrl(dest) };
|
|
498
|
+
return { uri: dest, url: s3ConsoleUrl(dest), presigned: presignS3Url(dest) };
|
|
478
499
|
}
|
package/src/cli.js
CHANGED
|
@@ -28,10 +28,14 @@
|
|
|
28
28
|
* comma-separated to sweep levels in bench mode).
|
|
29
29
|
* Sent to the model and recorded in the bench name.
|
|
30
30
|
* --repeat <n> Attempts per (model,reasoning) in a matrix (1)
|
|
31
|
-
* --push After a matrix, tar bench/ and upload
|
|
32
|
-
* s3://astra-bench-results/<timestamp
|
|
31
|
+
* --push After a matrix, tar bench/ and upload it (plus
|
|
32
|
+
* report.html) to s3://astra-bench-results/<timestamp>/;
|
|
33
|
+
* prints presigned download/view links
|
|
33
34
|
* --push-uri <s3-uri> Override the S3 push destination
|
|
34
35
|
* --tar After a matrix, write a bench/ tarball locally
|
|
36
|
+
* --report Rebuild bench/summary.json + bench/report.html
|
|
37
|
+
* from whatever runs already exist on disk, then
|
|
38
|
+
* exit (no model/API key needed).
|
|
35
39
|
* -t, --task <text> Task text -> bench mode (run to completion)
|
|
36
40
|
* -f, --task-file <path> Read task text from a file -> bench mode
|
|
37
41
|
* -p, --path <dir> Task directory (copied into an isolated bench
|
|
@@ -94,6 +98,7 @@ import {
|
|
|
94
98
|
defaultPushUri,
|
|
95
99
|
benchRoot,
|
|
96
100
|
} from "./bench.js";
|
|
101
|
+
import { refreshReport } from "./report.js";
|
|
97
102
|
|
|
98
103
|
/** Map a reasoning level to gateway modelKwargs. Empty for off/none/unset. */
|
|
99
104
|
function reasoningKwargs(level) {
|
|
@@ -115,6 +120,7 @@ function parseArgs(argv) {
|
|
|
115
120
|
"--push": "push",
|
|
116
121
|
"--push-uri": "push-uri",
|
|
117
122
|
"--tar": "tar",
|
|
123
|
+
"--report": "report",
|
|
118
124
|
"--bench-root": "bench-root",
|
|
119
125
|
"-C": "cwd", "--cwd": "cwd",
|
|
120
126
|
"-o": "output", "--output": "output",
|
|
@@ -130,7 +136,7 @@ function parseArgs(argv) {
|
|
|
130
136
|
"-y": "yolo", "--yolo": "yolo",
|
|
131
137
|
"-h": "help", "--help": "help",
|
|
132
138
|
};
|
|
133
|
-
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push"]);
|
|
139
|
+
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push", "report"]);
|
|
134
140
|
for (let i = 2; i < argv.length; i++) {
|
|
135
141
|
const key = alias[argv[i]];
|
|
136
142
|
if (!key) { console.error(`Unknown option: ${argv[i]}`); process.exit(2); }
|
|
@@ -154,6 +160,18 @@ async function main() {
|
|
|
154
160
|
|
|
155
161
|
if (args.sessions) { printSessions(); process.exit(0); }
|
|
156
162
|
if (args.help) { console.log(HELP); process.exit(0); }
|
|
163
|
+
if (args.report) {
|
|
164
|
+
try {
|
|
165
|
+
const res = refreshReport(args["bench-root"]);
|
|
166
|
+
console.error(`\x1b[1m[astra] report · ${res.runs} run(s) · ${res.models} model(s) · ${res.tasks} task(s)\x1b[0m`);
|
|
167
|
+
console.error(`\x1b[1m summary\x1b[0m ${res.summary}`);
|
|
168
|
+
console.error(`\x1b[1m html \x1b[0m ${res.html}`);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`\x1b[31m[astra] report failed: ${err.message}\x1b[0m`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
157
175
|
|
|
158
176
|
// Load a session to resume (if any) to infer defaults.
|
|
159
177
|
let resumeDoc = null;
|
|
@@ -348,6 +366,12 @@ async function main() {
|
|
|
348
366
|
const metrics = collectMetrics({ agent: benchAgent, model, reasoning });
|
|
349
367
|
const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
|
|
350
368
|
log(`[astra] bench done: exit=${res.exit_status} · metrics → ${paths.runMetrics}`);
|
|
369
|
+
try {
|
|
370
|
+
const report = refreshReport(alloc.root);
|
|
371
|
+
log(`[astra] report → ${report.html}`);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
log(`[astra] report generation skipped: ${err.message}`);
|
|
374
|
+
}
|
|
351
375
|
};
|
|
352
376
|
await runRepl(agent, { model, autoRun: !!args.yolo, fresh: !resumeDoc, reasoning, runBench });
|
|
353
377
|
process.exit(0);
|
|
@@ -384,6 +408,12 @@ async function main() {
|
|
|
384
408
|
console.error(`\x1b[2m[astra] metrics -> ${paths.runMetrics}\x1b[0m`);
|
|
385
409
|
console.error(`\x1b[2m[astra] index -> ${paths.index}\x1b[0m`);
|
|
386
410
|
}
|
|
411
|
+
try {
|
|
412
|
+
const report = refreshReport(benchRun.root);
|
|
413
|
+
if (!quiet) console.error(`\x1b[2m[astra] report -> ${report.html}\x1b[0m`);
|
|
414
|
+
} catch {
|
|
415
|
+
// Non-fatal: report regeneration is best-effort for single runs.
|
|
416
|
+
}
|
|
387
417
|
}
|
|
388
418
|
|
|
389
419
|
if (!quiet) {
|
|
@@ -408,9 +438,74 @@ async function main() {
|
|
|
408
438
|
console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
|
|
409
439
|
}
|
|
410
440
|
if (result.submission) console.log(result.submission);
|
|
441
|
+
|
|
442
|
+
// For a single-run bench, tar + push the bench/ folder if requested.
|
|
443
|
+
// (Matrix runs handle this inside runBenchMatrix.)
|
|
444
|
+
if (benchRun) {
|
|
445
|
+
const links = [];
|
|
446
|
+
archiveAndPush(args, benchRun.root, links);
|
|
447
|
+
if (links.length) {
|
|
448
|
+
const w = Math.max(...links.map(([k]) => k.length));
|
|
449
|
+
console.error("");
|
|
450
|
+
for (const [k, v] of links) console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
411
454
|
process.exit(result.exit_status === "Submitted" ? 0 : 1);
|
|
412
455
|
}
|
|
413
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Tar the bench/ folder and (optionally) upload it to S3. Mutates `links`
|
|
459
|
+
* with the resulting artifact paths/URLs and prints any failures.
|
|
460
|
+
* Works for both single-run and matrix bench runs.
|
|
461
|
+
*/
|
|
462
|
+
function archiveAndPush(args, root, links) {
|
|
463
|
+
if (!(args.tar || args.push)) return;
|
|
464
|
+
try {
|
|
465
|
+
const tarball = packBench({ root });
|
|
466
|
+
links.push(["tarball", tarball]);
|
|
467
|
+
if (args.push) {
|
|
468
|
+
// Bare --push uses the default bucket + timestamp prefix; --push-uri
|
|
469
|
+
// overrides the destination.
|
|
470
|
+
const dest = args["push-uri"] || defaultPushUri();
|
|
471
|
+
try {
|
|
472
|
+
const pushed = pushToS3(tarball, dest);
|
|
473
|
+
links.push(["s3 uri", pushed.uri]);
|
|
474
|
+
links.push(["s3 url", pushed.url]);
|
|
475
|
+
if (pushed.presigned) {
|
|
476
|
+
links.push(["download (presigned, 7d)", pushed.presigned]);
|
|
477
|
+
}
|
|
478
|
+
// Also push the self-contained HTML dashboard next to the tarball and
|
|
479
|
+
// emit a presigned view link: the bucket is private (no public read),
|
|
480
|
+
// so the presigned URL is the only way to open the report — e.g. by
|
|
481
|
+
// clicking it straight from the Jenkins console output. Uploaded with
|
|
482
|
+
// Content-Type: text/html so browsers render it instead of downloading.
|
|
483
|
+
const reportHtml = path.join(benchRoot(root), "report.html");
|
|
484
|
+
if (fs.existsSync(reportHtml)) {
|
|
485
|
+
const htmlDest = dest.endsWith("/")
|
|
486
|
+
? dest + "report.html"
|
|
487
|
+
: dest.slice(0, dest.lastIndexOf("/") + 1) + "report.html";
|
|
488
|
+
try {
|
|
489
|
+
const html = pushToS3(reportHtml, htmlDest, { contentType: "text/html" });
|
|
490
|
+
if (html.presigned) {
|
|
491
|
+
links.push(["view report (presigned, 7d)", html.presigned]);
|
|
492
|
+
} else {
|
|
493
|
+
links.push(["report (s3 url)", html.url]);
|
|
494
|
+
}
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error(`\x1b[31m[astra] S3 report push failed: ${err.message}\x1b[0m`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
501
|
+
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
console.error(`\x1b[31m[astra] tar failed: ${err.message}\x1b[0m`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
414
509
|
/**
|
|
415
510
|
* Run a multi-model / multi-reasoning / repeated bench matrix, print a
|
|
416
511
|
* leaderboard, optionally tar + upload the bench/ folder, then exit.
|
|
@@ -463,29 +558,18 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
463
558
|
// Collect the important artifact locations to print together at the end.
|
|
464
559
|
const links = [["metrics", indexPath]];
|
|
465
560
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// overrides the destination.
|
|
474
|
-
const dest = args["push-uri"] || defaultPushUri();
|
|
475
|
-
try {
|
|
476
|
-
const pushed = pushToS3(tarball, dest);
|
|
477
|
-
links.push(["s3 uri", pushed.uri]);
|
|
478
|
-
links.push(["s3 url", pushed.url]);
|
|
479
|
-
} catch (err) {
|
|
480
|
-
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
481
|
-
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} catch (err) {
|
|
485
|
-
console.error(`\x1b[31m[astra] tar failed: ${err.message}\x1b[0m`);
|
|
486
|
-
}
|
|
561
|
+
// Rebuild the dashboard (summary.json + report.html) from every run on
|
|
562
|
+
// disk under this root, including runs from earlier matrix invocations.
|
|
563
|
+
try {
|
|
564
|
+
const report = refreshReport(root);
|
|
565
|
+
links.push(["report", report.html]);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
console.error(`\x1b[33m[astra] report generation skipped: ${err.message}\x1b[0m`);
|
|
487
568
|
}
|
|
488
569
|
|
|
570
|
+
// Tar + optional S3 push of the whole bench/ folder.
|
|
571
|
+
archiveAndPush(args, root, links);
|
|
572
|
+
|
|
489
573
|
const solved = rows.filter((r) => r.resolved).length;
|
|
490
574
|
console.error(`\n\x1b[1m[astra] matrix done · ${solved}/${rows.length} resolved\x1b[0m`);
|
|
491
575
|
|
package/src/model.js
CHANGED
|
@@ -67,6 +67,9 @@ export class GatewayModel {
|
|
|
67
67
|
this.costSource = null; // "reported" | "estimated" | "mixed" | null
|
|
68
68
|
// Preferred token-cap parameter; swapped automatically on 400 if needed.
|
|
69
69
|
this._tokenParam = "max_tokens";
|
|
70
|
+
// Optional AbortSignal; when set and aborted, in-flight requests are
|
|
71
|
+
// cancelled (used to force-quit a long model call on a second Ctrl+C).
|
|
72
|
+
this.signal = null;
|
|
70
73
|
if (!this.apiKey) {
|
|
71
74
|
throw new Error(
|
|
72
75
|
"GatewayModel: no API key. Set ASTRA_GATEWAY_API_KEY or configure ~/.astra/config.json."
|
|
@@ -100,6 +103,7 @@ export class GatewayModel {
|
|
|
100
103
|
Authorization: `Bearer ${this.apiKey}`,
|
|
101
104
|
},
|
|
102
105
|
body: JSON.stringify(body),
|
|
106
|
+
signal: this.signal || undefined,
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
if (!res.ok) {
|
|
@@ -228,8 +232,10 @@ export class GatewayModel {
|
|
|
228
232
|
|
|
229
233
|
function isRetryable(err) {
|
|
230
234
|
// Never retry classified terminal errors (auth/quota/bad-request) or context
|
|
231
|
-
// overflow — those won't succeed on retry.
|
|
235
|
+
// overflow — those won't succeed on retry. Also never retry an aborted
|
|
236
|
+
// request (user force-quit via Ctrl+C).
|
|
232
237
|
if (err instanceof GatewayError || err instanceof ContextWindowError) return false;
|
|
238
|
+
if (err?.name === "AbortError") return false;
|
|
233
239
|
const msg = String(err?.message || err);
|
|
234
240
|
return /HTTP 5\d\d|HTTP 429|ECONNRESET|ETIMEDOUT|fetch failed|network/i.test(msg);
|
|
235
241
|
}
|
package/src/repl.js
CHANGED
|
@@ -134,7 +134,9 @@ function turnSummary({ turn, upTok, downTok, costUsd, costKind, seconds }) {
|
|
|
134
134
|
*/
|
|
135
135
|
function footerLines(agent, model, yolo, mode = "agent") {
|
|
136
136
|
const cols = process.stdout.columns || 100;
|
|
137
|
-
|
|
137
|
+
// Leave one cell of slack so a full-width rule never wraps to a new row
|
|
138
|
+
// (which pushes the whole footer up and looks broken on some terminals).
|
|
139
|
+
const rule = "─".repeat(Math.max(1, cols - 1));
|
|
138
140
|
|
|
139
141
|
const reasoning = model.modelKwargs?.reasoning_effort || model.modelKwargs?.reasoning?.effort || "off";
|
|
140
142
|
const modeTag = mode === "bench" ? C.yellow("◆ bench") : C.cyan("◆ agent");
|
|
@@ -154,12 +156,13 @@ function footerLines(agent, model, yolo, mode = "agent") {
|
|
|
154
156
|
|
|
155
157
|
return [
|
|
156
158
|
C.dim(rule),
|
|
157
|
-
modeTag + " " + C.cyan(repoLabel()) + " " + C.dim(modelBits.join(" "))
|
|
159
|
+
modeTag + " " + C.cyan(repoLabel()) + " " + C.dim(modelBits.join(" ")),
|
|
158
160
|
C.dim(usage),
|
|
161
|
+
C.dim("opt+left/right: model · opt+up/down: reasoning · shift+tab: mode"),
|
|
159
162
|
];
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
const FOOTER_LINES =
|
|
165
|
+
const FOOTER_LINES = 4; // divider + repo/model + usage + shortcuts
|
|
163
166
|
const PROMPT_PREFIX = C.green("› "); // live input row (minimal)
|
|
164
167
|
const HISTORY_PREFIX = C.green("you › "); // echoed into transcript/history
|
|
165
168
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -336,6 +339,11 @@ class Screen {
|
|
|
336
339
|
this.drawPrompt();
|
|
337
340
|
}
|
|
338
341
|
|
|
342
|
+
/** True while the agent is working (a turn is in progress). */
|
|
343
|
+
isBusy() {
|
|
344
|
+
return this._busy || this._busyResume;
|
|
345
|
+
}
|
|
346
|
+
|
|
339
347
|
/**
|
|
340
348
|
* Resolve with the next full line the user types. An optional prompt label
|
|
341
349
|
* replaces the default "you › " (used by the approval gate so the question
|
|
@@ -486,8 +494,12 @@ class Screen {
|
|
|
486
494
|
this.buf = this.buf.slice(0, -1);
|
|
487
495
|
this.drawPrompt();
|
|
488
496
|
} else if (ch === "\x03") { // Ctrl+C
|
|
497
|
+
// If a prompt is pending (idle), resolve it so the loop can react.
|
|
498
|
+
// Also always notify the interrupt hook so a *running* agent turn can
|
|
499
|
+
// be aborted even when no readLine is pending.
|
|
489
500
|
const r = this.resolve; this.resolve = null;
|
|
490
501
|
if (r) r("__SIGINT__");
|
|
502
|
+
if (this.onInterrupt) this.onInterrupt();
|
|
491
503
|
} else if (ch === "\x04") { // Ctrl+D
|
|
492
504
|
const r = this.resolve; this.resolve = null;
|
|
493
505
|
if (r) r("__EOF__");
|
|
@@ -576,6 +588,31 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
576
588
|
// extra needed here, but expose a hook for symmetry / future use).
|
|
577
589
|
screen.onClearInput = () => {};
|
|
578
590
|
|
|
591
|
+
// Ctrl+C handling. In raw mode Ctrl+C does NOT raise SIGINT — it arrives as
|
|
592
|
+
// a byte, so we handle it explicitly. The screen's key handler already
|
|
593
|
+
// resolves any pending readLine with "__SIGINT__" (idle case); here we only
|
|
594
|
+
// deal with the *busy* case where no readLine is pending:
|
|
595
|
+
// - first Ctrl+C while working -> request an abort after the current step
|
|
596
|
+
// - second Ctrl+C while working -> force an immediate quit
|
|
597
|
+
const interrupt = { aborted: false, quit: false };
|
|
598
|
+
screen.onInterrupt = () => {
|
|
599
|
+
if (!screen.isBusy()) {
|
|
600
|
+
// Idle: the readLine resolver was already fired with "__SIGINT__", which
|
|
601
|
+
// breaks the loop and quits gracefully. Nothing more to do here.
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (interrupt.aborted) {
|
|
605
|
+
// Second Ctrl+C while working -> force quit: abort the in-flight model
|
|
606
|
+
// request so a long call unblocks immediately.
|
|
607
|
+
interrupt.quit = true;
|
|
608
|
+
log(C.red("[astra] force quitting…"));
|
|
609
|
+
if (model._abort) { try { model._abort.abort(); } catch {} }
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
interrupt.aborted = true;
|
|
613
|
+
log(C.yellow("[astra] interrupting… (press Ctrl+C again to force quit)"));
|
|
614
|
+
};
|
|
615
|
+
|
|
579
616
|
// Approval gate: show the command, then ask for y/n/a on the prompt row.
|
|
580
617
|
agent.confirm = async (command) => {
|
|
581
618
|
log(C.yellow("$ " + command));
|
|
@@ -621,6 +658,7 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
621
658
|
mode = "agent"; refresh(); continue;
|
|
622
659
|
}
|
|
623
660
|
screen.startBusy("benching");
|
|
661
|
+
interrupt.aborted = false;
|
|
624
662
|
try {
|
|
625
663
|
await runBench(line, log);
|
|
626
664
|
} catch (err) {
|
|
@@ -628,18 +666,33 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
628
666
|
} finally {
|
|
629
667
|
screen.stopBusy();
|
|
630
668
|
}
|
|
669
|
+
if (interrupt.quit) break;
|
|
631
670
|
mode = "agent";
|
|
632
671
|
refresh();
|
|
633
672
|
continue;
|
|
634
673
|
}
|
|
635
674
|
|
|
636
675
|
agent.addUserMessage(line);
|
|
676
|
+
interrupt.aborted = false; // reset per turn
|
|
677
|
+
// Fresh AbortController for this turn so a force-quit can cancel an
|
|
678
|
+
// in-flight model request. Cleared in `finally`.
|
|
679
|
+
model._abort = new AbortController();
|
|
680
|
+
model.signal = model._abort.signal;
|
|
637
681
|
screen.startBusy("working");
|
|
638
682
|
try {
|
|
639
|
-
await driveUntilChat(agent, log);
|
|
683
|
+
await driveUntilChat(agent, log, interrupt);
|
|
684
|
+
} catch (err) {
|
|
685
|
+
if (err?.name === "AbortError" || interrupt.quit) {
|
|
686
|
+
// Force-quit path: swallow the abort and fall through to break.
|
|
687
|
+
} else {
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
640
690
|
} finally {
|
|
641
691
|
screen.stopBusy();
|
|
692
|
+
model.signal = null;
|
|
693
|
+
model._abort = null;
|
|
642
694
|
}
|
|
695
|
+
if (interrupt.quit) break; // force-quit requested via a second Ctrl+C
|
|
643
696
|
const after = usageSnapshot(model);
|
|
644
697
|
log(
|
|
645
698
|
turnSummary({
|
|
@@ -733,10 +786,16 @@ function usageSnapshot(model) {
|
|
|
733
786
|
|
|
734
787
|
/**
|
|
735
788
|
* Take turns until the agent produces a chat reply (or terminates). Commands
|
|
736
|
-
* run in between; their output goes back to the model automatically.
|
|
789
|
+
* run in between; their output goes back to the model automatically. If an
|
|
790
|
+
* `interrupt` signal is provided, the loop stops cleanly between turns when
|
|
791
|
+
* `interrupt.aborted` becomes true (Ctrl+C while working).
|
|
737
792
|
*/
|
|
738
|
-
async function driveUntilChat(agent, log) {
|
|
793
|
+
async function driveUntilChat(agent, log, interrupt = null) {
|
|
739
794
|
while (true) {
|
|
795
|
+
if (interrupt && interrupt.aborted) {
|
|
796
|
+
log(C.yellow("[astra] interrupted — returning to prompt."));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
740
799
|
let turn;
|
|
741
800
|
try {
|
|
742
801
|
turn = await agent.runTurn();
|
|
@@ -748,6 +807,10 @@ async function driveUntilChat(agent, log) {
|
|
|
748
807
|
throw err;
|
|
749
808
|
}
|
|
750
809
|
|
|
810
|
+
if (interrupt && interrupt.aborted) {
|
|
811
|
+
log(C.yellow("[astra] interrupted — returning to prompt."));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
751
814
|
if (turn.kind === "chat") {
|
|
752
815
|
log("\n" + C.cyan("astra › ") + turn.content.trim());
|
|
753
816
|
return;
|
package/src/report.html
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>astra bench report</title>
|
|
7
|
+
<link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" />
|
|
8
|
+
<style>
|
|
9
|
+
:root { --ink:#11131a; --muted:#69707d; --wash:#f5f6f8; --line:#e4e7eb; --lime:#a7ff63; --blue:#5285f7; --purple:#9160d8; --green:#39ae76; --orange:#f3a021; --red:#d85668; font-family:Satoshi,"Avenir Next",Arial,sans-serif; }
|
|
10
|
+
* { box-sizing:border-box; } body { margin:0; background:#fff; color:var(--ink); font-size:14px; line-height:1.45; } .wrap { max-width:1240px; margin:auto; padding:28px 32px 68px; }
|
|
11
|
+
header.top { display:flex; justify-content:space-between; gap:20px; align-items:flex-start; padding-bottom:28px; border-bottom:1px solid var(--line); } header.top h1 { font-size:32px; letter-spacing:-.045em; line-height:1; margin:8px 0 0; font-weight:700; } .eyebrow { display:inline-block; padding:5px 14px; border-radius:99px; background:var(--lime); font-size:12px; font-weight:700; } header.top .meta { color:var(--muted); font-size:12px; text-align:right; }
|
|
12
|
+
.kpis { display:grid; grid-template-columns:repeat(4,1fr); margin:24px 0 52px; } .kpi { min-height:96px; padding:8px 18px; border-left:1px solid var(--line); } .kpi:first-child { border:0; padding-left:0; } .kpi .v { font-size:28px; letter-spacing:-.045em; font-weight:700; } .kpi .l { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:8px; }
|
|
13
|
+
section { margin:56px 0; } section > h2 { font-size:20px; letter-spacing:-.035em; margin:0 0 22px; } section > h2 .sub { color:var(--muted); font-size:13px; font-weight:400; letter-spacing:0; } .grid2 { display:grid; grid-template-columns:1.1fr 1fr; gap:44px; } .panel { padding:0; } .panel h3 { color:var(--muted); letter-spacing:.07em; text-transform:uppercase; font-size:12px; margin:0 0 16px; }
|
|
14
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
15
|
+
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
16
|
+
th { color: var(--muted); font-weight: 600; font-size: 11.5px; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; user-select: none; }
|
|
17
|
+
th:hover { color: var(--text); }
|
|
18
|
+
th .arrow { opacity: .5; font-size: 10px; margin-left: 3px; }
|
|
19
|
+
tbody tr:hover { background: rgba(255,255,255,0.02); }
|
|
20
|
+
tbody tr.run-row { cursor: pointer; }
|
|
21
|
+
.pill { display: inline-block; padding: 1px 8px; border-radius: 100px; font-size: 11px; font-weight: 600; }
|
|
22
|
+
.pill.ok { background: rgba(62,207,142,.15); color: var(--good); }
|
|
23
|
+
.pill.bad { background: rgba(239,90,111,.15); color: var(--bad); }
|
|
24
|
+
.pill.warn { background: rgba(232,179,57,.15); color: var(--warn); }
|
|
25
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
|
|
26
|
+
.muted { color: var(--muted); }
|
|
27
|
+
.bar-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
|
|
28
|
+
.bar-row .lbl { width: 150px; flex: 0 0 150px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
|
29
|
+
.bar-track { flex: 1; height: 9px; background: var(--wash); border-radius: 99px; overflow: hidden; display: flex; }
|
|
30
|
+
.bar-seg { height: 100%; }
|
|
31
|
+
.bar-val { width: 64px; flex: 0 0 64px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
32
|
+
.legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: 11.5px; color: var(--muted); }
|
|
33
|
+
.legend .sw { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }
|
|
34
|
+
.heat-table td { text-align: center; font-variant-numeric: tabular-nums; }
|
|
35
|
+
.heat-cell { display: inline-block; min-width: 44px; padding: 3px 6px; border-radius: 5px; font-size: 12px; }
|
|
36
|
+
details.run-detail { margin: 0; }
|
|
37
|
+
details.run-detail > summary { display: none; }
|
|
38
|
+
.timeline { margin: 0 0 18px; background:var(--wash); padding:0 14px; overflow: hidden; }
|
|
39
|
+
.timeline .step { padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
40
|
+
.timeline .step:last-child { border-bottom: none; }
|
|
41
|
+
.timeline .step .head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; margin-bottom: 4px; }
|
|
42
|
+
.timeline .step .n { color: var(--accent); font-weight: 700; font-size: 12px; }
|
|
43
|
+
.timeline .step .stats { color: var(--muted); font-size: 11.5px; }
|
|
44
|
+
.timeline .step .thought { color: var(--muted); font-size: 12.5px; margin-bottom: 6px; }
|
|
45
|
+
.timeline .step pre { background: var(--panel-2); border-radius: 6px; padding: 8px 10px; margin: 4px 0; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
|
|
46
|
+
.rc0 { color: var(--good); } .rcN { color: var(--bad); }
|
|
47
|
+
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
|
48
|
+
.toolbar select, .toolbar input { background: var(--wash); border:0; color:var(--ink); border-radius:4px; padding:7px 9px; font-size:12px; } .model-filter { align-items:end; } .model-filter > span { color:var(--muted); font-size:11px; } .model-menu { position:relative; } .model-menu summary { cursor:pointer; list-style:none; min-width:220px; padding:9px 32px 9px 11px; background:var(--wash); border-radius:5px; font-size:12px; position:relative; } .model-menu summary::-webkit-details-marker { display:none; } .model-menu summary::after { content:"⌄"; position:absolute; right:11px; color:var(--muted); } .model-options { position:absolute; z-index:2; top:calc(100% + 6px); left:0; width:260px; max-height:240px; overflow:auto; padding:6px; background:#fff; border:1px solid var(--line); box-shadow:0 10px 24px rgba(17,19,26,.1); } .model-option { display:flex; gap:9px; align-items:center; padding:8px; font-size:12px; cursor:pointer; } .model-option:hover { background:var(--wash); } .model-option input { accent-color:var(--blue); }
|
|
49
|
+
.toolbar label { color: var(--muted); font-size: 12px; }
|
|
50
|
+
footer { color: var(--muted); font-size: 11.5px; margin-top: 40px; text-align: center; }
|
|
51
|
+
svg text { fill: var(--muted); font-size: 10.5px; }
|
|
52
|
+
.axis line, .axis path { stroke: var(--line); }
|
|
53
|
+
.n-a { color: var(--muted); }
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div class="wrap">
|
|
58
|
+
<header class="top">
|
|
59
|
+
<div><h1>Benchmark report</h1><div class="meta" id="generated"></div></div>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<div class="kpis" id="kpis"></div>
|
|
63
|
+
|
|
64
|
+
<section>
|
|
65
|
+
<h2>Performance <span class="sub">run volume and reliability by model</span></h2>
|
|
66
|
+
<div class="toolbar model-filter">
|
|
67
|
+
<span>Models</span>
|
|
68
|
+
<details class="model-menu">
|
|
69
|
+
<summary id="model-filter-label">All models</summary>
|
|
70
|
+
<div id="leaderboard-model-options" class="model-options"></div>
|
|
71
|
+
</details>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="panel" style="margin-top:16px;">
|
|
74
|
+
<table id="tbl-leaderboard"></table>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<section>
|
|
79
|
+
<h2>Efficiency <span class="sub">spend and token composition</span></h2>
|
|
80
|
+
<div class="grid2">
|
|
81
|
+
<div class="panel">
|
|
82
|
+
<h3>Cost vs. average duration</h3>
|
|
83
|
+
<div id="chart-scatter"></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="panel">
|
|
86
|
+
<h3>Cost vs. average steps</h3>
|
|
87
|
+
<div id="chart-scatter-steps"></div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<section>
|
|
93
|
+
<h2>Token mix <span class="sub">prompt, completion, reasoning, and cached tokens</span></h2>
|
|
94
|
+
<div class="panel">
|
|
95
|
+
<div id="chart-tokens"></div>
|
|
96
|
+
<div class="legend" id="legend-tokens"></div>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<section id="section-series">
|
|
101
|
+
<h2>Trajectory <span class="sub">where context and cost accumulate</span></h2>
|
|
102
|
+
<div class="grid2">
|
|
103
|
+
<div class="panel">
|
|
104
|
+
<h3>Prompt tokens (context) vs. step</h3>
|
|
105
|
+
<div id="chart-line-tokens"></div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="panel">
|
|
108
|
+
<h3>Cumulative cost vs. step</h3>
|
|
109
|
+
<div id="chart-line-cost"></div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="legend" id="legend-series"></div>
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
<footer>generated by <span class="mono">astra bench --report</span> · schema <span id="schema"></span></footer>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
/*__ASTRA_DATA__*/
|
|
120
|
+
</script>
|
|
121
|
+
<script>
|
|
122
|
+
(function () {
|
|
123
|
+
function loadReportData() {
|
|
124
|
+
try {
|
|
125
|
+
var request = new XMLHttpRequest();
|
|
126
|
+
request.open("GET", "summary.json", false);
|
|
127
|
+
request.send(null);
|
|
128
|
+
if ((request.status >= 200 && request.status < 300) || request.status === 0) return JSON.parse(request.responseText);
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// Direct file access can block JSON reads; use the embedded snapshot then.
|
|
131
|
+
}
|
|
132
|
+
return window.__ASTRA_EMBEDDED_DATA__ || { kpis: {}, leaderboard: [], matrix: [], runs: [], step_series: [] };
|
|
133
|
+
}
|
|
134
|
+
var DATA = loadReportData();
|
|
135
|
+
var PALETTE = ["#5285f7", "#9160d8", "#39ae76", "#f3a021", "#d85668", "#37a8ad"];
|
|
136
|
+
var colorCache = {};
|
|
137
|
+
function colorFor(key) {
|
|
138
|
+
if (!colorCache[key]) {
|
|
139
|
+
var n = Object.keys(colorCache).length;
|
|
140
|
+
colorCache[key] = PALETTE[n % PALETTE.length];
|
|
141
|
+
}
|
|
142
|
+
return colorCache[key];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fmt(n) { return Number(n || 0).toLocaleString("en-US"); }
|
|
146
|
+
function fmt1(n) { return (Number(n) || 0).toFixed(1); }
|
|
147
|
+
function pct(n) { return Math.round((Number(n) || 0) * 100) + "%"; }
|
|
148
|
+
function fmtUsd(n) {
|
|
149
|
+
if (n == null) return '<span class="n-a">n/a</span>';
|
|
150
|
+
var v = Number(n) || 0;
|
|
151
|
+
if (v === 0) return "$0";
|
|
152
|
+
if (v < 0.01) return "$" + v.toFixed(5);
|
|
153
|
+
if (v < 1) return "$" + v.toFixed(4);
|
|
154
|
+
return "$" + v.toFixed(2);
|
|
155
|
+
}
|
|
156
|
+
function esc(s) {
|
|
157
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
158
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function el(tag, attrs, html) {
|
|
162
|
+
var e = document.createElement(tag);
|
|
163
|
+
for (var k in attrs || {}) e.setAttribute(k, attrs[k]);
|
|
164
|
+
if (html != null) e.innerHTML = html;
|
|
165
|
+
return e;
|
|
166
|
+
}
|
|
167
|
+
function svgEl(tag, attrs) {
|
|
168
|
+
var e = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
169
|
+
for (var k in attrs || {}) e.setAttribute(k, attrs[k]);
|
|
170
|
+
return e;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------- header
|
|
174
|
+
document.getElementById("generated").textContent = DATA.generated_at
|
|
175
|
+
? "Generated " + new Date(DATA.generated_at).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })
|
|
176
|
+
: "";
|
|
177
|
+
document.getElementById("schema").textContent = DATA.schema || "";
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------- KPIs
|
|
180
|
+
(function renderKpis() {
|
|
181
|
+
var k = DATA.kpis || {};
|
|
182
|
+
var cards = [
|
|
183
|
+
["Models Benchmarked", fmt(k.models)],
|
|
184
|
+
["Total duration", fmt(k.elapsed_seconds) + "s"],
|
|
185
|
+
["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
|
|
186
|
+
["Tokens used", fmt(k.tokens)],
|
|
187
|
+
];
|
|
188
|
+
var host = document.getElementById("kpis");
|
|
189
|
+
cards.forEach(function (c) {
|
|
190
|
+
host.appendChild(el("div", { class: "kpi" }, '<div class="v">' + c[1] + '</div><div class="l">' + esc(c[0]) + "</div>"));
|
|
191
|
+
});
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------- sortable table helper
|
|
195
|
+
function sortableTable(container, columns, rows, opts) {
|
|
196
|
+
opts = opts || {};
|
|
197
|
+
var state = { key: opts.defaultKey || columns[0].key, dir: opts.defaultDir || -1 };
|
|
198
|
+
function draw() {
|
|
199
|
+
var sorted = rows.slice().sort(function (a, b) {
|
|
200
|
+
var va = a[state.key], vb = b[state.key];
|
|
201
|
+
if (typeof va === "string" || typeof vb === "string") {
|
|
202
|
+
return state.dir * String(va).localeCompare(String(vb));
|
|
203
|
+
}
|
|
204
|
+
return state.dir * ((Number(va) || 0) - (Number(vb) || 0));
|
|
205
|
+
});
|
|
206
|
+
var thead = "<thead><tr>" + columns.map(function (c) {
|
|
207
|
+
var arrow = c.key === state.key ? (state.dir === 1 ? "\u2191" : "\u2193") : "";
|
|
208
|
+
return '<th data-key="' + c.key + '">' + esc(c.label) + (arrow ? ' <span class="arrow">' + arrow + "</span>" : "") + "</th>";
|
|
209
|
+
}).join("") + "</tr></thead>";
|
|
210
|
+
var tbody = "<tbody>" + sorted.map(function (row, i) {
|
|
211
|
+
var cells = columns.map(function (c) { return "<td>" + c.render(row) + "</td>"; }).join("");
|
|
212
|
+
var extra = opts.rowAttrs ? opts.rowAttrs(row, i) : "";
|
|
213
|
+
return "<tr " + extra + ">" + cells + "</tr>";
|
|
214
|
+
}).join("") + "</tbody>";
|
|
215
|
+
container.innerHTML = thead + tbody;
|
|
216
|
+
container.querySelectorAll("th").forEach(function (th) {
|
|
217
|
+
th.addEventListener("click", function () {
|
|
218
|
+
var key = th.getAttribute("data-key");
|
|
219
|
+
if (state.key === key) state.dir *= -1;
|
|
220
|
+
else { state.key = key; state.dir = -1; }
|
|
221
|
+
draw();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
if (opts.afterDraw) opts.afterDraw(container, sorted);
|
|
225
|
+
}
|
|
226
|
+
draw();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------- leaderboard chart: run volume bars
|
|
230
|
+
(function () {
|
|
231
|
+
var host = document.getElementById("chart-solve");
|
|
232
|
+
if (!host) return;
|
|
233
|
+
var maxRuns = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) { return g.runs || 0; })));
|
|
234
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
235
|
+
var row = el("div", { class: "bar-row" });
|
|
236
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
237
|
+
var track = el("div", { class: "bar-track" });
|
|
238
|
+
var seg = el("div", {
|
|
239
|
+
class: "bar-seg",
|
|
240
|
+
style: "width:" + Math.round((g.runs || 0) / maxRuns * 100) + "%;background:" + colorFor(g.slug),
|
|
241
|
+
});
|
|
242
|
+
track.appendChild(seg);
|
|
243
|
+
row.appendChild(track);
|
|
244
|
+
row.appendChild(el("div", { class: "bar-val" }, fmt(g.runs) + " runs"));
|
|
245
|
+
host.appendChild(row);
|
|
246
|
+
});
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------- outcomes stacked bar
|
|
250
|
+
(function () {
|
|
251
|
+
var host = document.getElementById("chart-outcomes");
|
|
252
|
+
var legend = document.getElementById("legend-outcomes");
|
|
253
|
+
if (!host || !legend) return;
|
|
254
|
+
var outcomeColors = { Submitted: "#3ecf8e", LimitsExceeded: "#e8b339", TimeExceeded: "#e8b339", RepeatedFormatError: "#ef5a6f", ContextWindowExceeded: "#ef5a6f", Error: "#ef5a6f", Unknown: "#8b93a7" };
|
|
255
|
+
function colorForOutcome(k) { return outcomeColors[k] || "#8b93a7"; }
|
|
256
|
+
var allOutcomes = {};
|
|
257
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
258
|
+
var row = el("div", { class: "bar-row" });
|
|
259
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
260
|
+
var track = el("div", { class: "bar-track" });
|
|
261
|
+
var total = g.runs || 1;
|
|
262
|
+
Object.keys(g.outcomes || {}).forEach(function (k) {
|
|
263
|
+
allOutcomes[k] = true;
|
|
264
|
+
var w = (g.outcomes[k] / total) * 100;
|
|
265
|
+
track.appendChild(el("div", { class: "bar-seg", style: "width:" + w + "%;background:" + colorForOutcome(k) }));
|
|
266
|
+
});
|
|
267
|
+
row.appendChild(track);
|
|
268
|
+
row.appendChild(el("div", { class: "bar-val" }, g.runs + " runs"));
|
|
269
|
+
host.appendChild(row);
|
|
270
|
+
});
|
|
271
|
+
Object.keys(allOutcomes).forEach(function (k) {
|
|
272
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colorForOutcome(k) + '"></span>' + esc(k)));
|
|
273
|
+
});
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------- leaderboard table
|
|
277
|
+
(function () {
|
|
278
|
+
var container = document.getElementById("tbl-leaderboard");
|
|
279
|
+
var options = document.getElementById("leaderboard-model-options");
|
|
280
|
+
var filterLabel = document.getElementById("model-filter-label");
|
|
281
|
+
var columns = [
|
|
282
|
+
{ key: "model", label: "Model", render: function (r) { return esc(r.model); } },
|
|
283
|
+
{ key: "reasoning", label: "Reasoning", render: function (r) { return esc(r.reasoning); } },
|
|
284
|
+
{ key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
|
|
285
|
+
{ key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
|
|
286
|
+
{ key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmt(r.sum_elapsed_seconds) + "s"; } },
|
|
287
|
+
{ key: "sum_tokens", label: "Tokens", render: function (r) { return fmt(r.sum_tokens); } },
|
|
288
|
+
{ key: "cost_usd", label: "Cost", render: function (r) { return fmtUsd(r.cost_usd) + (r.cost_source === "estimated" ? "~" : ""); } },
|
|
289
|
+
];
|
|
290
|
+
(DATA.leaderboard || []).forEach(function (r) {
|
|
291
|
+
var option = el("label", { class: "model-option" });
|
|
292
|
+
option.appendChild(el("input", { type: "checkbox", value: r.slug }));
|
|
293
|
+
option.appendChild(document.createTextNode(r.model + " · " + r.reasoning));
|
|
294
|
+
options.appendChild(option);
|
|
295
|
+
});
|
|
296
|
+
function draw() {
|
|
297
|
+
var selected = Array.prototype.slice.call(options.querySelectorAll("input:checked")).map(function (o) { return o.value; });
|
|
298
|
+
var rows = selected.length ? (DATA.leaderboard || []).filter(function (r) { return selected.indexOf(r.slug) !== -1; }) : DATA.leaderboard || [];
|
|
299
|
+
filterLabel.textContent = selected.length ? selected.length + " model" + (selected.length === 1 ? "" : "s") + " selected" : "All models";
|
|
300
|
+
sortableTable(container, columns, rows, { defaultKey: "sum_tokens" });
|
|
301
|
+
}
|
|
302
|
+
options.addEventListener("change", draw);
|
|
303
|
+
draw();
|
|
304
|
+
})();
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------- token mix stacked bar
|
|
307
|
+
(function () {
|
|
308
|
+
var host = document.getElementById("chart-tokens");
|
|
309
|
+
var legend = document.getElementById("legend-tokens");
|
|
310
|
+
var keys = ["prompt", "completion", "reasoning", "cached"];
|
|
311
|
+
var colors = { prompt: "#5b9dff", completion: "#3ecf8e", reasoning: "#b57bff", cached: "#8b93a7" };
|
|
312
|
+
var maxTotal = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) {
|
|
313
|
+
return keys.reduce(function (s, k) { return s + (g.tokens[k] || 0); }, 0);
|
|
314
|
+
})));
|
|
315
|
+
(DATA.leaderboard || []).forEach(function (g) {
|
|
316
|
+
var row = el("div", { class: "bar-row" });
|
|
317
|
+
row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
|
|
318
|
+
var track = el("div", { class: "bar-track" });
|
|
319
|
+
var total = keys.reduce(function (s, k) { return s + (g.tokens[k] || 0); }, 0);
|
|
320
|
+
keys.forEach(function (k) {
|
|
321
|
+
var w = (g.tokens[k] || 0) / maxTotal * 100;
|
|
322
|
+
if (w > 0) track.appendChild(el("div", { class: "bar-seg", style: "width:" + w + "%;background:" + colors[k] }));
|
|
323
|
+
});
|
|
324
|
+
row.appendChild(track);
|
|
325
|
+
row.appendChild(el("div", { class: "bar-val" }, fmt(total)));
|
|
326
|
+
host.appendChild(row);
|
|
327
|
+
});
|
|
328
|
+
keys.forEach(function (k) {
|
|
329
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colors[k] + '"></span>' + k));
|
|
330
|
+
});
|
|
331
|
+
})();
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------- scatter: cost vs duration
|
|
334
|
+
(function () {
|
|
335
|
+
var host = document.getElementById("chart-scatter");
|
|
336
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
337
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
338
|
+
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
339
|
+
var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
|
|
340
|
+
var maxDuration = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_elapsed_seconds || 0; }).concat([1])));
|
|
341
|
+
function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
|
|
342
|
+
function y(v) { return H - pad.b - (v / maxDuration) * (H - pad.t - pad.b); }
|
|
343
|
+
// axes
|
|
344
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
|
|
345
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
|
|
346
|
+
[0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
|
|
347
|
+
var ty = y(maxDuration * t);
|
|
348
|
+
var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
|
|
349
|
+
lab.textContent = Math.round(maxDuration * t) + "s";
|
|
350
|
+
svg.appendChild(lab);
|
|
351
|
+
});
|
|
352
|
+
var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
|
|
353
|
+
lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
|
|
354
|
+
svg.appendChild(lab2);
|
|
355
|
+
data.forEach(function (g) {
|
|
356
|
+
var cx = x(g.cost_usd), cy = y(g.avg_elapsed_seconds || 0);
|
|
357
|
+
var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
|
|
358
|
+
var c = svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 });
|
|
359
|
+
svg.appendChild(c);
|
|
360
|
+
var t = svgEl("text", { x: cx + r + 4, y: cy + 3 });
|
|
361
|
+
t.textContent = g.slug;
|
|
362
|
+
svg.appendChild(t);
|
|
363
|
+
});
|
|
364
|
+
host.appendChild(svg);
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------- scatter: cost vs average steps
|
|
368
|
+
(function () {
|
|
369
|
+
var host = document.getElementById("chart-scatter-steps");
|
|
370
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
371
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
372
|
+
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
373
|
+
var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
|
|
374
|
+
var maxSteps = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_steps || 0; }).concat([1])));
|
|
375
|
+
function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
|
|
376
|
+
function y(v) { return H - pad.b - (v / maxSteps) * (H - pad.t - pad.b); }
|
|
377
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#e4e7eb" }));
|
|
378
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#e4e7eb" }));
|
|
379
|
+
[0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
|
|
380
|
+
var ty = y(maxSteps * t);
|
|
381
|
+
var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
|
|
382
|
+
lab.textContent = fmt1(maxSteps * t);
|
|
383
|
+
svg.appendChild(lab);
|
|
384
|
+
});
|
|
385
|
+
var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
|
|
386
|
+
lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
|
|
387
|
+
svg.appendChild(lab2);
|
|
388
|
+
data.forEach(function (g) {
|
|
389
|
+
var cx = x(g.cost_usd), cy = y(g.avg_steps || 0);
|
|
390
|
+
var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
|
|
391
|
+
svg.appendChild(svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 }));
|
|
392
|
+
var label = svgEl("text", { x: cx + r + 4, y: cy + 3 });
|
|
393
|
+
label.textContent = g.slug;
|
|
394
|
+
svg.appendChild(label);
|
|
395
|
+
});
|
|
396
|
+
host.appendChild(svg);
|
|
397
|
+
})();
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------- per-step line charts
|
|
400
|
+
function lineChart(host, series, valueFn, opts) {
|
|
401
|
+
opts = opts || {};
|
|
402
|
+
var W = host.clientWidth || 460, H = 260, pad = { l: 46, r: 16, t: 14, b: 26 };
|
|
403
|
+
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
404
|
+
var maxStep = 1, maxVal = 0.0001;
|
|
405
|
+
series.forEach(function (s) {
|
|
406
|
+
s.points.forEach(function (p) {
|
|
407
|
+
if (p.step > maxStep) maxStep = p.step;
|
|
408
|
+
var v = valueFn(p);
|
|
409
|
+
if (v > maxVal) maxVal = v;
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
function x(v) { return pad.l + (v - 1) / Math.max(1, maxStep - 1) * (W - pad.l - pad.r); }
|
|
413
|
+
function y(v) { return H - pad.b - (v / maxVal) * (H - pad.t - pad.b); }
|
|
414
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
|
|
415
|
+
svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
|
|
416
|
+
[0, 0.5, 1].forEach(function (t) {
|
|
417
|
+
var ty = y(maxVal * t);
|
|
418
|
+
var lab = svgEl("text", { x: pad.l - 6, y: ty + 3, "text-anchor": "end" });
|
|
419
|
+
lab.textContent = opts.fmt ? opts.fmt(maxVal * t) : Math.round(maxVal * t);
|
|
420
|
+
svg.appendChild(lab);
|
|
421
|
+
});
|
|
422
|
+
series.forEach(function (s) {
|
|
423
|
+
var pts = s.points.slice().sort(function (a, b) { return a.step - b.step; });
|
|
424
|
+
var d = pts.map(function (p, i) { return (i === 0 ? "M" : "L") + x(p.step) + "," + y(valueFn(p)); }).join(" ");
|
|
425
|
+
svg.appendChild(svgEl("path", { d: d, fill: "none", stroke: colorFor(s.slug), "stroke-width": 2 }));
|
|
426
|
+
pts.forEach(function (p) {
|
|
427
|
+
svg.appendChild(svgEl("circle", { cx: x(p.step), cy: y(valueFn(p)), r: 2.5, fill: colorFor(s.slug) }));
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
host.appendChild(svg);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
(function () {
|
|
434
|
+
var bySlug = {};
|
|
435
|
+
(DATA.step_series || []).forEach(function (r) {
|
|
436
|
+
if (!bySlug[r.slug]) bySlug[r.slug] = [];
|
|
437
|
+
bySlug[r.slug].push(r);
|
|
438
|
+
});
|
|
439
|
+
var series = Object.keys(bySlug).map(function (slug) { return { slug: slug, points: bySlug[slug] }; });
|
|
440
|
+
if (series.length === 0) {
|
|
441
|
+
document.getElementById("section-series").style.display = "none";
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
lineChart(document.getElementById("chart-line-tokens"), series, function (p) { return p.prompt_tokens.p50; }, { fmt: fmt });
|
|
445
|
+
lineChart(document.getElementById("chart-line-cost"), series, function (p) { return p.cum_cost_usd.mean; }, { fmt: fmtUsd });
|
|
446
|
+
var legend = document.getElementById("legend-series");
|
|
447
|
+
series.forEach(function (s) {
|
|
448
|
+
legend.appendChild(el("span", {}, '<span class="sw" style="background:' + colorFor(s.slug) + '"></span>' + esc(s.slug)));
|
|
449
|
+
});
|
|
450
|
+
})();
|
|
451
|
+
|
|
452
|
+
// ---------------------------------------------------------------- model x task matrix (heatmap table)
|
|
453
|
+
(function () {
|
|
454
|
+
var matrix = DATA.matrix || [];
|
|
455
|
+
var container = document.getElementById("tbl-matrix");
|
|
456
|
+
if (!container) return;
|
|
457
|
+
if (matrix.length === 0) {
|
|
458
|
+
document.getElementById("section-matrix").style.display = "none";
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
var tasks = [];
|
|
462
|
+
var slugs = [];
|
|
463
|
+
var cell = {};
|
|
464
|
+
matrix.forEach(function (m) {
|
|
465
|
+
if (tasks.indexOf(m.task_id) === -1) tasks.push(m.task_id);
|
|
466
|
+
if (slugs.indexOf(m.slug) === -1) slugs.push(m.slug);
|
|
467
|
+
cell[m.slug + "\u0000" + m.task_id] = m;
|
|
468
|
+
});
|
|
469
|
+
function heatColor(rate) {
|
|
470
|
+
if (rate == null) return "transparent";
|
|
471
|
+
var g = Math.round(60 + rate * 140);
|
|
472
|
+
var r = Math.round(220 - rate * 160);
|
|
473
|
+
return "rgba(" + r + "," + g + ",120,0.35)";
|
|
474
|
+
}
|
|
475
|
+
var thead = "<thead><tr><th>Model</th>" + tasks.map(function (t) { return "<th>" + esc(t) + "</th>"; }).join("") + "</tr></thead>";
|
|
476
|
+
var tbody = "<tbody>" + slugs.map(function (slug) {
|
|
477
|
+
var cells = tasks.map(function (t) {
|
|
478
|
+
var m = cell[slug + "\u0000" + t];
|
|
479
|
+
if (!m) return "<td>—</td>";
|
|
480
|
+
return '<td><span class="heat-cell" style="background:' + heatColor(m.pass_at_k) + '">' + m.passed + "/" + m.k + "<br>" + pct(m.pass_at_k) + "</span></td>";
|
|
481
|
+
}).join("");
|
|
482
|
+
return "<tr><td class=\"mono\">" + esc(slug) + "</td>" + cells + "</tr>";
|
|
483
|
+
}).join("") + "</tbody>";
|
|
484
|
+
container.innerHTML = thead + tbody;
|
|
485
|
+
})();
|
|
486
|
+
|
|
487
|
+
// ---------------------------------------------------------------- runs table + filters + expandable timeline
|
|
488
|
+
(function () {
|
|
489
|
+
var runs = DATA.runs || [];
|
|
490
|
+
var container = document.getElementById("tbl-runs");
|
|
491
|
+
var fModel = document.getElementById("filter-model");
|
|
492
|
+
var fTask = document.getElementById("filter-task");
|
|
493
|
+
var fOutcome = document.getElementById("filter-outcome");
|
|
494
|
+
if (!container || !fModel || !fTask || !fOutcome) return;
|
|
495
|
+
|
|
496
|
+
function uniq(fn) {
|
|
497
|
+
var seen = {}, out = [];
|
|
498
|
+
runs.forEach(function (r) { var v = fn(r); if (v && !seen[v]) { seen[v] = 1; out.push(v); } });
|
|
499
|
+
return out.sort();
|
|
500
|
+
}
|
|
501
|
+
uniq(function (r) { return r.slug; }).forEach(function (v) { fModel.appendChild(el("option", { value: v }, esc(v))); });
|
|
502
|
+
uniq(function (r) { return r.task_id; }).forEach(function (v) { fTask.appendChild(el("option", { value: v }, esc(v))); });
|
|
503
|
+
uniq(function (r) { return r.exit_status; }).forEach(function (v) { fOutcome.appendChild(el("option", { value: v }, esc(v))); });
|
|
504
|
+
|
|
505
|
+
var columns = [
|
|
506
|
+
{ key: "slug", label: "Model", render: function (r) { return '<span class="mono">' + esc(r.slug) + "</span>"; } },
|
|
507
|
+
{ key: "task_id", label: "Task", render: function (r) { return esc(r.task_id); } },
|
|
508
|
+
{ key: "run_id", label: "Run", render: function (r) { return esc(r.run_id); } },
|
|
509
|
+
{ key: "exit_status", label: "Outcome", render: function (r) {
|
|
510
|
+
var cls = r.resolved ? "ok" : (r.exit_status === "Error" ? "bad" : "warn");
|
|
511
|
+
return '<span class="pill ' + cls + '">' + esc(r.exit_status || "Unknown") + "</span>";
|
|
512
|
+
} },
|
|
513
|
+
{ key: "steps", label: "Steps", render: function (r) { return fmt(r.steps); } },
|
|
514
|
+
{ key: "elapsed_seconds", label: "Time", render: function (r) { return fmt(r.elapsed_seconds) + "s"; } },
|
|
515
|
+
{ key: "tokens", label: "Tokens", render: function (r) { return fmt(r.tokens.total); } },
|
|
516
|
+
{ key: "cost_usd", label: "Cost", render: function (r) { return fmtUsd(r.cost_usd) + (r.cost_source === "estimated" ? "~" : ""); } },
|
|
517
|
+
{ key: "n_failed_commands", label: "Fails", render: function (r) { return fmt(r.n_failed_commands); } },
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
function renderTimeline(run) {
|
|
521
|
+
var box = el("div", { class: "timeline" });
|
|
522
|
+
(run.timeline || []).forEach(function (s) {
|
|
523
|
+
var rcClass = s.returncode === 0 ? "rc0" : (s.returncode == null ? "muted" : "rcN");
|
|
524
|
+
var step = el("div", { class: "step" });
|
|
525
|
+
step.innerHTML =
|
|
526
|
+
'<div class="head"><span class="n">step ' + s.step + '</span>' +
|
|
527
|
+
'<span class="stats">' +
|
|
528
|
+
fmt(s.tokens.total) + " tok" +
|
|
529
|
+
(s.cost_usd != null ? " \u00b7 " + fmtUsd(s.cost_usd) : "") +
|
|
530
|
+
(s.returncode != null ? ' \u00b7 rc=<span class="' + rcClass + '">' + s.returncode + "</span>" : "") +
|
|
531
|
+
"</span></div>" +
|
|
532
|
+
(s.thought ? '<div class="thought">' + esc(s.thought) + "</div>" : "") +
|
|
533
|
+
(s.command ? "<pre>$ " + esc(s.command) + "</pre>" : "") +
|
|
534
|
+
(s.output_preview ? "<pre>" + esc(s.output_preview) + "</pre>" : "");
|
|
535
|
+
box.appendChild(step);
|
|
536
|
+
});
|
|
537
|
+
return box;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function draw() {
|
|
541
|
+
var filtered = runs.filter(function (r) {
|
|
542
|
+
if (fModel.value && r.slug !== fModel.value) return false;
|
|
543
|
+
if (fTask.value && r.task_id !== fTask.value) return false;
|
|
544
|
+
if (fOutcome.value && r.exit_status !== fOutcome.value) return false;
|
|
545
|
+
return true;
|
|
546
|
+
});
|
|
547
|
+
sortableTable(container, columns, filtered, {
|
|
548
|
+
defaultKey: "slug",
|
|
549
|
+
defaultDir: 1,
|
|
550
|
+
rowAttrs: function (r, i) { return 'class="run-row" data-idx="' + i + '"'; },
|
|
551
|
+
afterDraw: function (containerEl, sorted) {
|
|
552
|
+
containerEl.querySelectorAll("tbody tr").forEach(function (tr) {
|
|
553
|
+
var idx = Number(tr.getAttribute("data-idx"));
|
|
554
|
+
var run = sorted[idx];
|
|
555
|
+
tr.addEventListener("click", function () {
|
|
556
|
+
var next = tr.nextElementSibling;
|
|
557
|
+
if (next && next.classList.contains("detail-row")) {
|
|
558
|
+
next.remove();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
containerEl.querySelectorAll(".detail-row").forEach(function (d) { d.remove(); });
|
|
562
|
+
var detailRow = document.createElement("tr");
|
|
563
|
+
detailRow.className = "detail-row";
|
|
564
|
+
var td = document.createElement("td");
|
|
565
|
+
td.colSpan = columns.length;
|
|
566
|
+
td.appendChild(renderTimeline(run));
|
|
567
|
+
detailRow.appendChild(td);
|
|
568
|
+
tr.parentNode.insertBefore(detailRow, tr.nextSibling);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
[fModel, fTask, fOutcome].forEach(function (elm) { elm.addEventListener("change", draw); });
|
|
575
|
+
draw();
|
|
576
|
+
})();
|
|
577
|
+
})();
|
|
578
|
+
</script>
|
|
579
|
+
</body>
|
|
580
|
+
</html>
|
package/src/report.js
CHANGED
|
@@ -101,7 +101,9 @@ export function buildSummary(runs) {
|
|
|
101
101
|
solved_rate: rs.length ? solved / rs.length : 0,
|
|
102
102
|
outcomes,
|
|
103
103
|
avg_steps: avg(rs, (r) => r.steps),
|
|
104
|
+
sum_steps: sum(rs, (r) => r.steps),
|
|
104
105
|
avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
|
|
106
|
+
sum_elapsed_seconds: sum(rs, (r) => r.elapsed_seconds),
|
|
105
107
|
avg_tokens: avg(rs, (r) => r.tokens.total),
|
|
106
108
|
sum_tokens: sum(rs, (r) => r.tokens.total),
|
|
107
109
|
tokens: {
|
|
@@ -221,7 +223,7 @@ export function renderReportHtml(summary) {
|
|
|
221
223
|
if (!tpl.includes("/*__ASTRA_DATA__*/")) {
|
|
222
224
|
throw new Error("src/report.html is missing the /*__ASTRA_DATA__*/ injection marker");
|
|
223
225
|
}
|
|
224
|
-
return tpl.replace("/*__ASTRA_DATA__*/", `window.
|
|
226
|
+
return tpl.replace("/*__ASTRA_DATA__*/", `window.__ASTRA_EMBEDDED_DATA__ = ${data};`);
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
function toRunDoc(run) {
|