@hackerrank/astra-cli 0.1.0 → 0.1.1
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 +36 -2
- package/package.json +1 -1
- package/src/agent.js +9 -0
- package/src/bench.js +478 -0
- package/src/cli.js +322 -33
- package/src/config.js +78 -0
- package/src/model.js +9 -0
- package/src/models.js +31 -0
- package/src/prompts.js +7 -3
- package/src/repl.js +213 -16
- package/src/report.js +459 -0
package/src/cli.js
CHANGED
|
@@ -3,19 +3,42 @@
|
|
|
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
|
+
* -t, --task <text> Task text -> bench mode (run to completion)
|
|
36
|
+
* -f, --task-file <path> Read task text from a file -> bench mode
|
|
37
|
+
* -p, --path <dir> Task directory (copied into an isolated bench
|
|
38
|
+
* workspace) -> bench mode
|
|
39
|
+
* --bench-root <dir> Root folder for bench runs (default: ./bench)
|
|
17
40
|
* -C, --cwd <path> Working directory for commands (default: cwd)
|
|
18
|
-
* -o, --output <path> Also write trajectory JSON here (
|
|
41
|
+
* -o, --output <path> Also write trajectory JSON here (bench mode)
|
|
19
42
|
* -s, --steps <n> Step limit (default: 40)
|
|
20
43
|
* -w, --wall <seconds> Wall-clock limit (default: 0 = none)
|
|
21
44
|
* --timeout <seconds> Per-command timeout (default: 60)
|
|
@@ -25,8 +48,8 @@
|
|
|
25
48
|
* --resume <id> Resume a saved session by id
|
|
26
49
|
* --sessions List saved sessions and exit
|
|
27
50
|
* -y, --yolo Auto-run commands without confirmation
|
|
28
|
-
* (always on in
|
|
29
|
-
* -q, --quiet Do not stream steps (
|
|
51
|
+
* (always on in bench mode)
|
|
52
|
+
* -q, --quiet Do not stream steps (bench mode)
|
|
30
53
|
* -h, --help Show this help
|
|
31
54
|
*
|
|
32
55
|
* API key resolution (first hit wins):
|
|
@@ -34,6 +57,13 @@
|
|
|
34
57
|
* 2. ASTRA_GATEWAY_API_KEY env var
|
|
35
58
|
* 3. ~/.astra/config.json (dedicated astra config)
|
|
36
59
|
* 4. interactive prompt (when run in a terminal); offers to save to (3)
|
|
60
|
+
*
|
|
61
|
+
* Agent model resolution (agent mode, first hit wins):
|
|
62
|
+
* 1. --model / --reasoning flags
|
|
63
|
+
* 2. resumed session
|
|
64
|
+
* 3. cached agent prefs in ~/.astra/config.json (last model used)
|
|
65
|
+
* 4. first-run interactive setup (list pickers for model + reasoning)
|
|
66
|
+
* 5. fallback: glm-5.2 with medium reasoning
|
|
37
67
|
*/
|
|
38
68
|
|
|
39
69
|
import fs from "node:fs";
|
|
@@ -41,7 +71,8 @@ import path from "node:path";
|
|
|
41
71
|
import { GatewayModel } from "./model.js";
|
|
42
72
|
import { LocalEnvironment } from "./environment.js";
|
|
43
73
|
import { Agent } from "./agent.js";
|
|
44
|
-
import { resolveCredentials, promptForCredentials, canPrompt } from "./config.js";
|
|
74
|
+
import { resolveCredentials, promptForCredentials, canPrompt, readAgentPrefs, saveAgentPrefs, promptForAgentPrefs } from "./config.js";
|
|
75
|
+
import { AVAILABLE_MODELS, REASONING_LEVELS, DEFAULT_MODEL, DEFAULT_REASONING } from "./models.js";
|
|
45
76
|
import {
|
|
46
77
|
newSessionId,
|
|
47
78
|
saveSession,
|
|
@@ -51,6 +82,26 @@ import {
|
|
|
51
82
|
deriveTitle,
|
|
52
83
|
} from "./session.js";
|
|
53
84
|
import { runRepl } from "./repl.js";
|
|
85
|
+
import {
|
|
86
|
+
allocateRun,
|
|
87
|
+
seedWorkspace,
|
|
88
|
+
collectMetrics,
|
|
89
|
+
writeMetrics,
|
|
90
|
+
runMatrix,
|
|
91
|
+
aggregate,
|
|
92
|
+
packBench,
|
|
93
|
+
pushToS3,
|
|
94
|
+
defaultPushUri,
|
|
95
|
+
benchRoot,
|
|
96
|
+
} from "./bench.js";
|
|
97
|
+
|
|
98
|
+
/** Map a reasoning level to gateway modelKwargs. Empty for off/none/unset. */
|
|
99
|
+
function reasoningKwargs(level) {
|
|
100
|
+
if (!level) return {};
|
|
101
|
+
const l = String(level).toLowerCase();
|
|
102
|
+
if (l === "off" || l === "none" || l === "disabled") return {};
|
|
103
|
+
return { reasoning_effort: l };
|
|
104
|
+
}
|
|
54
105
|
|
|
55
106
|
function parseArgs(argv) {
|
|
56
107
|
const args = { steps: 40, wall: 0, timeout: 60, quiet: false, "max-output": 16000 };
|
|
@@ -58,6 +109,13 @@ function parseArgs(argv) {
|
|
|
58
109
|
"-m": "model", "--model": "model",
|
|
59
110
|
"-t": "task", "--task": "task",
|
|
60
111
|
"-f": "task-file", "--task-file": "task-file",
|
|
112
|
+
"-p": "path", "--path": "path",
|
|
113
|
+
"-r": "reasoning", "--reasoning": "reasoning",
|
|
114
|
+
"--repeat": "repeat",
|
|
115
|
+
"--push": "push",
|
|
116
|
+
"--push-uri": "push-uri",
|
|
117
|
+
"--tar": "tar",
|
|
118
|
+
"--bench-root": "bench-root",
|
|
61
119
|
"-C": "cwd", "--cwd": "cwd",
|
|
62
120
|
"-o": "output", "--output": "output",
|
|
63
121
|
"-s": "steps", "--steps": "steps",
|
|
@@ -72,7 +130,7 @@ function parseArgs(argv) {
|
|
|
72
130
|
"-y": "yolo", "--yolo": "yolo",
|
|
73
131
|
"-h": "help", "--help": "help",
|
|
74
132
|
};
|
|
75
|
-
const flags = new Set(["quiet", "yolo", "help", "sessions"]);
|
|
133
|
+
const flags = new Set(["quiet", "yolo", "help", "sessions", "tar", "push"]);
|
|
76
134
|
for (let i = 2; i < argv.length; i++) {
|
|
77
135
|
const key = alias[argv[i]];
|
|
78
136
|
if (!key) { console.error(`Unknown option: ${argv[i]}`); process.exit(2); }
|
|
@@ -107,17 +165,12 @@ async function main() {
|
|
|
107
165
|
resumeDoc = loadSession(args.resume);
|
|
108
166
|
}
|
|
109
167
|
|
|
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.
|
|
168
|
+
// A task (-t/-f) or task path (-p) selects bench mode; otherwise agent mode.
|
|
169
|
+
// Bench mode uses the autonomous engine; agent mode uses the interactive one.
|
|
118
170
|
let task = args.task;
|
|
119
171
|
if (args["task-file"]) task = fs.readFileSync(args["task-file"], "utf8");
|
|
120
|
-
const
|
|
172
|
+
const wantsBench = !!(task || args.path);
|
|
173
|
+
const mode = wantsBench
|
|
121
174
|
? "autonomous"
|
|
122
175
|
: resumeDoc?.info?.mode === "autonomous"
|
|
123
176
|
? "autonomous"
|
|
@@ -125,12 +178,13 @@ async function main() {
|
|
|
125
178
|
|
|
126
179
|
if (mode === "interactive" && !canPrompt()) {
|
|
127
180
|
console.error(
|
|
128
|
-
"\x1b[31m[astra]
|
|
129
|
-
"to run
|
|
181
|
+
"\x1b[31m[astra] agent mode needs a terminal. Provide a task with -t/-f/-p\n" +
|
|
182
|
+
"to run a bench, or run in a TTY.\x1b[0m"
|
|
130
183
|
);
|
|
131
184
|
process.exit(2);
|
|
132
185
|
}
|
|
133
186
|
|
|
187
|
+
// -------------------- API KEY RESOLUTION --------------------
|
|
134
188
|
let apiKey = resolveApiKey(args["api-key"], args["base-url"]);
|
|
135
189
|
if (!apiKey) {
|
|
136
190
|
if (canPrompt()) {
|
|
@@ -149,6 +203,69 @@ async function main() {
|
|
|
149
203
|
}
|
|
150
204
|
}
|
|
151
205
|
|
|
206
|
+
// -------------------- MODEL + REASONING RESOLUTION --------------------
|
|
207
|
+
// Bench (autonomous) mode: an explicit --model is required so runs are
|
|
208
|
+
// reproducible and never silently pick a cached/default model.
|
|
209
|
+
//
|
|
210
|
+
// Agent (interactive) mode: resolve in priority order
|
|
211
|
+
// 1. --model flag / --reasoning flag
|
|
212
|
+
// 2. resumed session
|
|
213
|
+
// 3. cached agent prefs in ~/.astra/config.json ("remember last model")
|
|
214
|
+
// 4. first-time interactive setup (list pickers for model + reasoning)
|
|
215
|
+
// 5. hard fallback: glm-5.2 with medium reasoning
|
|
216
|
+
// The chosen values are cached so the next `astra` needs no flags.
|
|
217
|
+
let modelId = args.model || resumeDoc?.info?.model;
|
|
218
|
+
let reasoning = args.reasoning || "";
|
|
219
|
+
|
|
220
|
+
if (mode === "autonomous") {
|
|
221
|
+
if (!modelId) {
|
|
222
|
+
console.error("\x1b[31m[astra] --model is required for bench mode.\x1b[0m\n");
|
|
223
|
+
console.log(HELP);
|
|
224
|
+
process.exit(2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Multi-model / multi-reasoning / repeated matrix. Triggered by a
|
|
228
|
+
// comma-separated model or reasoning list, or --repeat > 1. Runs each cell
|
|
229
|
+
// sequentially in its own isolated workspace, then prints a leaderboard and
|
|
230
|
+
// optionally tars + uploads the whole bench/ folder.
|
|
231
|
+
const models = String(modelId).split(",").map((s) => s.trim()).filter(Boolean);
|
|
232
|
+
const reasonings = reasoning
|
|
233
|
+
? String(reasoning).split(",").map((s) => s.trim()).filter(Boolean)
|
|
234
|
+
: [""];
|
|
235
|
+
const repeat = Math.max(1, Number(args.repeat) || 1);
|
|
236
|
+
const isMatrix = models.length > 1 || reasonings.length > 1 || repeat > 1;
|
|
237
|
+
|
|
238
|
+
if (isMatrix && !resumeDoc) {
|
|
239
|
+
await runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet: !!args.quiet });
|
|
240
|
+
return; // runBenchMatrix exits the process
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
const cached = readAgentPrefs();
|
|
244
|
+
if (!modelId) modelId = cached.model;
|
|
245
|
+
if (!reasoning) reasoning = cached.reasoning;
|
|
246
|
+
|
|
247
|
+
if (!modelId) {
|
|
248
|
+
// No flag, no resume, no cache — run first-time setup (list pickers).
|
|
249
|
+
if (canPrompt()) {
|
|
250
|
+
const prefs = await promptForAgentPrefs({
|
|
251
|
+
defaultModel: DEFAULT_MODEL,
|
|
252
|
+
defaultReasoning: DEFAULT_REASONING,
|
|
253
|
+
});
|
|
254
|
+
modelId = prefs.model;
|
|
255
|
+
reasoning = reasoning || prefs.reasoning;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Hard fallback if we still have nothing.
|
|
260
|
+
if (!modelId) {
|
|
261
|
+
modelId = DEFAULT_MODEL;
|
|
262
|
+
reasoning = reasoning || DEFAULT_REASONING;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Remember the last model/reasoning used in agent mode.
|
|
266
|
+
saveAgentPrefs({ model: modelId, reasoning: reasoning || DEFAULT_REASONING });
|
|
267
|
+
}
|
|
268
|
+
|
|
152
269
|
const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
|
|
153
270
|
const quiet = !!args.quiet;
|
|
154
271
|
|
|
@@ -156,10 +273,30 @@ async function main() {
|
|
|
156
273
|
model: modelId,
|
|
157
274
|
baseUrl: args["base-url"],
|
|
158
275
|
apiKey,
|
|
276
|
+
modelKwargs: reasoningKwargs(reasoning),
|
|
159
277
|
onRetry: quiet ? () => {} : (r) => printRetry(r),
|
|
160
278
|
});
|
|
279
|
+
|
|
280
|
+
// -------------------- BENCH SETUP --------------------
|
|
281
|
+
// A fresh bench run (task text or task path, not a resume) gets an isolated
|
|
282
|
+
// workspace under bench/<model-name-reasoning>/run-NN/. The agent's commands
|
|
283
|
+
// run inside that workspace and metrics are recorded when it finishes.
|
|
284
|
+
let benchRun = null;
|
|
285
|
+
if (mode === "autonomous" && !resumeDoc) {
|
|
286
|
+
benchRun = allocateRun({ model: modelId, reasoning, root: args["bench-root"] });
|
|
287
|
+
const seeded = seedWorkspace(benchRun.workspace, {
|
|
288
|
+
taskPath: args.path,
|
|
289
|
+
taskFile: args["task-file"],
|
|
290
|
+
taskText: task,
|
|
291
|
+
});
|
|
292
|
+
task = seeded.taskText || task || "";
|
|
293
|
+
// Persist the task text alongside the run for reproducibility.
|
|
294
|
+
fs.writeFileSync(path.join(benchRun.dir, "task.md"), task || "");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const cmdCwd = benchRun ? benchRun.workspace : cwd;
|
|
161
298
|
const env = new LocalEnvironment({
|
|
162
|
-
cwd,
|
|
299
|
+
cwd: cmdCwd,
|
|
163
300
|
timeout: Number(args.timeout),
|
|
164
301
|
maxOutputChars: Number(args["max-output"]),
|
|
165
302
|
});
|
|
@@ -169,7 +306,11 @@ async function main() {
|
|
|
169
306
|
mode,
|
|
170
307
|
stepLimit: Number(args.steps),
|
|
171
308
|
wallTimeLimitSeconds: Number(args.wall),
|
|
172
|
-
outputPath:
|
|
309
|
+
outputPath: benchRun
|
|
310
|
+
? path.join(benchRun.dir, "trajectory.json")
|
|
311
|
+
: args.output
|
|
312
|
+
? path.resolve(args.output)
|
|
313
|
+
: null,
|
|
173
314
|
sessionId,
|
|
174
315
|
saveSession,
|
|
175
316
|
onEvent: quiet || mode === "interactive" ? () => {} : (msg) => printEvent(msg),
|
|
@@ -184,26 +325,67 @@ async function main() {
|
|
|
184
325
|
// -------------------- INTERACTIVE MODE --------------------
|
|
185
326
|
if (mode === "interactive") {
|
|
186
327
|
if (!resumeDoc) agent.start();
|
|
187
|
-
agent.title = agent.title || "
|
|
188
|
-
|
|
328
|
+
agent.title = agent.title || "agent";
|
|
329
|
+
// Bench runner for the in-REPL Alt+Tab toggle: runs a task autonomously in
|
|
330
|
+
// an isolated workspace under bench/<model-name-reasoning>/run-NN/.
|
|
331
|
+
const runBench = async (taskText, log) => {
|
|
332
|
+
const alloc = allocateRun({ model: modelId, reasoning, root: args["bench-root"] });
|
|
333
|
+
seedWorkspace(alloc.workspace, { taskText });
|
|
334
|
+
fs.writeFileSync(path.join(alloc.dir, "task.md"), taskText || "");
|
|
335
|
+
log(`[astra] bench ${alloc.slug}/${alloc.runId} → ${alloc.workspace}`);
|
|
336
|
+
const benchEnv = new LocalEnvironment({
|
|
337
|
+
cwd: alloc.workspace,
|
|
338
|
+
timeout: Number(args.timeout),
|
|
339
|
+
maxOutputChars: Number(args["max-output"]),
|
|
340
|
+
});
|
|
341
|
+
const benchAgent = new Agent(model, benchEnv, {
|
|
342
|
+
mode: "autonomous",
|
|
343
|
+
stepLimit: Number(args.steps),
|
|
344
|
+
wallTimeLimitSeconds: Number(args.wall),
|
|
345
|
+
outputPath: path.join(alloc.dir, "trajectory.json"),
|
|
346
|
+
});
|
|
347
|
+
const res = await benchAgent.run(taskText);
|
|
348
|
+
const metrics = collectMetrics({ agent: benchAgent, model, reasoning });
|
|
349
|
+
const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
|
|
350
|
+
log(`[astra] bench done: exit=${res.exit_status} · metrics → ${paths.runMetrics}`);
|
|
351
|
+
};
|
|
352
|
+
await runRepl(agent, { model, autoRun: !!args.yolo, fresh: !resumeDoc, reasoning, runBench });
|
|
189
353
|
process.exit(0);
|
|
190
354
|
}
|
|
191
355
|
|
|
192
|
-
// --------------------
|
|
356
|
+
// -------------------- BENCH MODE (autonomous engine) --------------------
|
|
193
357
|
if (!task && !resumeDoc) {
|
|
194
|
-
console.error("\x1b[31m[astra] provide a task with -t or -
|
|
358
|
+
console.error("\x1b[31m[astra] provide a task with -t, -f, or -p.\x1b[0m");
|
|
195
359
|
process.exit(2);
|
|
196
360
|
}
|
|
197
361
|
if (task) agent.title = deriveTitle({ info: { task }, messages: [] });
|
|
198
362
|
|
|
199
363
|
if (!quiet) {
|
|
200
|
-
|
|
364
|
+
if (benchRun) {
|
|
365
|
+
console.error(
|
|
366
|
+
`\x1b[2m[astra] bench · ${benchRun.slug}/${benchRun.runId} ` +
|
|
367
|
+
`· model=${modelId}${reasoning ? ` reasoning=${reasoning}` : ""} steps<=${args.steps}\x1b[0m`
|
|
368
|
+
);
|
|
369
|
+
console.error(`\x1b[2m[astra] workspace -> ${benchRun.workspace}\x1b[0m`);
|
|
370
|
+
} else {
|
|
371
|
+
console.error(`\x1b[2m[astra] bench · model=${modelId} cwd=${cmdCwd} steps<=${args.steps}\x1b[0m`);
|
|
372
|
+
}
|
|
201
373
|
}
|
|
202
374
|
|
|
203
375
|
const result =
|
|
204
376
|
resumeDoc && !task ? await continueAutonomous(agent) : await agent.run(task);
|
|
205
377
|
agent.save();
|
|
206
378
|
|
|
379
|
+
// Record benchmark metrics (per-run CSV + rolled-up index).
|
|
380
|
+
if (benchRun) {
|
|
381
|
+
const metrics = collectMetrics({ agent, model, reasoning });
|
|
382
|
+
const paths = writeMetrics({ runDir: benchRun.dir, root: benchRun.root, metrics });
|
|
383
|
+
if (!quiet) {
|
|
384
|
+
console.error(`\x1b[2m[astra] metrics -> ${paths.runMetrics}\x1b[0m`);
|
|
385
|
+
console.error(`\x1b[2m[astra] index -> ${paths.index}\x1b[0m`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
207
389
|
if (!quiet) {
|
|
208
390
|
const pt = model.totalPromptTokens;
|
|
209
391
|
const ct = model.totalCompletionTokens;
|
|
@@ -229,6 +411,113 @@ async function main() {
|
|
|
229
411
|
process.exit(result.exit_status === "Submitted" ? 0 : 1);
|
|
230
412
|
}
|
|
231
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Run a multi-model / multi-reasoning / repeated bench matrix, print a
|
|
416
|
+
* leaderboard, optionally tar + upload the bench/ folder, then exit.
|
|
417
|
+
*/
|
|
418
|
+
async function runBenchMatrix({ args, apiKey, models, reasonings, repeat, task, quiet }) {
|
|
419
|
+
const root = args["bench-root"];
|
|
420
|
+
const total = models.length * reasonings.length * repeat;
|
|
421
|
+
console.error(
|
|
422
|
+
`\x1b[1m[astra] bench matrix · ${models.length} model(s) × ` +
|
|
423
|
+
`${reasonings.length} reasoning × ${repeat} repeat = ${total} run(s)\x1b[0m`
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
let n = 0;
|
|
427
|
+
const rows = await runMatrix({
|
|
428
|
+
models,
|
|
429
|
+
reasonings,
|
|
430
|
+
repeat,
|
|
431
|
+
apiKey,
|
|
432
|
+
baseUrl: args["base-url"],
|
|
433
|
+
task,
|
|
434
|
+
taskPath: args.path,
|
|
435
|
+
taskFile: args["task-file"],
|
|
436
|
+
root,
|
|
437
|
+
steps: Number(args.steps),
|
|
438
|
+
wall: Number(args.wall),
|
|
439
|
+
timeout: Number(args.timeout),
|
|
440
|
+
maxOutputChars: Number(args["max-output"]),
|
|
441
|
+
onStart: (c) => {
|
|
442
|
+
n++;
|
|
443
|
+
if (!quiet) {
|
|
444
|
+
console.error(`\x1b[36m[astra] (${n}/${total}) ${c.slug}/${c.runId} …\x1b[0m`);
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
onDone: (r) => {
|
|
448
|
+
const mark = r.resolved ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
449
|
+
const status = r.error ? `Error: ${r.error.split("\n")[0]}` : r.exit_status;
|
|
450
|
+
console.error(
|
|
451
|
+
` ${mark} ${String(r.model).padEnd(20)} ${String(r.reasoning).padEnd(8)} ` +
|
|
452
|
+
`${String(status).padEnd(16)} ${String(r.steps).padStart(3)} steps · ` +
|
|
453
|
+
`${fmt(r.total_tokens)} tok · ${fmtUsd(r.cost_usd)}` +
|
|
454
|
+
`${r.cost_source === "estimated" ? "~" : ""}`
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
printLeaderboard(aggregate(rows));
|
|
460
|
+
|
|
461
|
+
const indexPath = path.join(benchRoot(root), "metrics.csv");
|
|
462
|
+
|
|
463
|
+
// Collect the important artifact locations to print together at the end.
|
|
464
|
+
const links = [["metrics", indexPath]];
|
|
465
|
+
|
|
466
|
+
// Tar + optional S3 push of the whole bench/ folder.
|
|
467
|
+
if (args.tar || args.push) {
|
|
468
|
+
try {
|
|
469
|
+
const tarball = packBench({ root });
|
|
470
|
+
links.push(["tarball", tarball]);
|
|
471
|
+
if (args.push) {
|
|
472
|
+
// Bare --push uses the default bucket + timestamp prefix; --push-uri
|
|
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
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const solved = rows.filter((r) => r.resolved).length;
|
|
490
|
+
console.error(`\n\x1b[1m[astra] matrix done · ${solved}/${rows.length} resolved\x1b[0m`);
|
|
491
|
+
|
|
492
|
+
// Print the important URLs / paths, aligned, at the very end.
|
|
493
|
+
const w = Math.max(...links.map(([k]) => k.length));
|
|
494
|
+
console.error("");
|
|
495
|
+
for (const [k, v] of links) {
|
|
496
|
+
console.error(`\x1b[1m ${k.padEnd(w)}\x1b[0m ${v}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
process.exit(solved === rows.length ? 0 : 1);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Print the per-(model,reasoning) leaderboard table. */
|
|
503
|
+
function printLeaderboard(board) {
|
|
504
|
+
board.sort((a, b) => b.solved_rate - a.solved_rate || a.avg_steps - b.avg_steps);
|
|
505
|
+
console.error(
|
|
506
|
+
`\n\x1b[1m${"model".padEnd(20)} ${"reason".padEnd(8)} ${"solved".padEnd(8)} ` +
|
|
507
|
+
`${"steps".padStart(6)} ${"tokens".padStart(9)} ${"cost".padStart(9)} source\x1b[0m`
|
|
508
|
+
);
|
|
509
|
+
for (const g of board) {
|
|
510
|
+
const solved = `${g.solved}/${g.runs}`;
|
|
511
|
+
const rate = `${Math.round(g.solved_rate * 100)}%`;
|
|
512
|
+
const cost = g.cost_usd == null ? "n/a" : fmtUsd(g.cost_usd);
|
|
513
|
+
console.error(
|
|
514
|
+
`${String(g.model).padEnd(20)} ${String(g.reasoning).padEnd(8)} ` +
|
|
515
|
+
`${(solved + " " + rate).padEnd(8)} ${g.avg_steps.toFixed(1).padStart(6)} ` +
|
|
516
|
+
`${fmt(Math.round(g.avg_tokens)).padStart(9)} ${cost.padStart(9)} ${g.cost_source}`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
232
521
|
/** Drive an already-seeded autonomous agent to completion (used on resume). */
|
|
233
522
|
async function continueAutonomous(agent) {
|
|
234
523
|
while (true) {
|
|
@@ -259,11 +548,11 @@ function printSessions() {
|
|
|
259
548
|
}
|
|
260
549
|
|
|
261
550
|
function printEvent(msg) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.error(`\x1b[
|
|
551
|
+
// Bench mode keeps output compact: skip the verbose system/user/assistant
|
|
552
|
+
// message bodies (per-step progress is shown by printStep). Only surface a
|
|
553
|
+
// non-empty terminal exit event so the run's submission is still visible.
|
|
554
|
+
if (msg.role !== "exit" || !msg.content?.trim()) return;
|
|
555
|
+
console.error(`\x1b[35m--- SUBMISSION ---\x1b[0m\n${msg.content}\n`);
|
|
267
556
|
}
|
|
268
557
|
|
|
269
558
|
/** 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
|
+
}
|
package/src/model.js
CHANGED
|
@@ -55,6 +55,10 @@ export class GatewayModel {
|
|
|
55
55
|
// Cumulative token usage across all calls (exact, from the API).
|
|
56
56
|
this.totalPromptTokens = 0;
|
|
57
57
|
this.totalCompletionTokens = 0;
|
|
58
|
+
this.totalReasoningTokens = 0;
|
|
59
|
+
this.totalCachedTokens = 0;
|
|
60
|
+
this.totalCacheWriteTokens = 0;
|
|
61
|
+
this.nRetries = 0;
|
|
58
62
|
// Cumulative USD cost. `reported` = summed from the gateway's usage.cost;
|
|
59
63
|
// `estimated` = summed from the public price table (prices.js).
|
|
60
64
|
this.totalCostUsd = 0;
|
|
@@ -113,6 +117,7 @@ export class GatewayModel {
|
|
|
113
117
|
// backoff (honoring Retry-After when the server provides it).
|
|
114
118
|
if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {
|
|
115
119
|
const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
|
|
120
|
+
this.nRetries++;
|
|
116
121
|
this.onRetry({
|
|
117
122
|
attempt: attempt + 1,
|
|
118
123
|
maxRetries: this.maxRetries,
|
|
@@ -133,6 +138,9 @@ export class GatewayModel {
|
|
|
133
138
|
const usage = normalizeUsage(data?.usage);
|
|
134
139
|
this.totalPromptTokens += usage.prompt_tokens;
|
|
135
140
|
this.totalCompletionTokens += usage.completion_tokens;
|
|
141
|
+
this.totalReasoningTokens += usage.reasoning_tokens || 0;
|
|
142
|
+
this.totalCachedTokens += usage.cached_tokens || 0;
|
|
143
|
+
this.totalCacheWriteTokens += usage.cache_write_tokens || 0;
|
|
136
144
|
// Cost: prefer the gateway's exact number; otherwise estimate from the
|
|
137
145
|
// public price table. Attach per-call cost + source to usage.
|
|
138
146
|
const cost = this._accountCost(usage);
|
|
@@ -145,6 +153,7 @@ export class GatewayModel {
|
|
|
145
153
|
// (auth, quota, context window) are re-thrown immediately.
|
|
146
154
|
if (attempt < this.maxRetries && isRetryable(err)) {
|
|
147
155
|
const wait = backoffMs(attempt);
|
|
156
|
+
this.nRetries++;
|
|
148
157
|
this.onRetry({
|
|
149
158
|
attempt: attempt + 1,
|
|
150
159
|
maxRetries: this.maxRetries,
|
package/src/models.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central catalog of gateway models and reasoning levels used by astra.
|
|
3
|
+
*
|
|
4
|
+
* Kept in one place so the CLI, interactive setup, and any list-based pickers
|
|
5
|
+
* stay in sync. The list mirrors the context-window table in repl.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Selectable model ids on the gateway. */
|
|
9
|
+
export const AVAILABLE_MODELS = [
|
|
10
|
+
"claude-opus-5",
|
|
11
|
+
"claude-sonnet-5",
|
|
12
|
+
"gemini-3.7-flash",
|
|
13
|
+
"gpt-5.6-luna",
|
|
14
|
+
"gpt-5.6-sol",
|
|
15
|
+
"gpt-5.6-terra",
|
|
16
|
+
"grok-4.6",
|
|
17
|
+
"deepseek-v4-pro",
|
|
18
|
+
"glm-5.2",
|
|
19
|
+
"kimi-k3",
|
|
20
|
+
"qwen-3.8",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Selectable reasoning-effort levels. */
|
|
24
|
+
export const REASONING_LEVELS = ["off", "low", "medium", "high"];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ultimate fallback when no model is configured, cached, or chosen.
|
|
28
|
+
* (Requested behavior: fall back to glm-5.2 with medium reasoning.)
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_MODEL = "glm-5.2";
|
|
31
|
+
export const DEFAULT_REASONING = "medium";
|
package/src/prompts.js
CHANGED
|
@@ -42,13 +42,17 @@ Autonomous mode:
|
|
|
42
42
|
After that command you cannot continue.`;
|
|
43
43
|
|
|
44
44
|
export const INTERACTIVE_RULES = `
|
|
45
|
-
|
|
46
|
-
- You are a
|
|
45
|
+
Agent mode (interactive SWE assistant):
|
|
46
|
+
- You are astra, a sharp, pragmatic senior software engineer pair-programming
|
|
47
|
+
with the user in their shell. Be concise, direct, and technically precise.
|
|
48
|
+
- Work turn by turn. Prefer small, verifiable steps over large speculative ones.
|
|
47
49
|
- To run a shell command, use the bash block as described above.
|
|
48
50
|
- To talk to the user (answer a question, ask for clarification, summarize, or
|
|
49
51
|
report you are done) respond with plain text and NO bash block. That hands
|
|
50
52
|
the turn back to the user.
|
|
51
|
-
-
|
|
53
|
+
- Explain what you are about to do in one or two lines before acting.
|
|
54
|
+
- Never fabricate command output; always run the command and react to what you
|
|
55
|
+
actually observe.`;
|
|
52
56
|
|
|
53
57
|
export const INSTANCE_TEMPLATE = `Please complete this task:
|
|
54
58
|
|