@hackerrank/astra-cli 0.1.5 → 0.1.7
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 +41 -9
- package/package.json +1 -1
- package/src/agent.js +37 -6
- package/src/bench.js +65 -8
- package/src/cli.js +132 -6
- package/src/ledger.js +68 -0
- package/src/model.js +8 -1
- package/src/models.js +1 -0
- package/src/project-bench.js +154 -0
- package/src/project.js +89 -0
- package/src/prompts.js +2 -0
- package/src/report.html +77 -30
- package/src/report.js +106 -0
- package/src/result-contract.js +58 -0
- package/src/verifier-runner.js +159 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Every run is a resumable session, with exact token accounting and USD cost track
|
|
|
29
29
|
| `src/model.js` | Gateway client (OpenAI-compatible `/chat/completions`). Handles retries, the `max_tokens` vs `max_completion_tokens` difference, and cost accounting. |
|
|
30
30
|
| `src/environment.js` | Runs one command per action in a fresh subshell, with a per-command timeout that kills the whole process group and output truncation. |
|
|
31
31
|
| `src/prompts.js` | System / instance / format-error / observation templates + a tiny `{{var}}` renderer, split into interactive vs autonomous rules. |
|
|
32
|
-
| `src/agent.js` | The turn-based engine: query → parse
|
|
32
|
+
| `src/agent.js` | The turn-based engine: query → parse shell blocks → execute → observe → repeat. Autonomous runs can combine related blocks; interactive runs remain strict. |
|
|
33
33
|
| `src/repl.js` | The interactive assistant loop: prompts, command approval, slash commands. |
|
|
34
34
|
| `src/session.js` | Resumable sessions stored under `~/.astra/sessions/`. |
|
|
35
35
|
| `src/config.js` | Dedicated `~/.astra/config.json`, credential resolution, interactive key prompt. |
|
|
@@ -141,19 +141,26 @@ it through the existing bench persona:
|
|
|
141
141
|
astra bench --project ./task
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
`./task/.astra/project.toml` selects the
|
|
145
|
-
models, limits, output root, and
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
`./task/.astra/project.toml` selects the task type (`brownfield` or
|
|
145
|
+
`greenfield`), public workspace, instruction, models, limits, output root, and
|
|
146
|
+
optional context extensions. A task-owned `verifier/run_verifier.py` is
|
|
147
|
+
discovered automatically; an explicit `[verification]` entry may override its
|
|
148
|
+
command and lifecycle settings. CLI flags still override bench limits and
|
|
149
|
+
output paths.
|
|
150
|
+
|
|
151
|
+
A project-configured run copies only the public workspace, freezes each
|
|
152
|
+
candidate, invokes the task-owned verifier outside that workspace, reads its
|
|
153
|
+
versioned JSON report, and writes a merged result. Astra contains no task
|
|
154
|
+
business logic. If no verifier exists, the run is still recorded with
|
|
155
|
+
`solved_score = "NA"`; if a verifier is configured but fails to produce a
|
|
156
|
+
valid report, the result is an infrastructure failure. `model_error`,
|
|
157
|
+
`candidate`, `verifier`, and `infrastructure` failures remain distinct.
|
|
152
158
|
|
|
153
159
|
```toml
|
|
154
160
|
[project]
|
|
155
161
|
id = "peoplecore-scim"
|
|
156
162
|
version = 1
|
|
163
|
+
type = "brownfield"
|
|
157
164
|
instruction = "instruction.md"
|
|
158
165
|
workspace = "public/codebase"
|
|
159
166
|
|
|
@@ -165,12 +172,26 @@ steps = 60
|
|
|
165
172
|
wall_seconds = 3600
|
|
166
173
|
command_timeout_seconds = 120
|
|
167
174
|
|
|
175
|
+
[verification]
|
|
176
|
+
command = "python3 verifier/run_verifier.py --base-url $SCIM_BASE_URL --token $SCIM_BEARER_TOKEN --report $ASTRA_VERIFIER_REPORT"
|
|
177
|
+
base_url = "http://127.0.0.1:3000/api/v1/scim"
|
|
178
|
+
readiness_url = "http://127.0.0.1:3000/health"
|
|
179
|
+
timeout_seconds = 3600
|
|
180
|
+
report = "verifier-report.json"
|
|
181
|
+
token_env = "SCIM_BEARER_TOKEN"
|
|
182
|
+
|
|
168
183
|
[templates]
|
|
169
184
|
bench = ".astra/templates/bench.md"
|
|
170
185
|
[extensions]
|
|
171
186
|
directories = [".astra/extensions"]
|
|
172
187
|
```
|
|
173
188
|
|
|
189
|
+
Project runs are appendable. The durable cell key is task/version/model/
|
|
190
|
+
reasoning/repeat. The report display key is `model-reasoning`, so adding a
|
|
191
|
+
model, reasoning effort, or a higher per-cell repeat count preserves completed
|
|
192
|
+
cells and processes only missing cells. If a process stops, rerunning the same
|
|
193
|
+
project resumes the cell/session or verifier stage from the ledger.
|
|
194
|
+
|
|
174
195
|
### HTML report
|
|
175
196
|
|
|
176
197
|
Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
|
|
@@ -204,6 +225,17 @@ astra --resume <id> # resume (continue an interactive chat,
|
|
|
204
225
|
# or inspect/continue an autonomous run)
|
|
205
226
|
```
|
|
206
227
|
|
|
228
|
+
Resume an interrupted benchmark without repeating its task, model, or output
|
|
229
|
+
arguments:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
astra bench --resume <session-id>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
It reopens the original candidate workspace and updates that same run's
|
|
236
|
+
trajectory, metrics row, and report telemetry. Explicit flags override the
|
|
237
|
+
saved setting when needed.
|
|
238
|
+
|
|
207
239
|
Verified working models on the gateway include `claude-sonnet-5`, `claude-opus-5`,
|
|
208
240
|
`gpt-5.6-sol`, `gpt-5.6-terra`, `gemini-3.7-flash`, `grok-4.6`, `kimi-k3`, and others.
|
|
209
241
|
Some routes (e.g. `glm-5.2`) may return HTTP 402 when the underlying provider is out of
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -64,6 +64,7 @@ export class Agent {
|
|
|
64
64
|
this.sessionId = opts.sessionId ?? null;
|
|
65
65
|
this.sessionCreated = opts.sessionCreated ?? new Date().toISOString();
|
|
66
66
|
this.title = opts.title ?? "";
|
|
67
|
+
this.bench = opts.bench ?? null;
|
|
67
68
|
this._saveSession = opts.saveSession ?? null; // (id, doc) => void
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -149,7 +150,7 @@ export class Agent {
|
|
|
149
150
|
// --- 2. Parse a command (may be absent) ---
|
|
150
151
|
let command;
|
|
151
152
|
try {
|
|
152
|
-
command = parseCommand(content);
|
|
153
|
+
command = parseCommand(content, { allowMultiple: this.mode === "autonomous" });
|
|
153
154
|
this.formatErrorStreak = 0;
|
|
154
155
|
} catch (err) {
|
|
155
156
|
// In interactive mode, "no command" is a normal chat reply that yields
|
|
@@ -240,6 +241,7 @@ export class Agent {
|
|
|
240
241
|
id: this.sessionId ?? undefined,
|
|
241
242
|
created: this.sessionCreated,
|
|
242
243
|
title: this.title || undefined,
|
|
244
|
+
bench: this.bench ?? undefined,
|
|
243
245
|
info: {
|
|
244
246
|
mode: this.mode,
|
|
245
247
|
task: this.task || undefined,
|
|
@@ -248,10 +250,20 @@ export class Agent {
|
|
|
248
250
|
n_steps: this.nSteps,
|
|
249
251
|
model: this.model.model,
|
|
250
252
|
n_calls: this.model.nCalls,
|
|
253
|
+
n_retries: this.model.nRetries,
|
|
254
|
+
counters: {
|
|
255
|
+
commands: this.nCommands,
|
|
256
|
+
failed_commands: this.nFailedCommands,
|
|
257
|
+
format_errors: this.nFormatErrors,
|
|
258
|
+
declined: this.nDeclined,
|
|
259
|
+
},
|
|
251
260
|
elapsed_seconds: Math.round((Date.now() - this.startTime) / 1000),
|
|
252
261
|
tokens: {
|
|
253
262
|
prompt: this.model.totalPromptTokens,
|
|
254
263
|
completion: this.model.totalCompletionTokens,
|
|
264
|
+
cached: this.model.totalCachedTokens,
|
|
265
|
+
cache_write: this.model.totalCacheWriteTokens,
|
|
266
|
+
reasoning: this.model.totalReasoningTokens,
|
|
255
267
|
total: this.model.totalPromptTokens + this.model.totalCompletionTokens,
|
|
256
268
|
last_context: this.lastContextTokens,
|
|
257
269
|
},
|
|
@@ -282,8 +294,25 @@ export class Agent {
|
|
|
282
294
|
this.task = data?.info?.task ?? this.task;
|
|
283
295
|
this.mode = data?.info?.mode === "interactive" ? "interactive" : this.mode;
|
|
284
296
|
this.nSteps = data?.info?.n_steps ?? this.messages.filter((m) => m.role === "assistant").length;
|
|
297
|
+
this.nCommands = data?.info?.counters?.commands ?? 0;
|
|
298
|
+
this.nFailedCommands = data?.info?.counters?.failed_commands ?? 0;
|
|
299
|
+
this.nFormatErrors = data?.info?.counters?.format_errors ?? 0;
|
|
300
|
+
this.nDeclined = data?.info?.counters?.declined ?? 0;
|
|
285
301
|
this.lastContextTokens = data?.info?.tokens?.last_context ?? 0;
|
|
302
|
+
this.model.nCalls = data?.info?.n_calls ?? this.model.nCalls;
|
|
303
|
+
this.model.nRetries = data?.info?.n_retries ?? this.model.nRetries;
|
|
304
|
+
this.model.totalPromptTokens = data?.info?.tokens?.prompt ?? this.model.totalPromptTokens;
|
|
305
|
+
this.model.totalCompletionTokens = data?.info?.tokens?.completion ?? this.model.totalCompletionTokens;
|
|
306
|
+
this.model.totalCachedTokens = data?.info?.tokens?.cached ?? this.model.totalCachedTokens;
|
|
307
|
+
this.model.totalCacheWriteTokens = data?.info?.tokens?.cache_write ?? this.model.totalCacheWriteTokens;
|
|
308
|
+
this.model.totalReasoningTokens = data?.info?.tokens?.reasoning ?? this.model.totalReasoningTokens;
|
|
309
|
+
this.model.totalCostUsd = data?.info?.cost?.usd ?? this.model.totalCostUsd;
|
|
310
|
+
this.model.reportedCostUsd = data?.info?.cost?.reported_usd ?? this.model.reportedCostUsd;
|
|
311
|
+
this.model.estimatedCostUsd = data?.info?.cost?.estimated_usd ?? this.model.estimatedCostUsd;
|
|
312
|
+
this.model.costSource = data?.info?.cost?.source ?? this.model.costSource;
|
|
313
|
+
this.startTime = Date.now() - Number(data?.info?.elapsed_seconds || 0) * 1000;
|
|
286
314
|
this.exitStatus = data?.info?.exit_status || null;
|
|
315
|
+
this.bench = data?.bench ?? this.bench;
|
|
287
316
|
if (data?.id) this.sessionId = data.id;
|
|
288
317
|
if (data?.created) this.sessionCreated = data.created;
|
|
289
318
|
if (data?.title) this.title = data.title;
|
|
@@ -291,9 +320,11 @@ export class Agent {
|
|
|
291
320
|
}
|
|
292
321
|
|
|
293
322
|
/** Extract exactly one command from a fenced bash block. */
|
|
294
|
-
export function parseCommand(text) {
|
|
295
|
-
// Accept
|
|
296
|
-
|
|
323
|
+
export function parseCommand(text, { allowMultiple = false } = {}) {
|
|
324
|
+
// Accept common shell fence labels. Autonomous mode can combine several
|
|
325
|
+
// fenced blocks into one script; interactive mode retains the strict one
|
|
326
|
+
// command contract.
|
|
327
|
+
const re = /```(?:bash|sh|shell|zsh)?[ \t]*\r?\n([\s\S]*?)```/gi;
|
|
297
328
|
const blocks = [];
|
|
298
329
|
let m;
|
|
299
330
|
while ((m = re.exec(text)) !== null) blocks.push(m[1].trim());
|
|
@@ -303,12 +334,12 @@ export function parseCommand(text) {
|
|
|
303
334
|
err.code = "NO_COMMAND";
|
|
304
335
|
throw err;
|
|
305
336
|
}
|
|
306
|
-
if (blocks.length > 1) {
|
|
337
|
+
if (blocks.length > 1 && !allowMultiple) {
|
|
307
338
|
const err = new Error(`Found ${blocks.length} code blocks; provide exactly one.`);
|
|
308
339
|
err.code = "MULTI_COMMAND";
|
|
309
340
|
throw err;
|
|
310
341
|
}
|
|
311
|
-
const cmd = blocks
|
|
342
|
+
const cmd = blocks.join("\n").trim();
|
|
312
343
|
if (!cmd) {
|
|
313
344
|
const err = new Error("The bash code block was empty.");
|
|
314
345
|
err.code = "EMPTY_COMMAND";
|
package/src/bench.js
CHANGED
|
@@ -23,9 +23,11 @@ import { spawnSync } from "node:child_process";
|
|
|
23
23
|
import { GatewayModel } from "./model.js";
|
|
24
24
|
import { LocalEnvironment } from "./environment.js";
|
|
25
25
|
import { Agent } from "./agent.js";
|
|
26
|
+
import { loadSession, newSessionId, saveSession } from "./session.js";
|
|
26
27
|
|
|
27
28
|
/** CSV columns, in order, for the metrics table. */
|
|
28
29
|
export const METRIC_COLUMNS = [
|
|
30
|
+
"run_path",
|
|
29
31
|
"timestamp",
|
|
30
32
|
"model",
|
|
31
33
|
"reasoning",
|
|
@@ -48,6 +50,11 @@ export const METRIC_COLUMNS = [
|
|
|
48
50
|
"last_context_tokens",
|
|
49
51
|
"cost_usd",
|
|
50
52
|
"cost_source",
|
|
53
|
+
"verification_status",
|
|
54
|
+
"verification_score",
|
|
55
|
+
"solved_score",
|
|
56
|
+
"verification_hard_pass",
|
|
57
|
+
"failure_owner",
|
|
51
58
|
];
|
|
52
59
|
|
|
53
60
|
/**
|
|
@@ -211,8 +218,9 @@ export function metricsHeader() {
|
|
|
211
218
|
* the top-level rolled-up index at <root>/metrics.csv.
|
|
212
219
|
*/
|
|
213
220
|
export function writeMetrics({ runDir, root, metrics }) {
|
|
221
|
+
const rowMetrics = { ...metrics, run_path: path.relative(benchRoot(root), runDir) };
|
|
214
222
|
const header = metricsHeader();
|
|
215
|
-
const row = metricsRow(
|
|
223
|
+
const row = metricsRow(rowMetrics);
|
|
216
224
|
|
|
217
225
|
// Per-run file.
|
|
218
226
|
fs.writeFileSync(path.join(runDir, "metrics.csv"), header + "\n" + row + "\n");
|
|
@@ -222,7 +230,14 @@ export function writeMetrics({ runDir, root, metrics }) {
|
|
|
222
230
|
if (!fs.existsSync(indexPath)) {
|
|
223
231
|
fs.writeFileSync(indexPath, header + "\n" + row + "\n");
|
|
224
232
|
} else {
|
|
225
|
-
fs.
|
|
233
|
+
const lines = fs.readFileSync(indexPath, "utf8").trimEnd().split("\n");
|
|
234
|
+
const runPathIndex = lines[0].split(",").indexOf("run_path");
|
|
235
|
+
const existing = runPathIndex < 0
|
|
236
|
+
? -1
|
|
237
|
+
: lines.findIndex((line, index) => index > 0 && line.split(",")[runPathIndex] === rowMetrics.run_path);
|
|
238
|
+
if (existing >= 0) lines[existing] = row;
|
|
239
|
+
else lines.push(row);
|
|
240
|
+
fs.writeFileSync(indexPath, lines.join("\n") + "\n");
|
|
226
241
|
}
|
|
227
242
|
return { runMetrics: path.join(runDir, "metrics.csv"), index: indexPath };
|
|
228
243
|
}
|
|
@@ -286,15 +301,33 @@ export async function runCell({
|
|
|
286
301
|
wall = 0,
|
|
287
302
|
timeout = 60,
|
|
288
303
|
maxOutputChars = 16000,
|
|
304
|
+
runDir: existingRunDir = null,
|
|
305
|
+
sessionId: existingSessionId = null,
|
|
306
|
+
resume = false,
|
|
307
|
+
cell = null,
|
|
289
308
|
onStart = () => {},
|
|
290
309
|
onDone = () => {},
|
|
291
310
|
} = {}) {
|
|
292
|
-
const alloc =
|
|
311
|
+
const alloc = existingRunDir
|
|
312
|
+
? {
|
|
313
|
+
dir: path.resolve(existingRunDir),
|
|
314
|
+
workspace: path.join(path.resolve(existingRunDir), "workspace"),
|
|
315
|
+
root: benchRoot(root),
|
|
316
|
+
slug: benchSlug(model, reasoning),
|
|
317
|
+
runId: path.basename(path.resolve(existingRunDir)),
|
|
318
|
+
}
|
|
319
|
+
: allocateRun({ model, reasoning, root });
|
|
320
|
+
fs.mkdirSync(alloc.workspace, { recursive: true });
|
|
293
321
|
onStart({ ...alloc, model, reasoning });
|
|
294
322
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
323
|
+
let taskText = task || "";
|
|
324
|
+
if (!resume) {
|
|
325
|
+
const seeded = seedWorkspace(alloc.workspace, { taskPath, taskFile, taskText: task });
|
|
326
|
+
taskText = seeded.taskText || task || "";
|
|
327
|
+
fs.writeFileSync(path.join(alloc.dir, "task.md"), taskText);
|
|
328
|
+
} else if (!taskText) {
|
|
329
|
+
taskText = fs.existsSync(path.join(alloc.dir, "task.md")) ? fs.readFileSync(path.join(alloc.dir, "task.md"), "utf8") : "";
|
|
330
|
+
}
|
|
298
331
|
writeProjectMetadata(alloc.dir, projectMetadata);
|
|
299
332
|
|
|
300
333
|
const gw = new GatewayModel({
|
|
@@ -304,16 +337,40 @@ export async function runCell({
|
|
|
304
337
|
modelKwargs: reasoningKwargs(reasoning),
|
|
305
338
|
});
|
|
306
339
|
const env = new LocalEnvironment({ cwd: alloc.workspace, timeout, maxOutputChars });
|
|
340
|
+
const sessionId = existingSessionId || newSessionId();
|
|
307
341
|
const agent = new Agent(gw, env, {
|
|
308
342
|
mode: "autonomous",
|
|
309
343
|
stepLimit: steps,
|
|
310
344
|
wallTimeLimitSeconds: wall,
|
|
311
345
|
outputPath: path.join(alloc.dir, "trajectory.json"),
|
|
346
|
+
sessionId,
|
|
347
|
+
saveSession,
|
|
348
|
+
task: taskText,
|
|
349
|
+
bench: { dir: alloc.dir, workspace: alloc.workspace, root: alloc.root, task: taskText, taskPath, model, reasoning, steps, wall, timeout, segments: [] },
|
|
312
350
|
});
|
|
313
351
|
|
|
352
|
+
if (resume && existingSessionId) {
|
|
353
|
+
const saved = loadSession(existingSessionId);
|
|
354
|
+
agent.restore(saved);
|
|
355
|
+
agent.exitStatus = null;
|
|
356
|
+
if (Number(steps) > 0) agent.stepLimit = agent.nSteps + Number(steps);
|
|
357
|
+
if (Number(wall) > 0) agent.wallTimeLimitSeconds = Math.ceil((Date.now() - agent.startTime) / 1000) + Number(wall);
|
|
358
|
+
}
|
|
359
|
+
|
|
314
360
|
let error = null;
|
|
315
361
|
try {
|
|
316
|
-
|
|
362
|
+
if (resume && existingSessionId) {
|
|
363
|
+
while (true) {
|
|
364
|
+
if (agent.stepLimit > 0 && agent.nSteps >= agent.stepLimit) {
|
|
365
|
+
agent.exit("LimitsExceeded", "");
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
const turn = await agent.runTurn();
|
|
369
|
+
if (turn.kind === "exit") break;
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
await agent.run(taskText);
|
|
373
|
+
}
|
|
317
374
|
} catch (err) {
|
|
318
375
|
error = String(err?.message || err);
|
|
319
376
|
if (!agent.exitStatus) agent.exit("Error", "");
|
|
@@ -323,7 +380,7 @@ export async function runCell({
|
|
|
323
380
|
if (error) metrics.exit_status = metrics.exit_status || "Error";
|
|
324
381
|
const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
|
|
325
382
|
|
|
326
|
-
const result = { ...metrics, error, slug: alloc.slug, runId: alloc.runId, dir: alloc.dir, paths };
|
|
383
|
+
const result = { ...metrics, error, slug: alloc.slug, runId: alloc.runId, dir: alloc.dir, sessionId, cell, paths };
|
|
327
384
|
onDone(result);
|
|
328
385
|
return result;
|
|
329
386
|
}
|
package/src/cli.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
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
19
|
* astra bench --project ./task project-configured bench run
|
|
20
|
+
* astra bench --resume <session-id> continue the same bench attempt
|
|
20
21
|
* astra --resume <session-id> resume a saved session
|
|
21
22
|
* astra --sessions list saved sessions
|
|
22
23
|
*
|
|
@@ -103,6 +104,8 @@ import {
|
|
|
103
104
|
benchRoot,
|
|
104
105
|
} from "./bench.js";
|
|
105
106
|
import { refreshReport } from "./report.js";
|
|
107
|
+
import { runProjectBench } from "./project-bench.js";
|
|
108
|
+
import { loadLedger } from "./ledger.js";
|
|
106
109
|
|
|
107
110
|
/** Map a reasoning level to gateway modelKwargs. Empty for off/none/unset. */
|
|
108
111
|
function reasoningKwargs(level) {
|
|
@@ -186,7 +189,7 @@ async function main() {
|
|
|
186
189
|
|
|
187
190
|
// Load a session to resume (if any) to infer defaults.
|
|
188
191
|
let resumeDoc = null;
|
|
189
|
-
if (args.resume) {
|
|
192
|
+
if (args.resume && !args.project) {
|
|
190
193
|
if (!sessionExists(args.resume)) {
|
|
191
194
|
console.error(`\x1b[31m[astra] no such session: ${args.resume}\x1b[0m`);
|
|
192
195
|
process.exit(2);
|
|
@@ -198,6 +201,19 @@ async function main() {
|
|
|
198
201
|
// Bench mode uses the autonomous engine; agent mode uses the interactive one.
|
|
199
202
|
let task = args.task;
|
|
200
203
|
if (args["task-file"]) task = fs.readFileSync(args["task-file"], "utf8");
|
|
204
|
+
let resumeBench = resumeDoc?.bench ?? null;
|
|
205
|
+
if (args.bench && args.resume && !resumeBench) {
|
|
206
|
+
console.error("\x1b[31m[astra] request_error: this session is not a bench run.\x1b[0m");
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
if (resumeBench) {
|
|
210
|
+
if (!args._provided.has("task") && !args._provided.has("task-file")) task = resumeBench.task;
|
|
211
|
+
if (!args._provided.has("path")) args.path = resumeBench.taskPath;
|
|
212
|
+
if (!args._provided.has("bench-root")) args["bench-root"] = resumeBench.root;
|
|
213
|
+
if (!args._provided.has("steps")) args.steps = resumeBench.steps;
|
|
214
|
+
if (!args._provided.has("wall")) args.wall = resumeBench.wall;
|
|
215
|
+
if (!args._provided.has("timeout")) args.timeout = resumeBench.timeout;
|
|
216
|
+
}
|
|
201
217
|
let project = null;
|
|
202
218
|
if (args.project) {
|
|
203
219
|
try {
|
|
@@ -249,6 +265,51 @@ async function main() {
|
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
267
|
|
|
268
|
+
if (project) {
|
|
269
|
+
try {
|
|
270
|
+
let resumeCell = null;
|
|
271
|
+
if (args.resume) {
|
|
272
|
+
const ledgerPath = path.join(project.outputRoot, "benchmark.json");
|
|
273
|
+
if (fs.existsSync(ledgerPath)) {
|
|
274
|
+
const ledger = loadLedger(ledgerPath);
|
|
275
|
+
resumeCell = Object.values(ledger.cells).find((cell) => cell.sessionId === args.resume) || null;
|
|
276
|
+
}
|
|
277
|
+
if (!resumeCell) {
|
|
278
|
+
throw new Error(`no project benchmark cell found for resume session ${args.resume}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const effectiveProject = {
|
|
282
|
+
...project,
|
|
283
|
+
outputRoot: args._provided.has("bench-root") ? path.resolve(args["bench-root"]) : project.outputRoot,
|
|
284
|
+
bench: {
|
|
285
|
+
...project.bench,
|
|
286
|
+
models: args._provided.has("model") ? String(args.model).split(",").map((value) => value.trim()).filter(Boolean) : resumeCell ? [resumeCell.model] : project.bench.models,
|
|
287
|
+
reasoning: args._provided.has("reasoning") ? String(args.reasoning).split(",").map((value) => value.trim()).filter(Boolean) : resumeCell ? [resumeCell.reasoning] : project.bench.reasoning,
|
|
288
|
+
repeat: args._provided.has("repeat") ? Math.max(1, Number(args.repeat) || 1) : resumeCell ? resumeCell.repeatIndex : project.bench.repeat,
|
|
289
|
+
steps: args._provided.has("steps") ? Number(args.steps) : project.bench.steps,
|
|
290
|
+
wall: args._provided.has("wall") ? Number(args.wall) : project.bench.wall,
|
|
291
|
+
timeout: args._provided.has("timeout") ? Number(args.timeout) : project.bench.timeout,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
const results = await runProjectBench({
|
|
295
|
+
project: effectiveProject,
|
|
296
|
+
apiKey,
|
|
297
|
+
baseUrl: args["base-url"],
|
|
298
|
+
resume: Boolean(args.resume),
|
|
299
|
+
resumeSessionId: args.resume || null,
|
|
300
|
+
quiet: !!args.quiet,
|
|
301
|
+
onCell: ({ phase, cell }) => {
|
|
302
|
+
if (!args.quiet) console.error(`\x1b[2m[astra] ${phase} · ${cell.displayKey} repeat=${cell.repeatIndex}\x1b[0m`);
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
console.error(`\x1b[1m[astra] project bench complete · ${results.length} cell(s) processed\x1b[0m`);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error(`\x1b[31m[astra] project bench failed: ${error.message}\x1b[0m`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
252
313
|
// -------------------- MODEL + REASONING RESOLUTION --------------------
|
|
253
314
|
// Bench (autonomous) mode: an explicit --model is required so runs are
|
|
254
315
|
// reproducible and never silently pick a cached/default model.
|
|
@@ -261,7 +322,7 @@ async function main() {
|
|
|
261
322
|
// 5. hard fallback: glm-5.2 with medium reasoning
|
|
262
323
|
// The chosen values are cached so the next `astra` needs no flags.
|
|
263
324
|
let modelId = args.model || resumeDoc?.info?.model;
|
|
264
|
-
let reasoning = args.reasoning || "";
|
|
325
|
+
let reasoning = args.reasoning || resumeBench?.reasoning || "";
|
|
265
326
|
|
|
266
327
|
if (mode === "autonomous") {
|
|
267
328
|
if (project && !modelId) modelId = project.bench.models.join(",");
|
|
@@ -330,7 +391,21 @@ async function main() {
|
|
|
330
391
|
// workspace under bench/<model-name-reasoning>/run-NN/. The agent's commands
|
|
331
392
|
// run inside that workspace and metrics are recorded when it finishes.
|
|
332
393
|
let benchRun = null;
|
|
333
|
-
if (mode === "autonomous" &&
|
|
394
|
+
if (mode === "autonomous" && resumeBench) {
|
|
395
|
+
if (!fs.existsSync(resumeBench.dir) || !fs.existsSync(resumeBench.workspace)) {
|
|
396
|
+
console.error("\x1b[31m[astra] request_error: saved bench workspace is missing.\x1b[0m");
|
|
397
|
+
process.exit(2);
|
|
398
|
+
}
|
|
399
|
+
benchRun = {
|
|
400
|
+
dir: resumeBench.dir,
|
|
401
|
+
workspace: resumeBench.workspace,
|
|
402
|
+
root: resumeBench.root,
|
|
403
|
+
slug: path.basename(path.dirname(resumeBench.dir)),
|
|
404
|
+
runId: path.basename(resumeBench.dir),
|
|
405
|
+
};
|
|
406
|
+
resumeBench.segments = Array.isArray(resumeBench.segments) ? resumeBench.segments : [];
|
|
407
|
+
resumeBench.segments.push({ started_at: new Date().toISOString(), resumed: true });
|
|
408
|
+
} else if (mode === "autonomous" && !resumeDoc) {
|
|
334
409
|
benchRun = allocateRun({ model: modelId, reasoning, root: args["bench-root"] });
|
|
335
410
|
const seeded = seedWorkspace(benchRun.workspace, {
|
|
336
411
|
taskPath: args.path,
|
|
@@ -341,9 +416,22 @@ async function main() {
|
|
|
341
416
|
// Persist the task text alongside the run for reproducibility.
|
|
342
417
|
fs.writeFileSync(path.join(benchRun.dir, "task.md"), task || "");
|
|
343
418
|
writeProjectMetadata(benchRun.dir, project?.metadata);
|
|
419
|
+
resumeBench = {
|
|
420
|
+
dir: benchRun.dir,
|
|
421
|
+
workspace: benchRun.workspace,
|
|
422
|
+
root: benchRun.root,
|
|
423
|
+
task,
|
|
424
|
+
taskPath: args.path,
|
|
425
|
+
model: modelId,
|
|
426
|
+
reasoning,
|
|
427
|
+
steps: Number(args.steps),
|
|
428
|
+
wall: Number(args.wall),
|
|
429
|
+
timeout: Number(args.timeout),
|
|
430
|
+
segments: [{ started_at: new Date().toISOString(), resumed: false }],
|
|
431
|
+
};
|
|
344
432
|
}
|
|
345
433
|
|
|
346
|
-
const cmdCwd = benchRun
|
|
434
|
+
const cmdCwd = benchRun?.workspace || cwd;
|
|
347
435
|
const env = new LocalEnvironment({
|
|
348
436
|
cwd: cmdCwd,
|
|
349
437
|
timeout: Number(args.timeout),
|
|
@@ -362,12 +450,23 @@ async function main() {
|
|
|
362
450
|
: null,
|
|
363
451
|
sessionId,
|
|
364
452
|
saveSession,
|
|
453
|
+
bench: resumeBench,
|
|
365
454
|
onEvent: quiet || mode === "interactive" ? () => {} : (msg) => printEvent(msg),
|
|
366
455
|
onStep: quiet || mode === "interactive" ? () => {} : (s) => printStep(s),
|
|
367
456
|
});
|
|
368
457
|
|
|
369
458
|
if (resumeDoc) {
|
|
370
459
|
agent.restore(resumeDoc);
|
|
460
|
+
if (resumeBench) {
|
|
461
|
+
agent.exitStatus = null;
|
|
462
|
+
if (!args._provided.has("steps") && Number(resumeBench.steps) > 0) {
|
|
463
|
+
agent.stepLimit = agent.nSteps + Number(resumeBench.steps);
|
|
464
|
+
}
|
|
465
|
+
if (!args._provided.has("wall") && Number(resumeBench.wall) > 0) {
|
|
466
|
+
agent.wallTimeLimitSeconds =
|
|
467
|
+
Math.ceil((Date.now() - agent.startTime) / 1000) + Number(resumeBench.wall);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
371
470
|
console.error(`\x1b[2m[astra] resumed ${sessionId} (${agent.messages.length} messages)\x1b[0m`);
|
|
372
471
|
}
|
|
373
472
|
|
|
@@ -423,6 +522,7 @@ async function main() {
|
|
|
423
522
|
`${Number(args.steps) > 0 ? ` turns<=${args.steps}` : ""}\x1b[0m`
|
|
424
523
|
);
|
|
425
524
|
console.error(`\x1b[2m[astra] workspace -> ${benchRun.workspace}\x1b[0m`);
|
|
525
|
+
console.error(`\x1b[2m[astra] session -> ${sessionId}\x1b[0m`);
|
|
426
526
|
} else {
|
|
427
527
|
console.error(
|
|
428
528
|
`\x1b[2m[astra] bench · model=${modelId} cwd=${cmdCwd}` +
|
|
@@ -431,12 +531,38 @@ async function main() {
|
|
|
431
531
|
}
|
|
432
532
|
}
|
|
433
533
|
|
|
434
|
-
|
|
435
|
-
|
|
534
|
+
let result;
|
|
535
|
+
try {
|
|
536
|
+
result = resumeDoc ? await continueAutonomous(agent) : await agent.run(task);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
agent.exitStatus = "Error";
|
|
539
|
+
const segment = agent.bench?.segments?.at(-1);
|
|
540
|
+
if (segment) {
|
|
541
|
+
segment.finished_at = new Date().toISOString();
|
|
542
|
+
segment.exit_status = "Error";
|
|
543
|
+
segment.error = String(error?.message || error);
|
|
544
|
+
}
|
|
545
|
+
agent.save();
|
|
546
|
+
if (benchRun) {
|
|
547
|
+
writeMetrics({
|
|
548
|
+
runDir: benchRun.dir,
|
|
549
|
+
root: benchRun.root,
|
|
550
|
+
metrics: collectMetrics({ agent, model, reasoning }),
|
|
551
|
+
});
|
|
552
|
+
try { refreshReport(benchRun.root); } catch {}
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
436
556
|
agent.save();
|
|
437
557
|
|
|
438
558
|
// Record benchmark metrics (per-run CSV + rolled-up index).
|
|
439
559
|
if (benchRun) {
|
|
560
|
+
const segment = agent.bench?.segments?.at(-1);
|
|
561
|
+
if (segment) {
|
|
562
|
+
segment.finished_at = new Date().toISOString();
|
|
563
|
+
segment.exit_status = result.exit_status;
|
|
564
|
+
}
|
|
565
|
+
agent.save();
|
|
440
566
|
const metrics = collectMetrics({ agent, model, reasoning });
|
|
441
567
|
const paths = writeMetrics({ runDir: benchRun.dir, root: benchRun.root, metrics });
|
|
442
568
|
if (!quiet) {
|
package/src/ledger.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const LEDGER_VERSION = 1;
|
|
5
|
+
const STALE_AFTER_MS = 10 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
function slug(value) {
|
|
8
|
+
return String(value ?? "none").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "none";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function atomicWrite(file, value) {
|
|
12
|
+
const temp = `${file}.tmp-${process.pid}`;
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n");
|
|
15
|
+
fs.renameSync(temp, file);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadLedger(file) {
|
|
19
|
+
if (!fs.existsSync(file)) return { version: LEDGER_VERSION, cells: {}, path: file };
|
|
20
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
21
|
+
if (value.version !== LEDGER_VERSION || !value.cells || typeof value.cells !== "object") throw new Error("invalid benchmark ledger");
|
|
22
|
+
return { ...value, path: file };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nextRunPath(ledger, cell) {
|
|
26
|
+
const prefix = `${slug(cell.displayKey)}/run-`;
|
|
27
|
+
const used = Object.values(ledger.cells).filter((item) => item.runPath?.startsWith(prefix));
|
|
28
|
+
const next = used.reduce((max, item) => Math.max(max, Number(item.runPath.slice(prefix.length)) || 0), 0) + 1;
|
|
29
|
+
return `${prefix}${String(next).padStart(2, "0")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function claimCell(ledger, cell) {
|
|
33
|
+
const existing = ledger.cells[cell.cellKey];
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
const record = {
|
|
36
|
+
...cell,
|
|
37
|
+
status: "pending",
|
|
38
|
+
runPath: nextRunPath(ledger, cell),
|
|
39
|
+
createdAt: new Date().toISOString(),
|
|
40
|
+
leaseAt: null,
|
|
41
|
+
};
|
|
42
|
+
ledger.cells[cell.cellKey] = record;
|
|
43
|
+
atomicWrite(ledger.path, { version: LEDGER_VERSION, cells: ledger.cells });
|
|
44
|
+
return record;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function transitionCell(ledger, cellKey, status, patch = {}) {
|
|
48
|
+
const cell = ledger.cells[cellKey];
|
|
49
|
+
if (!cell) throw new Error(`unknown benchmark cell: ${cellKey}`);
|
|
50
|
+
const next = { ...cell, ...patch, status, updatedAt: new Date().toISOString() };
|
|
51
|
+
if (["generating", "verifying"].includes(status) && patch.leaseAt === undefined) next.leaseAt = next.updatedAt;
|
|
52
|
+
ledger.cells[cellKey] = next;
|
|
53
|
+
atomicWrite(ledger.path, { version: LEDGER_VERSION, cells: ledger.cells });
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function selectWork(ledger, desiredCells, now = Date.now(), { force = false, sessionId = null } = {}) {
|
|
58
|
+
return desiredCells.filter((cell) => {
|
|
59
|
+
const existing = ledger.cells[cell.cellKey];
|
|
60
|
+
if (!existing) return true;
|
|
61
|
+
if (["completed", "failed"].includes(existing.status)) return false;
|
|
62
|
+
if (sessionId && existing.sessionId !== sessionId) return false;
|
|
63
|
+
if (["generating", "verifying"].includes(existing.status)) {
|
|
64
|
+
return force || !existing.leaseAt || now - Date.parse(existing.leaseAt) > STALE_AFTER_MS;
|
|
65
|
+
}
|
|
66
|
+
return !sessionId || existing.sessionId === sessionId;
|
|
67
|
+
});
|
|
68
|
+
}
|
package/src/model.js
CHANGED
|
@@ -89,9 +89,9 @@ export class GatewayModel {
|
|
|
89
89
|
const body = {
|
|
90
90
|
model: this.model,
|
|
91
91
|
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
92
|
-
[this._tokenParam]: this.maxTokens,
|
|
93
92
|
...this.modelKwargs,
|
|
94
93
|
};
|
|
94
|
+
if (this._tokenParam) body[this._tokenParam] = this.maxTokens;
|
|
95
95
|
|
|
96
96
|
let lastErr;
|
|
97
97
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
@@ -226,6 +226,13 @@ export class GatewayModel {
|
|
|
226
226
|
this._tokenParam = "max_tokens";
|
|
227
227
|
return true;
|
|
228
228
|
}
|
|
229
|
+
// Some gateway routes reject either token-cap parameter. Retry once
|
|
230
|
+
// without one rather than making the model unavailable for benchmarking.
|
|
231
|
+
if (/max_tokens/.test(errText) && /not supported|unsupported/.test(errText) && "max_tokens" in body) {
|
|
232
|
+
delete body.max_tokens;
|
|
233
|
+
this._tokenParam = null;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
229
236
|
return false;
|
|
230
237
|
}
|
|
231
238
|
}
|