@hackerrank/astra-cli 0.1.3 → 0.1.5
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 +45 -5
- package/package.json +7 -3
- package/src/agent.js +1 -1
- package/src/bench.js +30 -12
- package/src/cli.js +65 -30
- package/src/project.js +177 -0
- package/src/report.html +6 -6
- package/src/report.js +32 -17
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ 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
|
```
|
|
122
122
|
|
|
123
|
-
Leaderboard columns:
|
|
123
|
+
Leaderboard columns: completed generation rate, average `steps` / `tokens`, and
|
|
124
124
|
`cost`. Cost is per-run summed; a model whose route reports no cost and has no
|
|
125
125
|
price entry shows **`n/a`** (never `$0`), and estimated costs (native routes) are
|
|
126
126
|
marked `~`. `--push` requires an `s3://` URI and the `aws` CLI on PATH; if the
|
|
@@ -132,13 +132,52 @@ dashboard with `Content-Type: text/html` and prints a **7-day presigned
|
|
|
132
132
|
read), so that presigned URL is the only way to open the dashboard — e.g.
|
|
133
133
|
clickable straight from a Jenkins console log.
|
|
134
134
|
|
|
135
|
+
### Project-configured bench runs
|
|
136
|
+
|
|
137
|
+
For repeatable benchmarks, keep the benchmark definition with the task and run
|
|
138
|
+
it through the existing bench persona:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
astra bench --project ./task
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`./task/.astra/project.toml` selects the public workspace, instruction,
|
|
145
|
+
models, limits, output root, and optional bench template/extensions. CLI flags
|
|
146
|
+
still override bench limits and output paths. Astra copies only the configured
|
|
147
|
+
workspace; container lifecycle, services, secrets, verification, and scoring
|
|
148
|
+
remain harness responsibilities. Run artifacts include `project.json` with the
|
|
149
|
+
project and input hashes. `completed` means the agent emitted its completion
|
|
150
|
+
sentinel—not that the candidate is correct; `model_error` and `request_error`
|
|
151
|
+
distinguish generation/provider failures from invalid invocation/configuration.
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
[project]
|
|
155
|
+
id = "peoplecore-scim"
|
|
156
|
+
version = 1
|
|
157
|
+
instruction = "instruction.md"
|
|
158
|
+
workspace = "public/codebase"
|
|
159
|
+
|
|
160
|
+
[bench]
|
|
161
|
+
models = ["gpt-5.6-sol"]
|
|
162
|
+
reasoning = ["medium"]
|
|
163
|
+
repeat = 3
|
|
164
|
+
steps = 60
|
|
165
|
+
wall_seconds = 3600
|
|
166
|
+
command_timeout_seconds = 120
|
|
167
|
+
|
|
168
|
+
[templates]
|
|
169
|
+
bench = ".astra/templates/bench.md"
|
|
170
|
+
[extensions]
|
|
171
|
+
directories = [".astra/extensions"]
|
|
172
|
+
```
|
|
173
|
+
|
|
135
174
|
### HTML report
|
|
136
175
|
|
|
137
176
|
Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
|
|
138
177
|
`bench/summary.json` and a self-contained `bench/report.html` dashboard —
|
|
139
178
|
no server, no build step, no external JS: open it straight from `file://` or
|
|
140
179
|
from inside the `bench-*.tgz` tarball. It has a leaderboard, a model × task
|
|
141
|
-
|
|
180
|
+
completion-rate heatmap, cost/token charts, per-step trend lines, and an
|
|
142
181
|
expandable command timeline for every run.
|
|
143
182
|
|
|
144
183
|
Regenerate it on demand (e.g. after manually editing/pruning `bench/`) with:
|
|
@@ -150,7 +189,7 @@ astra --report --bench-root ./other-bench
|
|
|
150
189
|
|
|
151
190
|
`summary.json` follows the `astra-bench-1` schema: `kpis` (totals), a tidy
|
|
152
191
|
`leaderboard[]` (one row per model×reasoning), `matrix[]` (model×reasoning×task
|
|
153
|
-
|
|
192
|
+
completion telemetry), `runs[]` (every attempt, with a compact per-step `timeline`), and
|
|
154
193
|
`step_series[]` (token/cost distributions by step, for the trend charts). Each
|
|
155
194
|
run folder also gets a standalone `run.json` for drill-down without loading
|
|
156
195
|
the full `trajectory.json`.
|
|
@@ -178,6 +217,7 @@ credits — that's a quota issue, not a bug.
|
|
|
178
217
|
-t, --task <text> Task text -> autonomous mode
|
|
179
218
|
-f, --task-file <path> Read task text from a file -> autonomous mode
|
|
180
219
|
-p, --path <dir> Task directory -> isolated bench workspace
|
|
220
|
+
--project <path> Load .astra/project.toml for an explicit bench run
|
|
181
221
|
--repeat <n> Attempts per (model,reasoning) in a matrix (default: 1)
|
|
182
222
|
--bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
183
223
|
--tar After a matrix, write a bench/ tarball locally
|
|
@@ -188,7 +228,7 @@ credits — that's a quota issue, not a bug.
|
|
|
188
228
|
whatever runs already exist on disk, then exit
|
|
189
229
|
-C, --cwd <path> Working directory for commands (default: cwd)
|
|
190
230
|
-o, --output <path> Also write trajectory JSON here (autonomous mode)
|
|
191
|
-
-s, --steps <n>
|
|
231
|
+
-s, --steps <n> Turn limit (unset = no limit)
|
|
192
232
|
-w, --wall <seconds> Wall-clock limit (default: 0 = none)
|
|
193
233
|
--timeout <seconds> Per-command timeout (default: 60)
|
|
194
234
|
--max-output <n> Max chars of command output kept (default: 16000)
|
|
@@ -197,7 +237,7 @@ credits — that's a quota issue, not a bug.
|
|
|
197
237
|
--resume <id> Resume a saved session
|
|
198
238
|
--sessions List saved sessions and exit
|
|
199
239
|
-y, --yolo Auto-run commands (always on in autonomous mode)
|
|
200
|
-
-q, --quiet Do not stream
|
|
240
|
+
-q, --quiet Do not stream turns (autonomous mode)
|
|
201
241
|
-h, --help Show help
|
|
202
242
|
```
|
|
203
243
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackerrank/astra-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Minimal zero-dependency AI coding agent for the HackerRank AI Gateway.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node src/cli.js",
|
|
19
|
-
"astra": "node src/cli.js"
|
|
19
|
+
"astra": "node src/cli.js",
|
|
20
|
+
"test": "node --test"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -26,5 +27,8 @@
|
|
|
26
27
|
"type": "git",
|
|
27
28
|
"url": "git+ssh://git@github.com/interviewstreet/hrbench.git"
|
|
28
29
|
},
|
|
29
|
-
"license": "MIT"
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@iarna/toml": "2.2.5"
|
|
33
|
+
}
|
|
30
34
|
}
|
package/src/agent.js
CHANGED
|
@@ -38,7 +38,7 @@ export class Agent {
|
|
|
38
38
|
this.model = model;
|
|
39
39
|
this.env = env;
|
|
40
40
|
this.mode = opts.mode === "interactive" ? "interactive" : "autonomous";
|
|
41
|
-
this.stepLimit = opts.stepLimit ??
|
|
41
|
+
this.stepLimit = opts.stepLimit ?? 0;
|
|
42
42
|
this.wallTimeLimitSeconds = opts.wallTimeLimitSeconds ?? 0;
|
|
43
43
|
this.maxConsecutiveFormatErrors = opts.maxConsecutiveFormatErrors ?? 3;
|
|
44
44
|
this.outputPath = opts.outputPath ?? null;
|
package/src/bench.js
CHANGED
|
@@ -30,7 +30,8 @@ export const METRIC_COLUMNS = [
|
|
|
30
30
|
"model",
|
|
31
31
|
"reasoning",
|
|
32
32
|
"exit_status",
|
|
33
|
-
"
|
|
33
|
+
"status",
|
|
34
|
+
"completed",
|
|
34
35
|
"steps",
|
|
35
36
|
"n_commands",
|
|
36
37
|
"n_failed_commands",
|
|
@@ -104,6 +105,7 @@ export function allocateRun({ model, reasoning, root } = {}) {
|
|
|
104
105
|
*/
|
|
105
106
|
export function seedWorkspace(workspace, { taskPath, taskFile, taskText } = {}) {
|
|
106
107
|
let text = taskText ?? "";
|
|
108
|
+
const hasExplicitTask = taskText !== undefined && taskText !== null;
|
|
107
109
|
let copyFrom = null;
|
|
108
110
|
let instructionName = null;
|
|
109
111
|
|
|
@@ -114,7 +116,7 @@ export function seedWorkspace(workspace, { taskPath, taskFile, taskText } = {})
|
|
|
114
116
|
copyFrom = resolved;
|
|
115
117
|
// Prefer a conventional instruction file for the task text.
|
|
116
118
|
for (const name of ["instruction.md", "INSTRUCTION.md", "task.md", "README.md"]) {
|
|
117
|
-
if (fs.existsSync(path.join(resolved, name))) {
|
|
119
|
+
if (!hasExplicitTask && fs.existsSync(path.join(resolved, name))) {
|
|
118
120
|
text = fs.readFileSync(path.join(resolved, name), "utf8");
|
|
119
121
|
break;
|
|
120
122
|
}
|
|
@@ -122,13 +124,13 @@ export function seedWorkspace(workspace, { taskPath, taskFile, taskText } = {})
|
|
|
122
124
|
} else {
|
|
123
125
|
copyFrom = path.dirname(resolved);
|
|
124
126
|
instructionName = path.basename(resolved);
|
|
125
|
-
text = fs.readFileSync(resolved, "utf8");
|
|
127
|
+
if (!hasExplicitTask) text = fs.readFileSync(resolved, "utf8");
|
|
126
128
|
}
|
|
127
129
|
} else if (taskFile) {
|
|
128
130
|
const resolved = path.resolve(taskFile);
|
|
129
131
|
copyFrom = path.dirname(resolved);
|
|
130
132
|
instructionName = path.basename(resolved);
|
|
131
|
-
text = fs.readFileSync(resolved, "utf8");
|
|
133
|
+
if (!hasExplicitTask) text = fs.readFileSync(resolved, "utf8");
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
if (copyFrom) copyDir(copyFrom, workspace);
|
|
@@ -162,13 +164,14 @@ function copyDir(from, to) {
|
|
|
162
164
|
/** Build a metrics row object from a finished agent + run info. */
|
|
163
165
|
export function collectMetrics({ agent, model, reasoning }) {
|
|
164
166
|
const info = agent.serialize().info;
|
|
165
|
-
const
|
|
167
|
+
const status = generationStatus(info.exit_status);
|
|
166
168
|
return {
|
|
167
169
|
timestamp: new Date().toISOString(),
|
|
168
170
|
model: model.model,
|
|
169
171
|
reasoning: reasoning || "none",
|
|
170
172
|
exit_status: info.exit_status || "",
|
|
171
|
-
|
|
173
|
+
status,
|
|
174
|
+
completed: status === "completed" ? 1 : 0,
|
|
172
175
|
steps: info.n_steps ?? 0,
|
|
173
176
|
n_commands: agent.nCommands ?? 0,
|
|
174
177
|
n_failed_commands: agent.nFailedCommands ?? 0,
|
|
@@ -188,6 +191,11 @@ export function collectMetrics({ agent, model, reasoning }) {
|
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
/** Classify autonomous generation without making a correctness claim. */
|
|
195
|
+
export function generationStatus(exitStatus) {
|
|
196
|
+
return exitStatus === "Submitted" ? "completed" : "model_error";
|
|
197
|
+
}
|
|
198
|
+
|
|
191
199
|
/** Serialize a metrics row to a single CSV line (columns in METRIC_COLUMNS order). */
|
|
192
200
|
export function metricsRow(m) {
|
|
193
201
|
return METRIC_COLUMNS.map((c) => csvCell(m[c])).join(",");
|
|
@@ -219,6 +227,14 @@ export function writeMetrics({ runDir, root, metrics }) {
|
|
|
219
227
|
return { runMetrics: path.join(runDir, "metrics.csv"), index: indexPath };
|
|
220
228
|
}
|
|
221
229
|
|
|
230
|
+
/** Persist the project-owned inputs that defined a bench attempt. */
|
|
231
|
+
export function writeProjectMetadata(runDir, metadata) {
|
|
232
|
+
if (!metadata) return null;
|
|
233
|
+
const file = path.join(runDir, "project.json");
|
|
234
|
+
fs.writeFileSync(file, JSON.stringify({ schema: "astra-project-1", ...metadata }, null, 2) + "\n");
|
|
235
|
+
return file;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
function csvCell(v) {
|
|
223
239
|
const s = v == null ? "" : String(v);
|
|
224
240
|
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
@@ -264,8 +280,9 @@ export async function runCell({
|
|
|
264
280
|
task,
|
|
265
281
|
taskPath,
|
|
266
282
|
taskFile,
|
|
283
|
+
projectMetadata,
|
|
267
284
|
root,
|
|
268
|
-
steps =
|
|
285
|
+
steps = 0,
|
|
269
286
|
wall = 0,
|
|
270
287
|
timeout = 60,
|
|
271
288
|
maxOutputChars = 16000,
|
|
@@ -278,6 +295,7 @@ export async function runCell({
|
|
|
278
295
|
const seeded = seedWorkspace(alloc.workspace, { taskPath, taskFile, taskText: task });
|
|
279
296
|
const taskText = seeded.taskText || task || "";
|
|
280
297
|
fs.writeFileSync(path.join(alloc.dir, "task.md"), taskText);
|
|
298
|
+
writeProjectMetadata(alloc.dir, projectMetadata);
|
|
281
299
|
|
|
282
300
|
const gw = new GatewayModel({
|
|
283
301
|
model,
|
|
@@ -340,7 +358,7 @@ export async function runMatrix({
|
|
|
340
358
|
|
|
341
359
|
/**
|
|
342
360
|
* Aggregate metrics rows into a per-(model,reasoning) leaderboard.
|
|
343
|
-
* `
|
|
361
|
+
* `completed` counts a completed generation. Cost is summed only over runs that reported a
|
|
344
362
|
* cost; a group with no cost data is reported as null (rendered "n/a"), never $0.
|
|
345
363
|
*/
|
|
346
364
|
export function aggregate(rows) {
|
|
@@ -352,7 +370,7 @@ export function aggregate(rows) {
|
|
|
352
370
|
model: r.model,
|
|
353
371
|
reasoning: r.reasoning || "none",
|
|
354
372
|
runs: 0,
|
|
355
|
-
|
|
373
|
+
completed: 0,
|
|
356
374
|
steps: 0,
|
|
357
375
|
totalTokens: 0,
|
|
358
376
|
costUsd: 0,
|
|
@@ -362,7 +380,7 @@ export function aggregate(rows) {
|
|
|
362
380
|
}
|
|
363
381
|
const g = groups.get(key);
|
|
364
382
|
g.runs++;
|
|
365
|
-
g.
|
|
383
|
+
g.completed += Number(r.completed) ? 1 : 0;
|
|
366
384
|
g.steps += Number(r.steps) || 0;
|
|
367
385
|
g.totalTokens += Number(r.total_tokens) || 0;
|
|
368
386
|
if (r.cost_source && r.cost_source !== "") {
|
|
@@ -375,8 +393,8 @@ export function aggregate(rows) {
|
|
|
375
393
|
model: g.model,
|
|
376
394
|
reasoning: g.reasoning,
|
|
377
395
|
runs: g.runs,
|
|
378
|
-
|
|
379
|
-
|
|
396
|
+
completed: g.completed,
|
|
397
|
+
completion_rate: g.runs ? g.completed / g.runs : 0,
|
|
380
398
|
avg_steps: g.runs ? g.steps / g.runs : 0,
|
|
381
399
|
avg_tokens: g.runs ? g.totalTokens / g.runs : 0,
|
|
382
400
|
cost_usd: g.costRuns ? g.costUsd : null,
|
package/src/cli.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* astra -m <model> -t "<task text>" bench run from task text
|
|
17
17
|
* astra -m <model> -f path/to/instruction.md
|
|
18
18
|
* astra -m <model> -p tasks/dummy-slugify bench run from a task directory
|
|
19
|
+
* astra bench --project ./task project-configured bench run
|
|
19
20
|
* astra --resume <session-id> resume a saved session
|
|
20
21
|
* astra --sessions list saved sessions
|
|
21
22
|
*
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
* --report Rebuild bench/summary.json + bench/report.html
|
|
37
38
|
* from whatever runs already exist on disk, then
|
|
38
39
|
* exit (no model/API key needed).
|
|
40
|
+
* --project <path> Load .astra/project.toml and run its bench setup
|
|
39
41
|
* -t, --task <text> Task text -> bench mode (run to completion)
|
|
40
42
|
* -f, --task-file <path> Read task text from a file -> bench mode
|
|
41
43
|
* -p, --path <dir> Task directory (copied into an isolated bench
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
* --bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
44
46
|
* -C, --cwd <path> Working directory for commands (default: cwd)
|
|
45
47
|
* -o, --output <path> Also write trajectory JSON here (bench mode)
|
|
46
|
-
* -s, --steps <n>
|
|
48
|
+
* -s, --steps <n> Turn limit (unset = no limit)
|
|
47
49
|
* -w, --wall <seconds> Wall-clock limit (default: 0 = none)
|
|
48
50
|
* --timeout <seconds> Per-command timeout (default: 60)
|
|
49
51
|
* --max-output <n> Max chars of command output kept (default: 16000)
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
* --sessions List saved sessions and exit
|
|
54
56
|
* -y, --yolo Auto-run commands without confirmation
|
|
55
57
|
* (always on in bench mode)
|
|
56
|
-
* -q, --quiet Do not stream
|
|
58
|
+
* -q, --quiet Do not stream turns (bench mode)
|
|
57
59
|
* -h, --help Show this help
|
|
58
60
|
*
|
|
59
61
|
* API key resolution (first hit wins):
|
|
@@ -86,11 +88,13 @@ import {
|
|
|
86
88
|
deriveTitle,
|
|
87
89
|
} from "./session.js";
|
|
88
90
|
import { runRepl } from "./repl.js";
|
|
91
|
+
import { ProjectConfigError, loadProject } from "./project.js";
|
|
89
92
|
import {
|
|
90
93
|
allocateRun,
|
|
91
94
|
seedWorkspace,
|
|
92
95
|
collectMetrics,
|
|
93
96
|
writeMetrics,
|
|
97
|
+
writeProjectMetadata,
|
|
94
98
|
runMatrix,
|
|
95
99
|
aggregate,
|
|
96
100
|
packBench,
|
|
@@ -109,7 +113,7 @@ function reasoningKwargs(level) {
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
function parseArgs(argv) {
|
|
112
|
-
const args = { steps:
|
|
116
|
+
const args = { steps: 0, wall: 0, timeout: 60, quiet: false, "max-output": 16000, _provided: new Set() };
|
|
113
117
|
const alias = {
|
|
114
118
|
"-m": "model", "--model": "model",
|
|
115
119
|
"-t": "task", "--task": "task",
|
|
@@ -121,6 +125,7 @@ function parseArgs(argv) {
|
|
|
121
125
|
"--push-uri": "push-uri",
|
|
122
126
|
"--tar": "tar",
|
|
123
127
|
"--report": "report",
|
|
128
|
+
"--project": "project",
|
|
124
129
|
"--bench-root": "bench-root",
|
|
125
130
|
"-C": "cwd", "--cwd": "cwd",
|
|
126
131
|
"-o": "output", "--output": "output",
|
|
@@ -137,11 +142,17 @@ function parseArgs(argv) {
|
|
|
137
142
|
"-h": "help", "--help": "help",
|
|
138
143
|
};
|
|
139
144
|
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push", "report"]);
|
|
140
|
-
|
|
145
|
+
let start = 2;
|
|
146
|
+
if (argv[start] === "bench") {
|
|
147
|
+
args.bench = true;
|
|
148
|
+
start++;
|
|
149
|
+
}
|
|
150
|
+
for (let i = start; i < argv.length; i++) {
|
|
141
151
|
const key = alias[argv[i]];
|
|
142
152
|
if (!key) { console.error(`Unknown option: ${argv[i]}`); process.exit(2); }
|
|
143
153
|
if (flags.has(key)) { args[key] = true; continue; }
|
|
144
154
|
args[key] = argv[++i];
|
|
155
|
+
args._provided.add(key);
|
|
145
156
|
}
|
|
146
157
|
return args;
|
|
147
158
|
}
|
|
@@ -183,11 +194,28 @@ async function main() {
|
|
|
183
194
|
resumeDoc = loadSession(args.resume);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
// A task (-t/-f)
|
|
197
|
+
// A task (-t/-f), task path (-p), or explicit bench project selects bench mode.
|
|
187
198
|
// Bench mode uses the autonomous engine; agent mode uses the interactive one.
|
|
188
199
|
let task = args.task;
|
|
189
200
|
if (args["task-file"]) task = fs.readFileSync(args["task-file"], "utf8");
|
|
190
|
-
|
|
201
|
+
let project = null;
|
|
202
|
+
if (args.project) {
|
|
203
|
+
try {
|
|
204
|
+
project = loadProject(args.project);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const message = error instanceof ProjectConfigError ? error.message : String(error?.message || error);
|
|
207
|
+
console.error(`\x1b[31m[astra] request_error: ${message}\x1b[0m`);
|
|
208
|
+
process.exit(2);
|
|
209
|
+
}
|
|
210
|
+
task = project.taskText;
|
|
211
|
+
args.path = project.workspace;
|
|
212
|
+
if (!args._provided.has("bench-root")) args["bench-root"] = project.outputRoot;
|
|
213
|
+
if (!args._provided.has("steps")) args.steps = project.bench.steps;
|
|
214
|
+
if (!args._provided.has("wall")) args.wall = project.bench.wall;
|
|
215
|
+
if (!args._provided.has("timeout")) args.timeout = project.bench.timeout;
|
|
216
|
+
if (!args._provided.has("repeat")) args.repeat = project.bench.repeat;
|
|
217
|
+
}
|
|
218
|
+
const wantsBench = !!(task || args.path || args.bench);
|
|
191
219
|
const mode = wantsBench
|
|
192
220
|
? "autonomous"
|
|
193
221
|
: resumeDoc?.info?.mode === "autonomous"
|
|
@@ -236,6 +264,8 @@ async function main() {
|
|
|
236
264
|
let reasoning = args.reasoning || "";
|
|
237
265
|
|
|
238
266
|
if (mode === "autonomous") {
|
|
267
|
+
if (project && !modelId) modelId = project.bench.models.join(",");
|
|
268
|
+
if (project && !reasoning) reasoning = project.bench.reasoning.join(",");
|
|
239
269
|
if (!modelId) {
|
|
240
270
|
console.error("\x1b[31m[astra] --model is required for bench mode.\x1b[0m\n");
|
|
241
271
|
console.log(HELP);
|
|
@@ -254,7 +284,7 @@ async function main() {
|
|
|
254
284
|
const isMatrix = models.length > 1 || reasonings.length > 1 || repeat > 1;
|
|
255
285
|
|
|
256
286
|
if (isMatrix && !resumeDoc) {
|
|
257
|
-
await runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet: !!args.quiet });
|
|
287
|
+
await runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, project, quiet: !!args.quiet });
|
|
258
288
|
return; // runBenchMatrix exits the process
|
|
259
289
|
}
|
|
260
290
|
} else {
|
|
@@ -310,6 +340,7 @@ async function main() {
|
|
|
310
340
|
task = seeded.taskText || task || "";
|
|
311
341
|
// Persist the task text alongside the run for reproducibility.
|
|
312
342
|
fs.writeFileSync(path.join(benchRun.dir, "task.md"), task || "");
|
|
343
|
+
writeProjectMetadata(benchRun.dir, project?.metadata);
|
|
313
344
|
}
|
|
314
345
|
|
|
315
346
|
const cmdCwd = benchRun ? benchRun.workspace : cwd;
|
|
@@ -388,11 +419,15 @@ async function main() {
|
|
|
388
419
|
if (benchRun) {
|
|
389
420
|
console.error(
|
|
390
421
|
`\x1b[2m[astra] bench · ${benchRun.slug}/${benchRun.runId} ` +
|
|
391
|
-
`· model=${modelId}${reasoning ? ` reasoning=${reasoning}` : ""}
|
|
422
|
+
`· model=${modelId}${reasoning ? ` reasoning=${reasoning}` : ""}` +
|
|
423
|
+
`${Number(args.steps) > 0 ? ` turns<=${args.steps}` : ""}\x1b[0m`
|
|
392
424
|
);
|
|
393
425
|
console.error(`\x1b[2m[astra] workspace -> ${benchRun.workspace}\x1b[0m`);
|
|
394
426
|
} else {
|
|
395
|
-
console.error(
|
|
427
|
+
console.error(
|
|
428
|
+
`\x1b[2m[astra] bench · model=${modelId} cwd=${cmdCwd}` +
|
|
429
|
+
`${Number(args.steps) > 0 ? ` turns<=${args.steps}` : ""}\x1b[0m`
|
|
430
|
+
);
|
|
396
431
|
}
|
|
397
432
|
}
|
|
398
433
|
|
|
@@ -420,7 +455,7 @@ async function main() {
|
|
|
420
455
|
const pt = model.totalPromptTokens;
|
|
421
456
|
const ct = model.totalCompletionTokens;
|
|
422
457
|
console.error(
|
|
423
|
-
`\n\x1b[1m[astra] exit=${result.exit_status}
|
|
458
|
+
`\n\x1b[1m[astra] exit=${result.exit_status} turns=${agent.nSteps}\x1b[0m`
|
|
424
459
|
);
|
|
425
460
|
console.error(
|
|
426
461
|
`\x1b[1m[astra] tokens: prompt=${fmt(pt)} completion=${fmt(ct)} total=${fmt(pt + ct)} ` +
|
|
@@ -510,7 +545,7 @@ function archiveAndPush(args, root, links) {
|
|
|
510
545
|
* Run a multi-model / multi-reasoning / repeated bench matrix, print a
|
|
511
546
|
* leaderboard, optionally tar + upload the bench/ folder, then exit.
|
|
512
547
|
*/
|
|
513
|
-
async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet }) {
|
|
548
|
+
async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, project, quiet }) {
|
|
514
549
|
const root = args["bench-root"];
|
|
515
550
|
const total = models.length * reasonings.length * repeat;
|
|
516
551
|
console.error(
|
|
@@ -528,6 +563,7 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
528
563
|
task,
|
|
529
564
|
taskPath: args.path,
|
|
530
565
|
taskFile: args["task-file"],
|
|
566
|
+
projectMetadata: project?.metadata,
|
|
531
567
|
root,
|
|
532
568
|
steps: Number(args.steps),
|
|
533
569
|
wall: Number(args.wall),
|
|
@@ -540,11 +576,11 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
540
576
|
}
|
|
541
577
|
},
|
|
542
578
|
onDone: (r) => {
|
|
543
|
-
const mark = r.
|
|
544
|
-
const status = r.error ? `
|
|
579
|
+
const mark = r.completed ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
580
|
+
const status = r.error ? `model_error: ${r.error.split("\n")[0]}` : r.status;
|
|
545
581
|
console.error(
|
|
546
582
|
` ${mark} ${String(r.model).padEnd(20)} ${String(r.reasoning).padEnd(8)} ` +
|
|
547
|
-
`${String(status).padEnd(16)} ${String(r.steps).padStart(3)}
|
|
583
|
+
`${String(status).padEnd(16)} ${String(r.steps).padStart(3)} turns · ` +
|
|
548
584
|
`${fmt(r.total_tokens)} tok · ${fmtUsd(r.cost_usd)}` +
|
|
549
585
|
`${r.cost_source === "estimated" ? "~" : ""}`
|
|
550
586
|
);
|
|
@@ -570,8 +606,8 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
570
606
|
// Tar + optional S3 push of the whole bench/ folder.
|
|
571
607
|
archiveAndPush(args, root, links);
|
|
572
608
|
|
|
573
|
-
const
|
|
574
|
-
console.error(`\n\x1b[1m[astra] matrix done · ${
|
|
609
|
+
const completed = rows.filter((r) => r.completed).length;
|
|
610
|
+
console.error(`\n\x1b[1m[astra] matrix done · ${completed}/${rows.length} completed\x1b[0m`);
|
|
575
611
|
|
|
576
612
|
// Print the important URLs / paths, aligned, at the very end.
|
|
577
613
|
const w = Math.max(...links.map(([k]) => k.length));
|
|
@@ -580,23 +616,23 @@ async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task,
|
|
|
580
616
|
console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
581
617
|
}
|
|
582
618
|
|
|
583
|
-
process.exit(
|
|
619
|
+
process.exit(completed === rows.length ? 0 : 1);
|
|
584
620
|
}
|
|
585
621
|
|
|
586
622
|
/** Print the per-(model,reasoning) leaderboard table. */
|
|
587
623
|
function printLeaderboard(board) {
|
|
588
|
-
board.sort((a, b) => b.
|
|
624
|
+
board.sort((a, b) => b.completion_rate - a.completion_rate || a.avg_steps - b.avg_steps);
|
|
589
625
|
console.error(
|
|
590
|
-
`\n\x1b[1m${"model".padEnd(20)} ${"reason".padEnd(8)} ${"
|
|
591
|
-
`${"
|
|
626
|
+
`\n\x1b[1m${"model".padEnd(20)} ${"reason".padEnd(8)} ${"complete".padEnd(8)} ` +
|
|
627
|
+
`${"turns".padStart(6)} ${"tokens".padStart(9)} ${"cost".padStart(9)} source\x1b[0m`
|
|
592
628
|
);
|
|
593
629
|
for (const g of board) {
|
|
594
|
-
const
|
|
595
|
-
const rate = `${Math.round(g.
|
|
630
|
+
const completed = `${g.completed}/${g.runs}`;
|
|
631
|
+
const rate = `${Math.round(g.completion_rate * 100)}%`;
|
|
596
632
|
const cost = g.cost_usd == null ? "n/a" : fmtUsd(g.cost_usd);
|
|
597
633
|
console.error(
|
|
598
634
|
`${String(g.model).padEnd(20)} ${String(g.reasoning).padEnd(8)} ` +
|
|
599
|
-
`${(
|
|
635
|
+
`${(completed + " " + rate).padEnd(8)} ${g.avg_steps.toFixed(1).padStart(6)} ` +
|
|
600
636
|
`${fmt(Math.round(g.avg_tokens)).padStart(9)} ${cost.padStart(9)} ${g.cost_source}`
|
|
601
637
|
);
|
|
602
638
|
}
|
|
@@ -633,23 +669,22 @@ function printSessions() {
|
|
|
633
669
|
|
|
634
670
|
function printEvent(msg) {
|
|
635
671
|
// Bench mode keeps output compact: skip the verbose system/user/assistant
|
|
636
|
-
// message bodies (per-
|
|
672
|
+
// message bodies (per-turn progress is shown by printStep). Only surface a
|
|
637
673
|
// non-empty terminal exit event so the run's submission is still visible.
|
|
638
674
|
if (msg.role !== "exit" || !msg.content?.trim()) return;
|
|
639
675
|
console.error(`\x1b[35m--- SUBMISSION ---\x1b[0m\n${msg.content}\n`);
|
|
640
676
|
}
|
|
641
677
|
|
|
642
|
-
/** Compact per-
|
|
678
|
+
/** Compact per-turn status line with exact token usage from the API. */
|
|
643
679
|
function printStep(s) {
|
|
644
|
-
const limit = s.stepLimit > 0 ? `/${s.stepLimit}` : "";
|
|
645
680
|
const cached = s.usage.cached_tokens ? ` (cached ${fmt(s.usage.cached_tokens)})` : "";
|
|
646
681
|
const cost = s.costUsd != null
|
|
647
|
-
? `
|
|
682
|
+
? ` │ ${fmtUsd(s.costUsd)}${s.costKind === "estimated" ? "~" : ""}`
|
|
648
683
|
: "";
|
|
649
684
|
console.error(
|
|
650
|
-
`\x1b[36m[astra]
|
|
651
|
-
`↑${fmt(s.usage.prompt_tokens)} ↓${fmt(s.usage.completion_tokens)} tok
|
|
652
|
-
`ctx ${fmt(s.contextTokens)}${cached}${cost}
|
|
685
|
+
`\x1b[36m[astra] turn ${s.step} │ ` +
|
|
686
|
+
`↑${fmt(s.usage.prompt_tokens)} ↓${fmt(s.usage.completion_tokens)} tok │ ` +
|
|
687
|
+
`ctx ${fmt(s.contextTokens)}${cached}${cost} │ ${s.elapsedSeconds.toFixed(1)}s\x1b[0m`
|
|
653
688
|
);
|
|
654
689
|
}
|
|
655
690
|
|
package/src/project.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import toml from "@iarna/toml";
|
|
5
|
+
|
|
6
|
+
export class ProjectConfigError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ProjectConfigError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BENCH = {
|
|
14
|
+
models: [],
|
|
15
|
+
reasoning: [""],
|
|
16
|
+
repeat: 1,
|
|
17
|
+
steps: 0,
|
|
18
|
+
wall_seconds: 0,
|
|
19
|
+
command_timeout_seconds: 60,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function requireObject(value, name) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new ProjectConfigError(`${name} must be a TOML table`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireString(value, name) {
|
|
30
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
31
|
+
throw new ProjectConfigError(`${name} must be a non-empty string`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function positiveInteger(value, name, fallback, { allowZero = false } = {}) {
|
|
37
|
+
if (value === undefined) return fallback;
|
|
38
|
+
if (!Number.isInteger(value) || value < (allowZero ? 0 : 1)) {
|
|
39
|
+
throw new ProjectConfigError(`${name} must be ${allowZero ? "a non-negative" : "a positive"} integer`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stringArray(value, name, fallback) {
|
|
45
|
+
if (value === undefined) return fallback;
|
|
46
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) {
|
|
47
|
+
throw new ProjectConfigError(`${name} must be an array of non-empty strings`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveInside(root, relative, name) {
|
|
53
|
+
const candidate = path.resolve(root, requireString(relative, name));
|
|
54
|
+
if (candidate !== root && !candidate.startsWith(root + path.sep)) {
|
|
55
|
+
throw new ProjectConfigError(`${name} must stay inside the project directory`);
|
|
56
|
+
}
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readRegularFile(file, name, { optional = false } = {}) {
|
|
61
|
+
if (!fs.existsSync(file)) {
|
|
62
|
+
if (optional) return null;
|
|
63
|
+
throw new ProjectConfigError(`${name} does not exist: ${file}`);
|
|
64
|
+
}
|
|
65
|
+
const stat = fs.lstatSync(file);
|
|
66
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
67
|
+
throw new ProjectConfigError(`${name} must be a regular file: ${file}`);
|
|
68
|
+
}
|
|
69
|
+
return fs.readFileSync(file, "utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sha256(value) {
|
|
73
|
+
return createHash("sha256").update(value).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extensionFiles(root, directories) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const extensions = [];
|
|
79
|
+
for (const directory of directories) {
|
|
80
|
+
const resolved = resolveInside(root, directory, "extensions.directories entry");
|
|
81
|
+
if (!fs.existsSync(resolved)) {
|
|
82
|
+
throw new ProjectConfigError(`extension directory does not exist: ${resolved}`);
|
|
83
|
+
}
|
|
84
|
+
const stat = fs.lstatSync(resolved);
|
|
85
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
86
|
+
throw new ProjectConfigError(`extension directory must be a regular directory: ${resolved}`);
|
|
87
|
+
}
|
|
88
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
89
|
+
if (!entry.isFile() || entry.isSymbolicLink()) continue;
|
|
90
|
+
const file = path.join(resolved, entry.name);
|
|
91
|
+
if (seen.has(file)) throw new ProjectConfigError(`duplicate extension path: ${file}`);
|
|
92
|
+
seen.add(file);
|
|
93
|
+
const content = readRegularFile(file, "extension");
|
|
94
|
+
extensions.push({ path: file, content, sha256: sha256(content) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return extensions;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function loadProject(projectPath) {
|
|
101
|
+
const root = path.resolve(requireString(projectPath, "project path"));
|
|
102
|
+
if (!fs.existsSync(root) || !fs.lstatSync(root).isDirectory()) {
|
|
103
|
+
throw new ProjectConfigError(`project directory does not exist: ${root}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const configPath = path.join(root, ".astra", "project.toml");
|
|
107
|
+
const configText = readRegularFile(configPath, "project configuration");
|
|
108
|
+
let config;
|
|
109
|
+
try {
|
|
110
|
+
config = toml.parse(configText);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new ProjectConfigError(`invalid project TOML: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const project = requireObject(config.project, "project");
|
|
116
|
+
const bench = requireObject(config.bench ?? {}, "bench");
|
|
117
|
+
const output = requireObject(config.output ?? {}, "output");
|
|
118
|
+
const templates = requireObject(config.templates ?? {}, "templates");
|
|
119
|
+
const extensionConfig = requireObject(config.extensions ?? {}, "extensions");
|
|
120
|
+
const provenance = requireObject(config.provenance ?? {}, "provenance");
|
|
121
|
+
const id = requireString(project.id, "project.id");
|
|
122
|
+
const version = positiveInteger(project.version, "project.version", undefined);
|
|
123
|
+
|
|
124
|
+
const instructionPath = resolveInside(root, project.instruction, "project.instruction");
|
|
125
|
+
const workspace = resolveInside(root, project.workspace, "project.workspace");
|
|
126
|
+
const instruction = readRegularFile(instructionPath, "instruction");
|
|
127
|
+
if (!instruction.trim()) throw new ProjectConfigError("instruction must not be empty");
|
|
128
|
+
if (!fs.existsSync(workspace) || !fs.lstatSync(workspace).isDirectory() || fs.lstatSync(workspace).isSymbolicLink()) {
|
|
129
|
+
throw new ProjectConfigError(`project.workspace must be a regular directory: ${workspace}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const templatePath = templates.bench === undefined ? null : resolveInside(root, templates.bench, "templates.bench");
|
|
133
|
+
const template = templatePath === null ? null : readRegularFile(templatePath, "bench template");
|
|
134
|
+
const extensions = extensionFiles(root, stringArray(extensionConfig.directories, "extensions.directories", []));
|
|
135
|
+
const taskText = [instruction, template, ...extensions.map((extension) => extension.content)]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join("\n\n");
|
|
138
|
+
|
|
139
|
+
const baselineSha256 = provenance.baseline_sha256;
|
|
140
|
+
if (baselineSha256 !== undefined && (typeof baselineSha256 !== "string" || !/^[a-f0-9]{64}$/.test(baselineSha256))) {
|
|
141
|
+
throw new ProjectConfigError("provenance.baseline_sha256 must be a lowercase SHA-256 hash");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
root,
|
|
146
|
+
id,
|
|
147
|
+
version,
|
|
148
|
+
instructionPath,
|
|
149
|
+
instruction,
|
|
150
|
+
instructionSha256: sha256(instruction),
|
|
151
|
+
workspace,
|
|
152
|
+
outputRoot: resolveInside(root, output.root ?? "bench", "output.root"),
|
|
153
|
+
template: template === null ? null : { path: templatePath, content: template, sha256: sha256(template) },
|
|
154
|
+
extensions,
|
|
155
|
+
taskText,
|
|
156
|
+
provenance: { baselineSha256: baselineSha256 ?? null },
|
|
157
|
+
metadata: {
|
|
158
|
+
id,
|
|
159
|
+
version,
|
|
160
|
+
instruction_sha256: sha256(instruction),
|
|
161
|
+
baseline_sha256: baselineSha256 ?? null,
|
|
162
|
+
template_sha256: template === null ? null : sha256(template),
|
|
163
|
+
extension_sha256: extensions.map((extension) => ({
|
|
164
|
+
path: path.relative(root, extension.path),
|
|
165
|
+
sha256: extension.sha256,
|
|
166
|
+
})),
|
|
167
|
+
},
|
|
168
|
+
bench: {
|
|
169
|
+
models: stringArray(bench.models, "bench.models", DEFAULT_BENCH.models),
|
|
170
|
+
reasoning: stringArray(bench.reasoning, "bench.reasoning", DEFAULT_BENCH.reasoning),
|
|
171
|
+
repeat: positiveInteger(bench.repeat, "bench.repeat", DEFAULT_BENCH.repeat),
|
|
172
|
+
steps: positiveInteger(bench.steps, "bench.steps", DEFAULT_BENCH.steps, { allowZero: true }),
|
|
173
|
+
wall: positiveInteger(bench.wall_seconds, "bench.wall_seconds", DEFAULT_BENCH.wall_seconds, { allowZero: true }),
|
|
174
|
+
timeout: positiveInteger(bench.command_timeout_seconds, "bench.command_timeout_seconds", DEFAULT_BENCH.command_timeout_seconds),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
package/src/report.html
CHANGED
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
var cells = tasks.map(function (t) {
|
|
478
478
|
var m = cell[slug + "\u0000" + t];
|
|
479
479
|
if (!m) return "<td>—</td>";
|
|
480
|
-
return '<td><span class="heat-cell" style="background:' + heatColor(m.
|
|
480
|
+
return '<td><span class="heat-cell" style="background:' + heatColor(m.completion_rate) + '">' + m.completed + "/" + m.k + "<br>" + pct(m.completion_rate) + "</span></td>";
|
|
481
481
|
}).join("");
|
|
482
482
|
return "<tr><td class=\"mono\">" + esc(slug) + "</td>" + cells + "</tr>";
|
|
483
483
|
}).join("") + "</tbody>";
|
|
@@ -500,15 +500,15 @@
|
|
|
500
500
|
}
|
|
501
501
|
uniq(function (r) { return r.slug; }).forEach(function (v) { fModel.appendChild(el("option", { value: v }, esc(v))); });
|
|
502
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.
|
|
503
|
+
uniq(function (r) { return r.status; }).forEach(function (v) { fOutcome.appendChild(el("option", { value: v }, esc(v))); });
|
|
504
504
|
|
|
505
505
|
var columns = [
|
|
506
506
|
{ key: "slug", label: "Model", render: function (r) { return '<span class="mono">' + esc(r.slug) + "</span>"; } },
|
|
507
507
|
{ key: "task_id", label: "Task", render: function (r) { return esc(r.task_id); } },
|
|
508
508
|
{ key: "run_id", label: "Run", render: function (r) { return esc(r.run_id); } },
|
|
509
|
-
{ key: "
|
|
510
|
-
var cls = r.
|
|
511
|
-
return '<span class="pill ' + cls + '">' + esc(r.
|
|
509
|
+
{ key: "status", label: "Status", render: function (r) {
|
|
510
|
+
var cls = r.completed ? "ok" : (r.status === "request_error" ? "bad" : "warn");
|
|
511
|
+
return '<span class="pill ' + cls + '">' + esc(r.status || "Unknown") + "</span>";
|
|
512
512
|
} },
|
|
513
513
|
{ key: "steps", label: "Steps", render: function (r) { return fmt(r.steps); } },
|
|
514
514
|
{ key: "elapsed_seconds", label: "Time", render: function (r) { return fmt(r.elapsed_seconds) + "s"; } },
|
|
@@ -541,7 +541,7 @@
|
|
|
541
541
|
var filtered = runs.filter(function (r) {
|
|
542
542
|
if (fModel.value && r.slug !== fModel.value) return false;
|
|
543
543
|
if (fTask.value && r.task_id !== fTask.value) return false;
|
|
544
|
-
if (fOutcome.value && r.
|
|
544
|
+
if (fOutcome.value && r.status !== fOutcome.value) return false;
|
|
545
545
|
return true;
|
|
546
546
|
});
|
|
547
547
|
sortableTable(container, columns, filtered, {
|
package/src/report.js
CHANGED
|
@@ -91,14 +91,14 @@ export function buildSummary(runs) {
|
|
|
91
91
|
const k = r.exit_status || "Unknown";
|
|
92
92
|
outcomes[k] = (outcomes[k] || 0) + 1;
|
|
93
93
|
}
|
|
94
|
-
const
|
|
94
|
+
const completed = rs.filter((r) => r.completed).length;
|
|
95
95
|
return {
|
|
96
96
|
model: rs[0].model,
|
|
97
97
|
reasoning: rs[0].reasoning,
|
|
98
98
|
slug,
|
|
99
99
|
runs: rs.length,
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
completed,
|
|
101
|
+
completion_rate: rs.length ? completed / rs.length : 0,
|
|
102
102
|
outcomes,
|
|
103
103
|
avg_steps: avg(rs, (r) => r.steps),
|
|
104
104
|
sum_steps: sum(rs, (r) => r.steps),
|
|
@@ -120,10 +120,10 @@ export function buildSummary(runs) {
|
|
|
120
120
|
avg_n_retries: avg(rs, (r) => r.n_retries),
|
|
121
121
|
};
|
|
122
122
|
});
|
|
123
|
-
leaderboard.sort((a, b) => b.
|
|
123
|
+
leaderboard.sort((a, b) => b.completion_rate - a.completion_rate || a.avg_steps - b.avg_steps);
|
|
124
124
|
|
|
125
125
|
const matrix = groupBy(runs, (r) => `${r.slug}\u0000${r.task_id}`).map(([, rs]) => {
|
|
126
|
-
const
|
|
126
|
+
const completed = rs.filter((r) => r.completed).length;
|
|
127
127
|
const costRuns = rs.filter((r) => r.cost_source);
|
|
128
128
|
return {
|
|
129
129
|
model: rs[0].model,
|
|
@@ -132,8 +132,8 @@ export function buildSummary(runs) {
|
|
|
132
132
|
task_id: rs[0].task_id,
|
|
133
133
|
task_title: rs[0].task_title,
|
|
134
134
|
k: rs.length,
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
completed,
|
|
136
|
+
completion_rate: rs.length ? completed / rs.length : 0,
|
|
137
137
|
avg_steps: avg(rs, (r) => r.steps),
|
|
138
138
|
avg_tokens: avg(rs, (r) => r.tokens.total),
|
|
139
139
|
avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
|
|
@@ -192,7 +192,7 @@ export function buildSummary(runs) {
|
|
|
192
192
|
.sort((a, b) => a.slug.localeCompare(b.slug) || a.step - b.step);
|
|
193
193
|
|
|
194
194
|
const costRuns = runs.filter((r) => r.cost_source);
|
|
195
|
-
const
|
|
195
|
+
const completed = runs.filter((r) => r.completed).length;
|
|
196
196
|
const sources = new Set(costRuns.map((r) => r.cost_source));
|
|
197
197
|
|
|
198
198
|
return {
|
|
@@ -202,8 +202,8 @@ export function buildSummary(runs) {
|
|
|
202
202
|
models: new Set(runs.map((r) => r.slug)).size,
|
|
203
203
|
tasks: new Set(runs.map((r) => r.task_id)).size,
|
|
204
204
|
runs: runs.length,
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
completed,
|
|
206
|
+
completion_rate: runs.length ? completed / runs.length : 0,
|
|
207
207
|
cost_usd: costRuns.length ? round(sum(costRuns, (r) => r.cost_usd), 6) : null,
|
|
208
208
|
cost_source: sources.size === 0 ? "unknown" : sources.size === 1 ? [...sources][0] : "mixed",
|
|
209
209
|
tokens: sum(runs, (r) => r.tokens.total),
|
|
@@ -234,6 +234,7 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
234
234
|
const trajPath = path.join(dir, "trajectory.json");
|
|
235
235
|
const metricsPath = path.join(dir, "metrics.csv");
|
|
236
236
|
const taskPath = path.join(dir, "task.md");
|
|
237
|
+
const projectPath = path.join(dir, "project.json");
|
|
237
238
|
if (!fs.existsSync(trajPath) && !fs.existsSync(metricsPath)) return null;
|
|
238
239
|
|
|
239
240
|
let traj = null;
|
|
@@ -245,6 +246,7 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
248
|
const metricsRow = fs.existsSync(metricsPath) ? parseCsv(fs.readFileSync(metricsPath, "utf8"))[0] : null;
|
|
249
|
+
const project = readJson(projectPath);
|
|
248
250
|
const taskMd = fs.existsSync(taskPath) ? fs.readFileSync(taskPath, "utf8") : traj?.info?.task || "";
|
|
249
251
|
const parsed = parseSlug(slug);
|
|
250
252
|
const model = metricsRow?.model || traj?.info?.model || parsed.model;
|
|
@@ -261,11 +263,11 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
261
263
|
last_context: num(metricsRow?.last_context_tokens ?? traj?.info?.tokens?.last_context),
|
|
262
264
|
};
|
|
263
265
|
const costSource = metricsRow?.cost_source || traj?.info?.cost?.source || "";
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
?
|
|
268
|
-
:
|
|
266
|
+
const legacyResolved = metricsRow?.resolved;
|
|
267
|
+
const status = metricsRow?.status ||
|
|
268
|
+
((legacyResolved == null ? traj?.info?.exit_status === "Submitted" : legacyResolved === true || legacyResolved === "1" || legacyResolved === 1)
|
|
269
|
+
? "completed"
|
|
270
|
+
: "model_error");
|
|
269
271
|
|
|
270
272
|
return {
|
|
271
273
|
id: `${slug}/${runId}`,
|
|
@@ -273,12 +275,16 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
273
275
|
model,
|
|
274
276
|
reasoning,
|
|
275
277
|
slug,
|
|
276
|
-
task_id: inferTaskId({ taskMd }),
|
|
278
|
+
task_id: typeof project?.id === "string" && project.id ? project.id : inferTaskId({ taskMd }),
|
|
277
279
|
task_title: inferTaskTitle(taskMd),
|
|
280
|
+
project: project && typeof project.id === "string"
|
|
281
|
+
? { id: project.id, version: project.version }
|
|
282
|
+
: null,
|
|
278
283
|
run: runNum,
|
|
279
284
|
run_id: runId,
|
|
280
285
|
exit_status: metricsRow?.exit_status || traj?.info?.exit_status || "",
|
|
281
|
-
|
|
286
|
+
status,
|
|
287
|
+
completed: status === "completed",
|
|
282
288
|
error: null,
|
|
283
289
|
steps: num(metricsRow?.steps ?? traj?.info?.n_steps),
|
|
284
290
|
n_calls: num(metricsRow?.n_calls ?? traj?.info?.n_calls),
|
|
@@ -301,6 +307,15 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
301
307
|
};
|
|
302
308
|
}
|
|
303
309
|
|
|
310
|
+
function readJson(file) {
|
|
311
|
+
if (!fs.existsSync(file)) return null;
|
|
312
|
+
try {
|
|
313
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
304
319
|
export function extractTimeline(doc) {
|
|
305
320
|
const messages = doc?.messages || [];
|
|
306
321
|
const steps = [];
|