@hackerrank/astra-cli 0.1.0 → 0.1.2
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 +61 -2
- package/package.json +1 -1
- package/src/agent.js +9 -0
- package/src/bench.js +478 -0
- package/src/cli.js +375 -33
- package/src/config.js +78 -0
- package/src/model.js +16 -1
- package/src/models.js +31 -0
- package/src/prompts.js +7 -3
- package/src/repl.js +281 -21
- package/src/report.html +569 -0
- package/src/report.js +459 -0
package/src/cli.js
CHANGED
|
@@ -3,19 +3,45 @@
|
|
|
3
3
|
* astra CLI — a minimal, zero-dependency AI coding agent for the
|
|
4
4
|
* HackerRank AI Gateway.
|
|
5
5
|
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* agent (default) interactive SWE assistant — a chat REPL. Toggle to bench
|
|
8
|
+
* mode at any time with Alt+Tab.
|
|
9
|
+
* bench benchmark runner — runs a task autonomously in an isolated
|
|
10
|
+
* workspace and records metrics. Entered automatically when
|
|
11
|
+
* a task (-t/-f) or task path (-p) is provided.
|
|
12
|
+
*
|
|
6
13
|
* Usage:
|
|
7
|
-
* astra
|
|
8
|
-
* astra -m <model>
|
|
14
|
+
* astra agent mode (uses cached / prompted model)
|
|
15
|
+
* astra -m <model> agent mode (interactive assistant)
|
|
16
|
+
* astra -m <model> -t "<task text>" bench run from task text
|
|
9
17
|
* astra -m <model> -f path/to/instruction.md
|
|
18
|
+
* astra -m <model> -p tasks/dummy-slugify bench run from a task directory
|
|
10
19
|
* astra --resume <session-id> resume a saved session
|
|
11
20
|
* astra --sessions list saved sessions
|
|
12
21
|
*
|
|
13
22
|
* Options:
|
|
14
|
-
* -m, --model <id> Model id on the gateway (
|
|
15
|
-
*
|
|
16
|
-
*
|
|
23
|
+
* -m, --model <id> Model id on the gateway (bench mode: required;
|
|
24
|
+
* comma-separated for a multi-model matrix, e.g.
|
|
25
|
+
* m1,m2,m3; agent mode: falls back to cached prefs,
|
|
26
|
+
* then a first-run picker, then glm-5.2)
|
|
27
|
+
* -r, --reasoning <lvl> Reasoning effort (e.g. off|low|medium|high;
|
|
28
|
+
* comma-separated to sweep levels in bench mode).
|
|
29
|
+
* Sent to the model and recorded in the bench name.
|
|
30
|
+
* --repeat <n> Attempts per (model,reasoning) in a matrix (1)
|
|
31
|
+
* --push After a matrix, tar bench/ and upload to
|
|
32
|
+
* s3://astra-bench-results/<timestamp>/
|
|
33
|
+
* --push-uri <s3-uri> Override the S3 push destination
|
|
34
|
+
* --tar After a matrix, write a bench/ tarball locally
|
|
35
|
+
* --report Rebuild bench/summary.json + bench/report.html
|
|
36
|
+
* from whatever runs already exist on disk, then
|
|
37
|
+
* exit (no model/API key needed).
|
|
38
|
+
* -t, --task <text> Task text -> bench mode (run to completion)
|
|
39
|
+
* -f, --task-file <path> Read task text from a file -> bench mode
|
|
40
|
+
* -p, --path <dir> Task directory (copied into an isolated bench
|
|
41
|
+
* workspace) -> bench mode
|
|
42
|
+
* --bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
17
43
|
* -C, --cwd <path> Working directory for commands (default: cwd)
|
|
18
|
-
* -o, --output <path> Also write trajectory JSON here (
|
|
44
|
+
* -o, --output <path> Also write trajectory JSON here (bench mode)
|
|
19
45
|
* -s, --steps <n> Step limit (default: 40)
|
|
20
46
|
* -w, --wall <seconds> Wall-clock limit (default: 0 = none)
|
|
21
47
|
* --timeout <seconds> Per-command timeout (default: 60)
|
|
@@ -25,8 +51,8 @@
|
|
|
25
51
|
* --resume <id> Resume a saved session by id
|
|
26
52
|
* --sessions List saved sessions and exit
|
|
27
53
|
* -y, --yolo Auto-run commands without confirmation
|
|
28
|
-
* (always on in
|
|
29
|
-
* -q, --quiet Do not stream steps (
|
|
54
|
+
* (always on in bench mode)
|
|
55
|
+
* -q, --quiet Do not stream steps (bench mode)
|
|
30
56
|
* -h, --help Show this help
|
|
31
57
|
*
|
|
32
58
|
* API key resolution (first hit wins):
|
|
@@ -34,6 +60,13 @@
|
|
|
34
60
|
* 2. ASTRA_GATEWAY_API_KEY env var
|
|
35
61
|
* 3. ~/.astra/config.json (dedicated astra config)
|
|
36
62
|
* 4. interactive prompt (when run in a terminal); offers to save to (3)
|
|
63
|
+
*
|
|
64
|
+
* Agent model resolution (agent mode, first hit wins):
|
|
65
|
+
* 1. --model / --reasoning flags
|
|
66
|
+
* 2. resumed session
|
|
67
|
+
* 3. cached agent prefs in ~/.astra/config.json (last model used)
|
|
68
|
+
* 4. first-run interactive setup (list pickers for model + reasoning)
|
|
69
|
+
* 5. fallback: glm-5.2 with medium reasoning
|
|
37
70
|
*/
|
|
38
71
|
|
|
39
72
|
import fs from "node:fs";
|
|
@@ -41,7 +74,8 @@ import path from "node:path";
|
|
|
41
74
|
import { GatewayModel } from "./model.js";
|
|
42
75
|
import { LocalEnvironment } from "./environment.js";
|
|
43
76
|
import { Agent } from "./agent.js";
|
|
44
|
-
import { resolveCredentials, promptForCredentials, canPrompt } from "./config.js";
|
|
77
|
+
import { resolveCredentials, promptForCredentials, canPrompt, readAgentPrefs, saveAgentPrefs, promptForAgentPrefs } from "./config.js";
|
|
78
|
+
import { AVAILABLE_MODELS, REASONING_LEVELS, DEFAULT_MODEL, DEFAULT_REASONING } from "./models.js";
|
|
45
79
|
import {
|
|
46
80
|
newSessionId,
|
|
47
81
|
saveSession,
|
|
@@ -51,6 +85,27 @@ import {
|
|
|
51
85
|
deriveTitle,
|
|
52
86
|
} from "./session.js";
|
|
53
87
|
import { runRepl } from "./repl.js";
|
|
88
|
+
import {
|
|
89
|
+
allocateRun,
|
|
90
|
+
seedWorkspace,
|
|
91
|
+
collectMetrics,
|
|
92
|
+
writeMetrics,
|
|
93
|
+
runMatrix,
|
|
94
|
+
aggregate,
|
|
95
|
+
packBench,
|
|
96
|
+
pushToS3,
|
|
97
|
+
defaultPushUri,
|
|
98
|
+
benchRoot,
|
|
99
|
+
} from "./bench.js";
|
|
100
|
+
import { refreshReport } from "./report.js";
|
|
101
|
+
|
|
102
|
+
/** Map a reasoning level to gateway modelKwargs. Empty for off/none/unset. */
|
|
103
|
+
function reasoningKwargs(level) {
|
|
104
|
+
if (!level) return {};
|
|
105
|
+
const l = String(level).toLowerCase();
|
|
106
|
+
if (l === "off" || l === "none" || l === "disabled") return {};
|
|
107
|
+
return { reasoning_effort: l };
|
|
108
|
+
}
|
|
54
109
|
|
|
55
110
|
function parseArgs(argv) {
|
|
56
111
|
const args = { steps: 40, wall: 0, timeout: 60, quiet: false, "max-output": 16000 };
|
|
@@ -58,6 +113,14 @@ function parseArgs(argv) {
|
|
|
58
113
|
"-m": "model", "--model": "model",
|
|
59
114
|
"-t": "task", "--task": "task",
|
|
60
115
|
"-f": "task-file", "--task-file": "task-file",
|
|
116
|
+
"-p": "path", "--path": "path",
|
|
117
|
+
"-r": "reasoning", "--reasoning": "reasoning",
|
|
118
|
+
"--repeat": "repeat",
|
|
119
|
+
"--push": "push",
|
|
120
|
+
"--push-uri": "push-uri",
|
|
121
|
+
"--tar": "tar",
|
|
122
|
+
"--report": "report",
|
|
123
|
+
"--bench-root": "bench-root",
|
|
61
124
|
"-C": "cwd", "--cwd": "cwd",
|
|
62
125
|
"-o": "output", "--output": "output",
|
|
63
126
|
"-s": "steps", "--steps": "steps",
|
|
@@ -72,7 +135,7 @@ function parseArgs(argv) {
|
|
|
72
135
|
"-y": "yolo", "--yolo": "yolo",
|
|
73
136
|
"-h": "help", "--help": "help",
|
|
74
137
|
};
|
|
75
|
-
const flags = new Set(["quiet", "yolo", "help", "sessions"]);
|
|
138
|
+
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push", "report"]);
|
|
76
139
|
for (let i = 2; i < argv.length; i++) {
|
|
77
140
|
const key = alias[argv[i]];
|
|
78
141
|
if (!key) { console.error(`Unknown option: ${argv[i]}`); process.exit(2); }
|
|
@@ -96,6 +159,18 @@ async function main() {
|
|
|
96
159
|
|
|
97
160
|
if (args.sessions) { printSessions(); process.exit(0); }
|
|
98
161
|
if (args.help) { console.log(HELP); process.exit(0); }
|
|
162
|
+
if (args.report) {
|
|
163
|
+
try {
|
|
164
|
+
const res = refreshReport(args["bench-root"]);
|
|
165
|
+
console.error(`\x1b[1m[astra] report · ${res.runs} run(s) · ${res.models} model(s) · ${res.tasks} task(s)\x1b[0m`);
|
|
166
|
+
console.error(`\x1b[1m summary\x1b[0m ${res.summary}`);
|
|
167
|
+
console.error(`\x1b[1m html \x1b[0m ${res.html}`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`\x1b[31m[astra] report failed: ${err.message}\x1b[0m`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
99
174
|
|
|
100
175
|
// Load a session to resume (if any) to infer defaults.
|
|
101
176
|
let resumeDoc = null;
|
|
@@ -107,17 +182,12 @@ async function main() {
|
|
|
107
182
|
resumeDoc = loadSession(args.resume);
|
|
108
183
|
}
|
|
109
184
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.error("\x1b[31m[astra] --model is required.\x1b[0m\n");
|
|
113
|
-
console.log(HELP);
|
|
114
|
-
process.exit(2);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Task text (flag/file) selects autonomous mode; otherwise interactive.
|
|
185
|
+
// A task (-t/-f) or task path (-p) selects bench mode; otherwise agent mode.
|
|
186
|
+
// Bench mode uses the autonomous engine; agent mode uses the interactive one.
|
|
118
187
|
let task = args.task;
|
|
119
188
|
if (args["task-file"]) task = fs.readFileSync(args["task-file"], "utf8");
|
|
120
|
-
const
|
|
189
|
+
const wantsBench = !!(task || args.path);
|
|
190
|
+
const mode = wantsBench
|
|
121
191
|
? "autonomous"
|
|
122
192
|
: resumeDoc?.info?.mode === "autonomous"
|
|
123
193
|
? "autonomous"
|
|
@@ -125,12 +195,13 @@ async function main() {
|
|
|
125
195
|
|
|
126
196
|
if (mode === "interactive" && !canPrompt()) {
|
|
127
197
|
console.error(
|
|
128
|
-
"\x1b[31m[astra]
|
|
129
|
-
"to run
|
|
198
|
+
"\x1b[31m[astra] agent mode needs a terminal. Provide a task with -t/-f/-p\n" +
|
|
199
|
+
"to run a bench, or run in a TTY.\x1b[0m"
|
|
130
200
|
);
|
|
131
201
|
process.exit(2);
|
|
132
202
|
}
|
|
133
203
|
|
|
204
|
+
// -------------------- API KEY RESOLUTION --------------------
|
|
134
205
|
let apiKey = resolveApiKey(args["api-key"], args["base-url"]);
|
|
135
206
|
if (!apiKey) {
|
|
136
207
|
if (canPrompt()) {
|
|
@@ -149,6 +220,69 @@ async function main() {
|
|
|
149
220
|
}
|
|
150
221
|
}
|
|
151
222
|
|
|
223
|
+
// -------------------- MODEL + REASONING RESOLUTION --------------------
|
|
224
|
+
// Bench (autonomous) mode: an explicit --model is required so runs are
|
|
225
|
+
// reproducible and never silently pick a cached/default model.
|
|
226
|
+
//
|
|
227
|
+
// Agent (interactive) mode: resolve in priority order
|
|
228
|
+
// 1. --model flag / --reasoning flag
|
|
229
|
+
// 2. resumed session
|
|
230
|
+
// 3. cached agent prefs in ~/.astra/config.json ("remember last model")
|
|
231
|
+
// 4. first-time interactive setup (list pickers for model + reasoning)
|
|
232
|
+
// 5. hard fallback: glm-5.2 with medium reasoning
|
|
233
|
+
// The chosen values are cached so the next `astra` needs no flags.
|
|
234
|
+
let modelId = args.model || resumeDoc?.info?.model;
|
|
235
|
+
let reasoning = args.reasoning || "";
|
|
236
|
+
|
|
237
|
+
if (mode === "autonomous") {
|
|
238
|
+
if (!modelId) {
|
|
239
|
+
console.error("\x1b[31m[astra] --model is required for bench mode.\x1b[0m\n");
|
|
240
|
+
console.log(HELP);
|
|
241
|
+
process.exit(2);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Multi-model / multi-reasoning / repeated matrix. Triggered by a
|
|
245
|
+
// comma-separated model or reasoning list, or --repeat > 1. Runs each cell
|
|
246
|
+
// sequentially in its own isolated workspace, then prints a leaderboard and
|
|
247
|
+
// optionally tars + uploads the whole bench/ folder.
|
|
248
|
+
const models = String(modelId).split(",").map((s) => s.trim()).filter(Boolean);
|
|
249
|
+
const reasonings = reasoning
|
|
250
|
+
? String(reasoning).split(",").map((s) => s.trim()).filter(Boolean)
|
|
251
|
+
: [""];
|
|
252
|
+
const repeat = Math.max(1, Number(args.repeat) || 1);
|
|
253
|
+
const isMatrix = models.length > 1 || reasonings.length > 1 || repeat > 1;
|
|
254
|
+
|
|
255
|
+
if (isMatrix && !resumeDoc) {
|
|
256
|
+
await runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet: !!args.quiet });
|
|
257
|
+
return; // runBenchMatrix exits the process
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const cached = readAgentPrefs();
|
|
261
|
+
if (!modelId) modelId = cached.model;
|
|
262
|
+
if (!reasoning) reasoning = cached.reasoning;
|
|
263
|
+
|
|
264
|
+
if (!modelId) {
|
|
265
|
+
// No flag, no resume, no cache — run first-time setup (list pickers).
|
|
266
|
+
if (canPrompt()) {
|
|
267
|
+
const prefs = await promptForAgentPrefs({
|
|
268
|
+
defaultModel: DEFAULT_MODEL,
|
|
269
|
+
defaultReasoning: DEFAULT_REASONING,
|
|
270
|
+
});
|
|
271
|
+
modelId = prefs.model;
|
|
272
|
+
reasoning = reasoning || prefs.reasoning;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Hard fallback if we still have nothing.
|
|
277
|
+
if (!modelId) {
|
|
278
|
+
modelId = DEFAULT_MODEL;
|
|
279
|
+
reasoning = reasoning || DEFAULT_REASONING;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Remember the last model/reasoning used in agent mode.
|
|
283
|
+
saveAgentPrefs({ model: modelId, reasoning: reasoning || DEFAULT_REASONING });
|
|
284
|
+
}
|
|
285
|
+
|
|
152
286
|
const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
|
|
153
287
|
const quiet = !!args.quiet;
|
|
154
288
|
|
|
@@ -156,10 +290,30 @@ async function main() {
|
|
|
156
290
|
model: modelId,
|
|
157
291
|
baseUrl: args["base-url"],
|
|
158
292
|
apiKey,
|
|
293
|
+
modelKwargs: reasoningKwargs(reasoning),
|
|
159
294
|
onRetry: quiet ? () => {} : (r) => printRetry(r),
|
|
160
295
|
});
|
|
296
|
+
|
|
297
|
+
// -------------------- BENCH SETUP --------------------
|
|
298
|
+
// A fresh bench run (task text or task path, not a resume) gets an isolated
|
|
299
|
+
// workspace under bench/<model-name-reasoning>/run-NN/. The agent's commands
|
|
300
|
+
// run inside that workspace and metrics are recorded when it finishes.
|
|
301
|
+
let benchRun = null;
|
|
302
|
+
if (mode === "autonomous" && !resumeDoc) {
|
|
303
|
+
benchRun = allocateRun({ model: modelId, reasoning, root: args["bench-root"] });
|
|
304
|
+
const seeded = seedWorkspace(benchRun.workspace, {
|
|
305
|
+
taskPath: args.path,
|
|
306
|
+
taskFile: args["task-file"],
|
|
307
|
+
taskText: task,
|
|
308
|
+
});
|
|
309
|
+
task = seeded.taskText || task || "";
|
|
310
|
+
// Persist the task text alongside the run for reproducibility.
|
|
311
|
+
fs.writeFileSync(path.join(benchRun.dir, "task.md"), task || "");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const cmdCwd = benchRun ? benchRun.workspace : cwd;
|
|
161
315
|
const env = new LocalEnvironment({
|
|
162
|
-
cwd,
|
|
316
|
+
cwd: cmdCwd,
|
|
163
317
|
timeout: Number(args.timeout),
|
|
164
318
|
maxOutputChars: Number(args["max-output"]),
|
|
165
319
|
});
|
|
@@ -169,7 +323,11 @@ async function main() {
|
|
|
169
323
|
mode,
|
|
170
324
|
stepLimit: Number(args.steps),
|
|
171
325
|
wallTimeLimitSeconds: Number(args.wall),
|
|
172
|
-
outputPath:
|
|
326
|
+
outputPath: benchRun
|
|
327
|
+
? path.join(benchRun.dir, "trajectory.json")
|
|
328
|
+
: args.output
|
|
329
|
+
? path.resolve(args.output)
|
|
330
|
+
: null,
|
|
173
331
|
sessionId,
|
|
174
332
|
saveSession,
|
|
175
333
|
onEvent: quiet || mode === "interactive" ? () => {} : (msg) => printEvent(msg),
|
|
@@ -184,26 +342,73 @@ async function main() {
|
|
|
184
342
|
// -------------------- INTERACTIVE MODE --------------------
|
|
185
343
|
if (mode === "interactive") {
|
|
186
344
|
if (!resumeDoc) agent.start();
|
|
187
|
-
agent.title = agent.title || "
|
|
188
|
-
|
|
345
|
+
agent.title = agent.title || "agent";
|
|
346
|
+
// Bench runner for the in-REPL Alt+Tab toggle: runs a task autonomously in
|
|
347
|
+
// an isolated workspace under bench/<model-name-reasoning>/run-NN/.
|
|
348
|
+
const runBench = async (taskText, log) => {
|
|
349
|
+
const alloc = allocateRun({ model: modelId, reasoning, root: args["bench-root"] });
|
|
350
|
+
seedWorkspace(alloc.workspace, { taskText });
|
|
351
|
+
fs.writeFileSync(path.join(alloc.dir, "task.md"), taskText || "");
|
|
352
|
+
log(`[astra] bench ${alloc.slug}/${alloc.runId} → ${alloc.workspace}`);
|
|
353
|
+
const benchEnv = new LocalEnvironment({
|
|
354
|
+
cwd: alloc.workspace,
|
|
355
|
+
timeout: Number(args.timeout),
|
|
356
|
+
maxOutputChars: Number(args["max-output"]),
|
|
357
|
+
});
|
|
358
|
+
const benchAgent = new Agent(model, benchEnv, {
|
|
359
|
+
mode: "autonomous",
|
|
360
|
+
stepLimit: Number(args.steps),
|
|
361
|
+
wallTimeLimitSeconds: Number(args.wall),
|
|
362
|
+
outputPath: path.join(alloc.dir, "trajectory.json"),
|
|
363
|
+
});
|
|
364
|
+
const res = await benchAgent.run(taskText);
|
|
365
|
+
const metrics = collectMetrics({ agent: benchAgent, model, reasoning });
|
|
366
|
+
const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
|
|
367
|
+
log(`[astra] bench done: exit=${res.exit_status} · metrics → ${paths.runMetrics}`);
|
|
368
|
+
};
|
|
369
|
+
await runRepl(agent, { model, autoRun: !!args.yolo, fresh: !resumeDoc, reasoning, runBench });
|
|
189
370
|
process.exit(0);
|
|
190
371
|
}
|
|
191
372
|
|
|
192
|
-
// --------------------
|
|
373
|
+
// -------------------- BENCH MODE (autonomous engine) --------------------
|
|
193
374
|
if (!task && !resumeDoc) {
|
|
194
|
-
console.error("\x1b[31m[astra] provide a task with -t or -
|
|
375
|
+
console.error("\x1b[31m[astra] provide a task with -t, -f, or -p.\x1b[0m");
|
|
195
376
|
process.exit(2);
|
|
196
377
|
}
|
|
197
378
|
if (task) agent.title = deriveTitle({ info: { task }, messages: [] });
|
|
198
379
|
|
|
199
380
|
if (!quiet) {
|
|
200
|
-
|
|
381
|
+
if (benchRun) {
|
|
382
|
+
console.error(
|
|
383
|
+
`\x1b[2m[astra] bench · ${benchRun.slug}/${benchRun.runId} ` +
|
|
384
|
+
`· model=${modelId}${reasoning ? ` reasoning=${reasoning}` : ""} steps<=${args.steps}\x1b[0m`
|
|
385
|
+
);
|
|
386
|
+
console.error(`\x1b[2m[astra] workspace -> ${benchRun.workspace}\x1b[0m`);
|
|
387
|
+
} else {
|
|
388
|
+
console.error(`\x1b[2m[astra] bench · model=${modelId} cwd=${cmdCwd} steps<=${args.steps}\x1b[0m`);
|
|
389
|
+
}
|
|
201
390
|
}
|
|
202
391
|
|
|
203
392
|
const result =
|
|
204
393
|
resumeDoc && !task ? await continueAutonomous(agent) : await agent.run(task);
|
|
205
394
|
agent.save();
|
|
206
395
|
|
|
396
|
+
// Record benchmark metrics (per-run CSV + rolled-up index).
|
|
397
|
+
if (benchRun) {
|
|
398
|
+
const metrics = collectMetrics({ agent, model, reasoning });
|
|
399
|
+
const paths = writeMetrics({ runDir: benchRun.dir, root: benchRun.root, metrics });
|
|
400
|
+
if (!quiet) {
|
|
401
|
+
console.error(`\x1b[2m[astra] metrics -> ${paths.runMetrics}\x1b[0m`);
|
|
402
|
+
console.error(`\x1b[2m[astra] index -> ${paths.index}\x1b[0m`);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const report = refreshReport(benchRun.root);
|
|
406
|
+
if (!quiet) console.error(`\x1b[2m[astra] report -> ${report.html}\x1b[0m`);
|
|
407
|
+
} catch {
|
|
408
|
+
// Non-fatal: report regeneration is best-effort for single runs.
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
207
412
|
if (!quiet) {
|
|
208
413
|
const pt = model.totalPromptTokens;
|
|
209
414
|
const ct = model.totalCompletionTokens;
|
|
@@ -226,9 +431,146 @@ async function main() {
|
|
|
226
431
|
console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
|
|
227
432
|
}
|
|
228
433
|
if (result.submission) console.log(result.submission);
|
|
434
|
+
|
|
435
|
+
// For a single-run bench, tar + push the bench/ folder if requested.
|
|
436
|
+
// (Matrix runs handle this inside runBenchMatrix.)
|
|
437
|
+
if (benchRun) {
|
|
438
|
+
const links = [];
|
|
439
|
+
archiveAndPush(args, benchRun.root, links);
|
|
440
|
+
if (links.length) {
|
|
441
|
+
const w = Math.max(...links.map(([k]) => k.length));
|
|
442
|
+
console.error("");
|
|
443
|
+
for (const [k, v] of links) console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
229
447
|
process.exit(result.exit_status === "Submitted" ? 0 : 1);
|
|
230
448
|
}
|
|
231
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Tar the bench/ folder and (optionally) upload it to S3. Mutates `links`
|
|
452
|
+
* with the resulting artifact paths/URLs and prints any failures.
|
|
453
|
+
* Works for both single-run and matrix bench runs.
|
|
454
|
+
*/
|
|
455
|
+
function archiveAndPush(args, root, links) {
|
|
456
|
+
if (!(args.tar || args.push)) return;
|
|
457
|
+
try {
|
|
458
|
+
const tarball = packBench({ root });
|
|
459
|
+
links.push(["tarball", tarball]);
|
|
460
|
+
if (args.push) {
|
|
461
|
+
// Bare --push uses the default bucket + timestamp prefix; --push-uri
|
|
462
|
+
// overrides the destination.
|
|
463
|
+
const dest = args["push-uri"] || defaultPushUri();
|
|
464
|
+
try {
|
|
465
|
+
const pushed = pushToS3(tarball, dest);
|
|
466
|
+
links.push(["s3 uri", pushed.uri]);
|
|
467
|
+
links.push(["s3 url", pushed.url]);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
|
|
470
|
+
console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
474
|
+
console.error(`\x1b[31m[astra] tar failed: ${err.message}\x1b[0m`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Run a multi-model / multi-reasoning / repeated bench matrix, print a
|
|
480
|
+
* leaderboard, optionally tar + upload the bench/ folder, then exit.
|
|
481
|
+
*/
|
|
482
|
+
async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet }) {
|
|
483
|
+
const root = args["bench-root"];
|
|
484
|
+
const total = models.length * reasonings.length * repeat;
|
|
485
|
+
console.error(
|
|
486
|
+
`\x1b[1m[astra] bench matrix · ${models.length} model(s) × ` +
|
|
487
|
+
`${reasonings.length} reasoning × ${repeat} repeat = ${total} run(s)\x1b[0m`
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
let n = 0;
|
|
491
|
+
const rows = await runMatrix({
|
|
492
|
+
models,
|
|
493
|
+
reasonings,
|
|
494
|
+
repeat,
|
|
495
|
+
apiKey,
|
|
496
|
+
baseUrl: args["base-url"],
|
|
497
|
+
task,
|
|
498
|
+
taskPath: args.path,
|
|
499
|
+
taskFile: args["task-file"],
|
|
500
|
+
root,
|
|
501
|
+
steps: Number(args.steps),
|
|
502
|
+
wall: Number(args.wall),
|
|
503
|
+
timeout: Number(args.timeout),
|
|
504
|
+
maxOutputChars: Number(args["max-output"]),
|
|
505
|
+
onStart: (c) => {
|
|
506
|
+
n++;
|
|
507
|
+
if (!quiet) {
|
|
508
|
+
console.error(`\x1b[36m[astra] (${n}/${total}) ${c.slug}/${c.runId} …\x1b[0m`);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
onDone: (r) => {
|
|
512
|
+
const mark = r.resolved ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
513
|
+
const status = r.error ? `Error: ${r.error.split("\n")[0]}` : r.exit_status;
|
|
514
|
+
console.error(
|
|
515
|
+
` ${mark} ${String(r.model).padEnd(20)} ${String(r.reasoning).padEnd(8)} ` +
|
|
516
|
+
`${String(status).padEnd(16)} ${String(r.steps).padStart(3)} steps · ` +
|
|
517
|
+
`${fmt(r.total_tokens)} tok · ${fmtUsd(r.cost_usd)}` +
|
|
518
|
+
`${r.cost_source === "estimated" ? "~" : ""}`
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
printLeaderboard(aggregate(rows));
|
|
524
|
+
|
|
525
|
+
const indexPath = path.join(benchRoot(root), "metrics.csv");
|
|
526
|
+
|
|
527
|
+
// Collect the important artifact locations to print together at the end.
|
|
528
|
+
const links = [["metrics", indexPath]];
|
|
529
|
+
|
|
530
|
+
// Rebuild the dashboard (summary.json + report.html) from every run on
|
|
531
|
+
// disk under this root, including runs from earlier matrix invocations.
|
|
532
|
+
try {
|
|
533
|
+
const report = refreshReport(root);
|
|
534
|
+
links.push(["report", report.html]);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.error(`\x1b[33m[astra] report generation skipped: ${err.message}\x1b[0m`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Tar + optional S3 push of the whole bench/ folder.
|
|
540
|
+
archiveAndPush(args, root, links);
|
|
541
|
+
|
|
542
|
+
const solved = rows.filter((r) => r.resolved).length;
|
|
543
|
+
console.error(`\n\x1b[1m[astra] matrix done · ${solved}/${rows.length} resolved\x1b[0m`);
|
|
544
|
+
|
|
545
|
+
// Print the important URLs / paths, aligned, at the very end.
|
|
546
|
+
const w = Math.max(...links.map(([k]) => k.length));
|
|
547
|
+
console.error("");
|
|
548
|
+
for (const [k, v] of links) {
|
|
549
|
+
console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
process.exit(solved === rows.length ? 0 : 1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Print the per-(model,reasoning) leaderboard table. */
|
|
556
|
+
function printLeaderboard(board) {
|
|
557
|
+
board.sort((a, b) => b.solved_rate - a.solved_rate || a.avg_steps - b.avg_steps);
|
|
558
|
+
console.error(
|
|
559
|
+
`\n\x1b[1m${"model".padEnd(20)} ${"reason".padEnd(8)} ${"solved".padEnd(8)} ` +
|
|
560
|
+
`${"steps".padStart(6)} ${"tokens".padStart(9)} ${"cost".padStart(9)} source\x1b[0m`
|
|
561
|
+
);
|
|
562
|
+
for (const g of board) {
|
|
563
|
+
const solved = `${g.solved}/${g.runs}`;
|
|
564
|
+
const rate = `${Math.round(g.solved_rate * 100)}%`;
|
|
565
|
+
const cost = g.cost_usd == null ? "n/a" : fmtUsd(g.cost_usd);
|
|
566
|
+
console.error(
|
|
567
|
+
`${String(g.model).padEnd(20)} ${String(g.reasoning).padEnd(8)} ` +
|
|
568
|
+
`${(solved + " " + rate).padEnd(8)} ${g.avg_steps.toFixed(1).padStart(6)} ` +
|
|
569
|
+
`${fmt(Math.round(g.avg_tokens)).padStart(9)} ${cost.padStart(9)} ${g.cost_source}`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
232
574
|
/** Drive an already-seeded autonomous agent to completion (used on resume). */
|
|
233
575
|
async function continueAutonomous(agent) {
|
|
234
576
|
while (true) {
|
|
@@ -259,11 +601,11 @@ function printSessions() {
|
|
|
259
601
|
}
|
|
260
602
|
|
|
261
603
|
function printEvent(msg) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.error(`\x1b[
|
|
604
|
+
// Bench mode keeps output compact: skip the verbose system/user/assistant
|
|
605
|
+
// message bodies (per-step progress is shown by printStep). Only surface a
|
|
606
|
+
// non-empty terminal exit event so the run's submission is still visible.
|
|
607
|
+
if (msg.role !== "exit" || !msg.content?.trim()) return;
|
|
608
|
+
console.error(`\x1b[35m--- SUBMISSION ---\x1b[0m\n${msg.content}\n`);
|
|
267
609
|
}
|
|
268
610
|
|
|
269
611
|
/** Compact per-step status line with exact token usage from the API. */
|
package/src/config.js
CHANGED
|
@@ -17,6 +17,12 @@ import fs from "node:fs";
|
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import readline from "node:readline";
|
|
20
|
+
import {
|
|
21
|
+
AVAILABLE_MODELS,
|
|
22
|
+
REASONING_LEVELS,
|
|
23
|
+
DEFAULT_MODEL,
|
|
24
|
+
DEFAULT_REASONING,
|
|
25
|
+
} from "./models.js";
|
|
20
26
|
|
|
21
27
|
export const DEFAULT_BASE_URL = "https://gateway-central.ai.private.hackerrank.link/v1";
|
|
22
28
|
|
|
@@ -54,6 +60,32 @@ export function saveGatewayCredentials({ apiKey, baseUrl }) {
|
|
|
54
60
|
return configPath();
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Save the last / preferred agent model + reasoning into the dedicated config
|
|
65
|
+
* (merging existing). Used so agent mode remembers the last model — users can
|
|
66
|
+
* then just run `astra` without `-m`.
|
|
67
|
+
*/
|
|
68
|
+
export function saveAgentPrefs({ model, reasoning } = {}) {
|
|
69
|
+
const cfg = readConfig();
|
|
70
|
+
cfg.agent = { ...(cfg.agent || {}) };
|
|
71
|
+
if (model) cfg.agent.model = model;
|
|
72
|
+
if (reasoning != null) cfg.agent.reasoning = reasoning;
|
|
73
|
+
writeConfig(cfg);
|
|
74
|
+
return configPath();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the cached / preferred agent model + reasoning from the dedicated
|
|
79
|
+
* config. Returns { model, reasoning } (either may be empty when unset).
|
|
80
|
+
*/
|
|
81
|
+
export function readAgentPrefs() {
|
|
82
|
+
const cfg = readConfig();
|
|
83
|
+
return {
|
|
84
|
+
model: cfg.agent?.model || "",
|
|
85
|
+
reasoning: cfg.agent?.reasoning || "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
/**
|
|
58
90
|
* Resolve gateway credentials in priority order (no prompting):
|
|
59
91
|
* 1. explicit args (CLI flags)
|
|
@@ -133,3 +165,49 @@ export async function promptForCredentials({ baseUrl } = {}) {
|
|
|
133
165
|
}
|
|
134
166
|
return { apiKey, baseUrl: effectiveBase };
|
|
135
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Present a numbered list on the TTY and return the chosen item. Re-prompts on
|
|
171
|
+
* invalid input; returns the default (index 0 or `defaultValue`) on blank.
|
|
172
|
+
* @param {string} label
|
|
173
|
+
* @param {string[]} options
|
|
174
|
+
* @param {object} [opts]
|
|
175
|
+
* @param {string} [opts.defaultValue] pre-selected value (defaults to options[0])
|
|
176
|
+
*/
|
|
177
|
+
export async function selectFromList(label, options, { defaultValue } = {}) {
|
|
178
|
+
if (!options || options.length === 0) return defaultValue || "";
|
|
179
|
+
const def = defaultValue && options.includes(defaultValue) ? defaultValue : options[0];
|
|
180
|
+
console.error(`\x1b[1m${label}\x1b[0m`);
|
|
181
|
+
for (let i = 0; i < options.length; i++) {
|
|
182
|
+
const marker = options[i] === def ? "\x1b[36m•\x1b[0m" : " ";
|
|
183
|
+
console.error(` ${marker} ${String(i + 1).padStart(2)}) ${options[i]}`);
|
|
184
|
+
}
|
|
185
|
+
while (true) {
|
|
186
|
+
const answer = (await ask(`Select [1-${options.length}] (default: ${def}): `)).trim();
|
|
187
|
+
if (answer === "") return def;
|
|
188
|
+
const n = Number(answer);
|
|
189
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) return options[n - 1];
|
|
190
|
+
// Allow typing the value directly too.
|
|
191
|
+
if (options.includes(answer)) return answer;
|
|
192
|
+
console.error("\x1b[31m[astra] Invalid choice, try again.\x1b[0m");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Interactive first-time setup for the agent's preferred model + reasoning.
|
|
198
|
+
* Both are chosen from a list (not free-text) and saved to the dedicated
|
|
199
|
+
* config so agent mode remembers them. Returns { model, reasoning }.
|
|
200
|
+
*/
|
|
201
|
+
export async function promptForAgentPrefs({ defaultModel, defaultReasoning } = {}) {
|
|
202
|
+
const model = await selectFromList("[astra] Choose your preferred model:", AVAILABLE_MODELS, {
|
|
203
|
+
defaultValue: defaultModel || DEFAULT_MODEL,
|
|
204
|
+
});
|
|
205
|
+
const reasoning = await selectFromList("[astra] Choose reasoning effort:", REASONING_LEVELS, {
|
|
206
|
+
defaultValue: defaultReasoning || DEFAULT_REASONING,
|
|
207
|
+
});
|
|
208
|
+
saveAgentPrefs({ model, reasoning });
|
|
209
|
+
console.error(
|
|
210
|
+
`\x1b[2m[astra] Saved model=${model} reasoning=${reasoning} to ${configPath()}\x1b[0m`
|
|
211
|
+
);
|
|
212
|
+
return { model, reasoning };
|
|
213
|
+
}
|