firegoat 0.30.0 → 0.31.0
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 +1 -1
- package/dist/cli.js +423 -208
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ So far we've got the CLI. It lets anyone use a simplified version of our product
|
|
|
8
8
|
|
|
9
9
|
## What you get
|
|
10
10
|
|
|
11
|
-
When you run `firegoat eval`, you get visibility into how agents use your product: whether they fail or succeed, and how much time they take doing it. You can point them at your local codebase or at your cloud product, and see how Claude acts on the simple tasks your product aims to do.
|
|
11
|
+
When you run `firegoat eval`, you get visibility into how agents use your product: whether they fail or succeed, and how much time and how many tokens they take doing it. You can point them at your local codebase or at your cloud product, and see how Claude acts on the simple tasks your product aims to do. Give harder tests more `reward_points` and eval tells you the tokens spent per point earned, per harness and model.
|
|
12
12
|
|
|
13
13
|
## Start
|
|
14
14
|
|
package/dist/cli.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6947
6947
|
// package.json
|
|
6948
6948
|
var package_default = {
|
|
6949
6949
|
name: "firegoat",
|
|
6950
|
-
version: "0.
|
|
6950
|
+
version: "0.31.0",
|
|
6951
6951
|
repository: {
|
|
6952
6952
|
type: "git",
|
|
6953
6953
|
url: "git+https://github.com/firegoat-ai/firegoat-cli.git"
|
|
@@ -7039,6 +7039,230 @@ var $visitAsync = visit.visitAsync;
|
|
|
7039
7039
|
import { spawnSync } from "node:child_process";
|
|
7040
7040
|
import { existsSync, readdirSync } from "node:fs";
|
|
7041
7041
|
import { join } from "node:path";
|
|
7042
|
+
|
|
7043
|
+
// src/process.ts
|
|
7044
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7045
|
+
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7046
|
+
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
7047
|
+
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7048
|
+
function hostEnv() {
|
|
7049
|
+
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7050
|
+
}
|
|
7051
|
+
var GRACE_MS = 5000;
|
|
7052
|
+
var liveGroups = new Set;
|
|
7053
|
+
var signalsHooked = false;
|
|
7054
|
+
function hookSignals() {
|
|
7055
|
+
if (signalsHooked)
|
|
7056
|
+
return;
|
|
7057
|
+
signalsHooked = true;
|
|
7058
|
+
const onSignal = (signal) => {
|
|
7059
|
+
for (const g of liveGroups)
|
|
7060
|
+
try {
|
|
7061
|
+
process.kill(-g, "SIGKILL");
|
|
7062
|
+
} catch {}
|
|
7063
|
+
process.kill(process.pid, signal);
|
|
7064
|
+
};
|
|
7065
|
+
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
7066
|
+
process.once(s, onSignal);
|
|
7067
|
+
}
|
|
7068
|
+
function forgetGroup(pgid) {
|
|
7069
|
+
liveGroups.delete(pgid);
|
|
7070
|
+
}
|
|
7071
|
+
function processTree(pid) {
|
|
7072
|
+
const children = new Map;
|
|
7073
|
+
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
7074
|
+
encoding: "utf8"
|
|
7075
|
+
});
|
|
7076
|
+
for (const line of ps.split(`
|
|
7077
|
+
`)) {
|
|
7078
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
7079
|
+
if (!child || parent === undefined)
|
|
7080
|
+
continue;
|
|
7081
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
7082
|
+
}
|
|
7083
|
+
const tree = [pid];
|
|
7084
|
+
for (let i = 0;i < tree.length; i++)
|
|
7085
|
+
tree.push(...children.get(tree[i]) ?? []);
|
|
7086
|
+
return tree;
|
|
7087
|
+
}
|
|
7088
|
+
function killTree(pid, group) {
|
|
7089
|
+
if (group)
|
|
7090
|
+
try {
|
|
7091
|
+
process.kill(-pid, "SIGKILL");
|
|
7092
|
+
} catch {}
|
|
7093
|
+
let tree = [pid];
|
|
7094
|
+
let problem;
|
|
7095
|
+
try {
|
|
7096
|
+
tree = processTree(pid);
|
|
7097
|
+
} catch (e) {
|
|
7098
|
+
problem = e.message;
|
|
7099
|
+
}
|
|
7100
|
+
for (const p of tree)
|
|
7101
|
+
try {
|
|
7102
|
+
process.kill(p, "SIGKILL");
|
|
7103
|
+
} catch {}
|
|
7104
|
+
return problem;
|
|
7105
|
+
}
|
|
7106
|
+
function run(opts) {
|
|
7107
|
+
return new Promise((resolve) => {
|
|
7108
|
+
const detached = opts.detached === true;
|
|
7109
|
+
if (detached)
|
|
7110
|
+
hookSignals();
|
|
7111
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
7112
|
+
cwd: opts.cwd,
|
|
7113
|
+
env: opts.env,
|
|
7114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7115
|
+
detached
|
|
7116
|
+
});
|
|
7117
|
+
if (child.pid !== undefined) {
|
|
7118
|
+
if (detached)
|
|
7119
|
+
liveGroups.add(child.pid);
|
|
7120
|
+
opts.onSpawn?.(child.pid);
|
|
7121
|
+
}
|
|
7122
|
+
let timedOut = false;
|
|
7123
|
+
let settled = false;
|
|
7124
|
+
const timer = setTimeout(() => {
|
|
7125
|
+
timedOut = true;
|
|
7126
|
+
if (child.pid === undefined)
|
|
7127
|
+
return;
|
|
7128
|
+
const problem = killTree(child.pid, detached);
|
|
7129
|
+
if (problem)
|
|
7130
|
+
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
7131
|
+
`));
|
|
7132
|
+
}, opts.timeoutMs);
|
|
7133
|
+
const done = (r) => {
|
|
7134
|
+
if (settled)
|
|
7135
|
+
return;
|
|
7136
|
+
settled = true;
|
|
7137
|
+
clearTimeout(timer);
|
|
7138
|
+
resolve(r);
|
|
7139
|
+
};
|
|
7140
|
+
child.stdout.on("data", opts.onStdout);
|
|
7141
|
+
child.stderr.on("data", opts.onStderr);
|
|
7142
|
+
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
7143
|
+
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7144
|
+
child.on("close", (code) => done({ code, timedOut }));
|
|
7145
|
+
child.on("exit", (code) => setTimeout(() => {
|
|
7146
|
+
child.stdout.destroy();
|
|
7147
|
+
child.stderr.destroy();
|
|
7148
|
+
done({ code, timedOut });
|
|
7149
|
+
}, 2000).unref());
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7152
|
+
async function runShell(opts) {
|
|
7153
|
+
opts.log.write(`$ ${opts.run}
|
|
7154
|
+
`);
|
|
7155
|
+
const r = await run({
|
|
7156
|
+
cmd: "/bin/sh",
|
|
7157
|
+
args: ["-c", opts.run],
|
|
7158
|
+
cwd: opts.cwd,
|
|
7159
|
+
env: opts.env,
|
|
7160
|
+
timeoutMs: opts.timeoutMs,
|
|
7161
|
+
onStdout: (c) => opts.log.write(c),
|
|
7162
|
+
onStderr: (c) => opts.log.write(c)
|
|
7163
|
+
});
|
|
7164
|
+
let error;
|
|
7165
|
+
if (r.error)
|
|
7166
|
+
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7167
|
+
else if (r.timedOut)
|
|
7168
|
+
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7169
|
+
else if (r.code === null)
|
|
7170
|
+
error = "killed by a signal";
|
|
7171
|
+
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7172
|
+
|
|
7173
|
+
`);
|
|
7174
|
+
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7175
|
+
}
|
|
7176
|
+
|
|
7177
|
+
// src/harness.ts
|
|
7178
|
+
function tokens(parts = {
|
|
7179
|
+
input: null,
|
|
7180
|
+
output: null,
|
|
7181
|
+
cacheRead: null,
|
|
7182
|
+
cacheWrite: null
|
|
7183
|
+
}) {
|
|
7184
|
+
const { input, output, cacheRead, cacheWrite } = parts;
|
|
7185
|
+
const total = input === null || output === null || cacheRead === null || cacheWrite === null ? null : input + output + cacheRead + cacheWrite;
|
|
7186
|
+
return { input, output, cacheRead, cacheWrite, total };
|
|
7187
|
+
}
|
|
7188
|
+
function modelUsageTokens(modelUsage) {
|
|
7189
|
+
if (typeof modelUsage !== "object" || modelUsage === null)
|
|
7190
|
+
return tokens();
|
|
7191
|
+
const entries = Object.values(modelUsage);
|
|
7192
|
+
if (entries.length === 0)
|
|
7193
|
+
return tokens();
|
|
7194
|
+
const sum = (key) => {
|
|
7195
|
+
let n = 0;
|
|
7196
|
+
for (const e of entries) {
|
|
7197
|
+
const v = e?.[key];
|
|
7198
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0)
|
|
7199
|
+
return null;
|
|
7200
|
+
n += v;
|
|
7201
|
+
}
|
|
7202
|
+
return n;
|
|
7203
|
+
};
|
|
7204
|
+
return tokens({
|
|
7205
|
+
input: sum("inputTokens"),
|
|
7206
|
+
output: sum("outputTokens"),
|
|
7207
|
+
cacheRead: sum("cacheReadInputTokens"),
|
|
7208
|
+
cacheWrite: sum("cacheCreationInputTokens")
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
async function runAgent(harness, opts) {
|
|
7212
|
+
let stdout = "";
|
|
7213
|
+
let pending = "";
|
|
7214
|
+
let group;
|
|
7215
|
+
let markFinal = () => {};
|
|
7216
|
+
const finalSeen = new Promise((r) => {
|
|
7217
|
+
markFinal = r;
|
|
7218
|
+
});
|
|
7219
|
+
const exited = await run({
|
|
7220
|
+
cmd: opts.bin,
|
|
7221
|
+
args: harness.args(opts),
|
|
7222
|
+
cwd: opts.cwd,
|
|
7223
|
+
env: opts.env,
|
|
7224
|
+
timeoutMs: opts.timeoutMs,
|
|
7225
|
+
detached: true,
|
|
7226
|
+
finalSeen,
|
|
7227
|
+
onSpawn: (pid) => {
|
|
7228
|
+
group = pid;
|
|
7229
|
+
},
|
|
7230
|
+
onStdout: (c) => {
|
|
7231
|
+
const text = c.toString();
|
|
7232
|
+
stdout += text;
|
|
7233
|
+
pending += text;
|
|
7234
|
+
const lines = pending.split(`
|
|
7235
|
+
`);
|
|
7236
|
+
pending = lines.pop() ?? "";
|
|
7237
|
+
for (const line of lines) {
|
|
7238
|
+
if (harness.isFinal(line))
|
|
7239
|
+
markFinal();
|
|
7240
|
+
const p = opts.onProgress && harness.progress(line);
|
|
7241
|
+
if (p)
|
|
7242
|
+
opts.onProgress?.(p);
|
|
7243
|
+
}
|
|
7244
|
+
},
|
|
7245
|
+
onStderr: (c) => opts.stderr.write(c)
|
|
7246
|
+
});
|
|
7247
|
+
if (exited.error)
|
|
7248
|
+
return {
|
|
7249
|
+
group,
|
|
7250
|
+
outcome: {
|
|
7251
|
+
ok: false,
|
|
7252
|
+
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
7253
|
+
steps: [],
|
|
7254
|
+
final: "",
|
|
7255
|
+
reportedModel: null,
|
|
7256
|
+
version: null,
|
|
7257
|
+
turns: null,
|
|
7258
|
+
cost: null,
|
|
7259
|
+
tokens: tokens()
|
|
7260
|
+
}
|
|
7261
|
+
};
|
|
7262
|
+
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
// src/harnesses/claude.ts
|
|
7042
7266
|
var FLAGS = [
|
|
7043
7267
|
"--output-format",
|
|
7044
7268
|
"stream-json",
|
|
@@ -7050,6 +7274,17 @@ var FLAGS = [
|
|
|
7050
7274
|
"--disable-slash-commands"
|
|
7051
7275
|
];
|
|
7052
7276
|
var SET = "firegoat sets it for every run";
|
|
7277
|
+
function sumTurns(results) {
|
|
7278
|
+
if (results.length === 0)
|
|
7279
|
+
return null;
|
|
7280
|
+
let n = 0;
|
|
7281
|
+
for (const r of results) {
|
|
7282
|
+
if (typeof r.num_turns !== "number")
|
|
7283
|
+
return null;
|
|
7284
|
+
n += r.num_turns;
|
|
7285
|
+
}
|
|
7286
|
+
return n;
|
|
7287
|
+
}
|
|
7053
7288
|
function parse(line) {
|
|
7054
7289
|
try {
|
|
7055
7290
|
const e = JSON.parse(line);
|
|
@@ -7105,12 +7340,14 @@ function read(stdout, exited, timeoutMs) {
|
|
|
7105
7340
|
for (const e of events)
|
|
7106
7341
|
if (e?.type === "result" && typeof e.result === "string")
|
|
7107
7342
|
final = e.result;
|
|
7108
|
-
const
|
|
7343
|
+
const results = events.filter((e) => e?.type === "result");
|
|
7344
|
+
const result = results.at(-1);
|
|
7109
7345
|
const trace = {
|
|
7110
7346
|
steps: steps(events),
|
|
7111
7347
|
final,
|
|
7112
|
-
turns:
|
|
7113
|
-
cost: result?.total_cost_usd ?? null
|
|
7348
|
+
turns: sumTurns(results),
|
|
7349
|
+
cost: result?.total_cost_usd ?? null,
|
|
7350
|
+
tokens: modelUsageTokens(result?.modelUsage)
|
|
7114
7351
|
};
|
|
7115
7352
|
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7116
7353
|
if (!init) {
|
|
@@ -7269,142 +7506,6 @@ import { execFile } from "node:child_process";
|
|
|
7269
7506
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7270
7507
|
import { join as join2 } from "node:path";
|
|
7271
7508
|
import { promisify } from "node:util";
|
|
7272
|
-
|
|
7273
|
-
// src/process.ts
|
|
7274
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
7275
|
-
var NPM_INJECTED = /^(npm_config_|npm_lifecycle_|npm_package_)|^(npm_execpath|npm_node_execpath|npm_command|INIT_CWD)$/;
|
|
7276
|
-
var CLAUDE_SESSION = /^(CLAUDECODE|CLAUDE_CODE_.+|CLAUDE_PID|CLAUDE_ENV_FILE|CLAUDE_SESSION_ID|CLAUDE_PROJECT_DIR|CLAUDE_EFFORT|CLAUDE_AGENT_SDK_.+|CLAUDE_PLUGIN_ROOT|CLAUDE_PLUGIN_DATA|CLAUDE_SKILL_DIR)$/;
|
|
7277
|
-
var CLAUDE_AUTH = /^CLAUDE_CODE_(OAUTH_TOKEN|OAUTH_REFRESH_TOKEN|USE_[A-Z_]+|SKIP_[A-Z]+_AUTH|CLIENT_CERT|CLIENT_KEY|CLIENT_KEY_PASSPHRASE)$/;
|
|
7278
|
-
function hostEnv() {
|
|
7279
|
-
return Object.fromEntries(Object.entries(process.env).filter(([k]) => !NPM_INJECTED.test(k) && !k.startsWith("FIREGOAT_") && (!CLAUDE_SESSION.test(k) || CLAUDE_AUTH.test(k))));
|
|
7280
|
-
}
|
|
7281
|
-
var GRACE_MS = 5000;
|
|
7282
|
-
var liveGroups = new Set;
|
|
7283
|
-
var signalsHooked = false;
|
|
7284
|
-
function hookSignals() {
|
|
7285
|
-
if (signalsHooked)
|
|
7286
|
-
return;
|
|
7287
|
-
signalsHooked = true;
|
|
7288
|
-
const onSignal = (signal) => {
|
|
7289
|
-
for (const g of liveGroups)
|
|
7290
|
-
try {
|
|
7291
|
-
process.kill(-g, "SIGKILL");
|
|
7292
|
-
} catch {}
|
|
7293
|
-
process.kill(process.pid, signal);
|
|
7294
|
-
};
|
|
7295
|
-
for (const s of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
7296
|
-
process.once(s, onSignal);
|
|
7297
|
-
}
|
|
7298
|
-
function forgetGroup(pgid) {
|
|
7299
|
-
liveGroups.delete(pgid);
|
|
7300
|
-
}
|
|
7301
|
-
function processTree(pid) {
|
|
7302
|
-
const children = new Map;
|
|
7303
|
-
const ps = execFileSync("ps", ["-A", "-o", "pid=,ppid="], {
|
|
7304
|
-
encoding: "utf8"
|
|
7305
|
-
});
|
|
7306
|
-
for (const line of ps.split(`
|
|
7307
|
-
`)) {
|
|
7308
|
-
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
7309
|
-
if (!child || parent === undefined)
|
|
7310
|
-
continue;
|
|
7311
|
-
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
7312
|
-
}
|
|
7313
|
-
const tree = [pid];
|
|
7314
|
-
for (let i = 0;i < tree.length; i++)
|
|
7315
|
-
tree.push(...children.get(tree[i]) ?? []);
|
|
7316
|
-
return tree;
|
|
7317
|
-
}
|
|
7318
|
-
function killTree(pid, group) {
|
|
7319
|
-
if (group)
|
|
7320
|
-
try {
|
|
7321
|
-
process.kill(-pid, "SIGKILL");
|
|
7322
|
-
} catch {}
|
|
7323
|
-
let tree = [pid];
|
|
7324
|
-
let problem;
|
|
7325
|
-
try {
|
|
7326
|
-
tree = processTree(pid);
|
|
7327
|
-
} catch (e) {
|
|
7328
|
-
problem = e.message;
|
|
7329
|
-
}
|
|
7330
|
-
for (const p of tree)
|
|
7331
|
-
try {
|
|
7332
|
-
process.kill(p, "SIGKILL");
|
|
7333
|
-
} catch {}
|
|
7334
|
-
return problem;
|
|
7335
|
-
}
|
|
7336
|
-
function run(opts) {
|
|
7337
|
-
return new Promise((resolve) => {
|
|
7338
|
-
const detached = opts.detached === true;
|
|
7339
|
-
if (detached)
|
|
7340
|
-
hookSignals();
|
|
7341
|
-
const child = spawn(opts.cmd, opts.args, {
|
|
7342
|
-
cwd: opts.cwd,
|
|
7343
|
-
env: opts.env,
|
|
7344
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
7345
|
-
detached
|
|
7346
|
-
});
|
|
7347
|
-
if (child.pid !== undefined) {
|
|
7348
|
-
if (detached)
|
|
7349
|
-
liveGroups.add(child.pid);
|
|
7350
|
-
opts.onSpawn?.(child.pid);
|
|
7351
|
-
}
|
|
7352
|
-
let timedOut = false;
|
|
7353
|
-
let settled = false;
|
|
7354
|
-
const timer = setTimeout(() => {
|
|
7355
|
-
timedOut = true;
|
|
7356
|
-
if (child.pid === undefined)
|
|
7357
|
-
return;
|
|
7358
|
-
const problem = killTree(child.pid, detached);
|
|
7359
|
-
if (problem)
|
|
7360
|
-
opts.onStderr(Buffer.from(`firegoat: timed out; could not list ${opts.cmd}'s child processes (${problem}), killed only ${opts.cmd}
|
|
7361
|
-
`));
|
|
7362
|
-
}, opts.timeoutMs);
|
|
7363
|
-
const done = (r) => {
|
|
7364
|
-
if (settled)
|
|
7365
|
-
return;
|
|
7366
|
-
settled = true;
|
|
7367
|
-
clearTimeout(timer);
|
|
7368
|
-
resolve(r);
|
|
7369
|
-
};
|
|
7370
|
-
child.stdout.on("data", opts.onStdout);
|
|
7371
|
-
child.stderr.on("data", opts.onStderr);
|
|
7372
|
-
opts.finalSeen?.then(() => setTimeout(() => done({ code: null, timedOut: false, ended: true }), GRACE_MS).unref());
|
|
7373
|
-
child.on("error", (error) => done({ code: null, timedOut, error }));
|
|
7374
|
-
child.on("close", (code) => done({ code, timedOut }));
|
|
7375
|
-
child.on("exit", (code) => setTimeout(() => {
|
|
7376
|
-
child.stdout.destroy();
|
|
7377
|
-
child.stderr.destroy();
|
|
7378
|
-
done({ code, timedOut });
|
|
7379
|
-
}, 2000).unref());
|
|
7380
|
-
});
|
|
7381
|
-
}
|
|
7382
|
-
async function runShell(opts) {
|
|
7383
|
-
opts.log.write(`$ ${opts.run}
|
|
7384
|
-
`);
|
|
7385
|
-
const r = await run({
|
|
7386
|
-
cmd: "/bin/sh",
|
|
7387
|
-
args: ["-c", opts.run],
|
|
7388
|
-
cwd: opts.cwd,
|
|
7389
|
-
env: opts.env,
|
|
7390
|
-
timeoutMs: opts.timeoutMs,
|
|
7391
|
-
onStdout: (c) => opts.log.write(c),
|
|
7392
|
-
onStderr: (c) => opts.log.write(c)
|
|
7393
|
-
});
|
|
7394
|
-
let error;
|
|
7395
|
-
if (r.error)
|
|
7396
|
-
error = `could not start /bin/sh: ${r.error.message}`;
|
|
7397
|
-
else if (r.timedOut)
|
|
7398
|
-
error = `timed out after ${opts.timeoutMs / 1000}s`;
|
|
7399
|
-
else if (r.code === null)
|
|
7400
|
-
error = "killed by a signal";
|
|
7401
|
-
opts.log.write(`[exit ${r.code ?? "none"}${error ? `: ${error}` : ""}]
|
|
7402
|
-
|
|
7403
|
-
`);
|
|
7404
|
-
return error ? { run: opts.run, code: r.code, error } : { run: opts.run, code: r.code };
|
|
7405
|
-
}
|
|
7406
|
-
|
|
7407
|
-
// src/home.ts
|
|
7408
7509
|
function homeEnv(home) {
|
|
7409
7510
|
return {
|
|
7410
7511
|
HOME: home,
|
|
@@ -7551,6 +7652,33 @@ function errorMessage(raw) {
|
|
|
7551
7652
|
} catch {}
|
|
7552
7653
|
return raw;
|
|
7553
7654
|
}
|
|
7655
|
+
function usageTokens(events) {
|
|
7656
|
+
const usages = events.filter((e) => e?.type === "turn.completed").map((e) => e?.usage ?? {});
|
|
7657
|
+
if (usages.length === 0)
|
|
7658
|
+
return tokens();
|
|
7659
|
+
const sum = (key) => {
|
|
7660
|
+
let n = 0;
|
|
7661
|
+
for (const u of usages) {
|
|
7662
|
+
const v = u[key];
|
|
7663
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0)
|
|
7664
|
+
return null;
|
|
7665
|
+
n += v;
|
|
7666
|
+
}
|
|
7667
|
+
return n;
|
|
7668
|
+
};
|
|
7669
|
+
const input = sum("input_tokens");
|
|
7670
|
+
const cacheRead = sum("cached_input_tokens");
|
|
7671
|
+
const cacheWrite = sum("cache_write_input_tokens");
|
|
7672
|
+
const uncached = input === null || cacheRead === null || cacheWrite === null ? null : input - cacheRead - cacheWrite;
|
|
7673
|
+
if (uncached !== null && uncached < 0)
|
|
7674
|
+
throw new Error(`codex reported input_tokens ${input} below cached_input_tokens ${cacheRead} + cache_write_input_tokens ${cacheWrite}: its usage is no longer what firegoat reads (src/harnesses/codex.ts usageTokens)`);
|
|
7675
|
+
return tokens({
|
|
7676
|
+
input: uncached,
|
|
7677
|
+
output: sum("output_tokens"),
|
|
7678
|
+
cacheRead,
|
|
7679
|
+
cacheWrite
|
|
7680
|
+
});
|
|
7681
|
+
}
|
|
7554
7682
|
function read2(stdout, exited, timeoutMs) {
|
|
7555
7683
|
const events = stdout.split(`
|
|
7556
7684
|
`).filter((l) => l.trim() !== "").map(parse2);
|
|
@@ -7560,7 +7688,8 @@ function read2(stdout, exited, timeoutMs) {
|
|
|
7560
7688
|
reportedModel: null,
|
|
7561
7689
|
version: null,
|
|
7562
7690
|
turns: null,
|
|
7563
|
-
cost: null
|
|
7691
|
+
cost: null,
|
|
7692
|
+
tokens: usageTokens(events)
|
|
7564
7693
|
};
|
|
7565
7694
|
const thread = events.find((e) => e?.type === "thread.started");
|
|
7566
7695
|
if (!thread) {
|
|
@@ -7833,7 +7962,8 @@ function read3(stdout, exited, timeoutMs) {
|
|
|
7833
7962
|
steps: steps3(events),
|
|
7834
7963
|
final: typeof result?.result === "string" ? result.result : "",
|
|
7835
7964
|
turns: result?.num_turns ?? null,
|
|
7836
|
-
cost: result?.total_cost_usd ?? null
|
|
7965
|
+
cost: result?.total_cost_usd ?? null,
|
|
7966
|
+
tokens: modelUsageTokens(result?.modelUsage)
|
|
7837
7967
|
};
|
|
7838
7968
|
const init = events.find((e) => e?.type === "system" && e.subtype === "init");
|
|
7839
7969
|
const reportedModel = typeof init?.model === "string" && init.model !== "unknown" ? init.model : null;
|
|
@@ -8110,6 +8240,7 @@ function parseConfig(text) {
|
|
|
8110
8240
|
"end_goal",
|
|
8111
8241
|
"checks",
|
|
8112
8242
|
"timeout",
|
|
8243
|
+
"reward_points",
|
|
8113
8244
|
"env",
|
|
8114
8245
|
"install",
|
|
8115
8246
|
"cleanup",
|
|
@@ -8140,6 +8271,7 @@ function parseConfig(text) {
|
|
|
8140
8271
|
end_goal: str2(t.end_goal, `${p}.end_goal`),
|
|
8141
8272
|
checks,
|
|
8142
8273
|
timeout: posInt(t.timeout, `${p}.timeout`, DEFAULT_TIMEOUT),
|
|
8274
|
+
rewardPoints: posInt(t.reward_points, `${p}.reward_points`, 1),
|
|
8143
8275
|
env: { ...env, ...parseEnv(t.env, `${p}.env`) },
|
|
8144
8276
|
install: commands(t.install, `${p}.install`, RUN_PLACEHOLDERS),
|
|
8145
8277
|
cleanup: commands(t.cleanup, `${p}.cleanup`, RUN_PLACEHOLDERS),
|
|
@@ -8743,60 +8875,6 @@ function gitRepo(dir) {
|
|
|
8743
8875
|
};
|
|
8744
8876
|
}
|
|
8745
8877
|
|
|
8746
|
-
// src/harness.ts
|
|
8747
|
-
async function runAgent(harness, opts) {
|
|
8748
|
-
let stdout = "";
|
|
8749
|
-
let pending = "";
|
|
8750
|
-
let group;
|
|
8751
|
-
let markFinal = () => {};
|
|
8752
|
-
const finalSeen = new Promise((r) => {
|
|
8753
|
-
markFinal = r;
|
|
8754
|
-
});
|
|
8755
|
-
const exited = await run({
|
|
8756
|
-
cmd: opts.bin,
|
|
8757
|
-
args: harness.args(opts),
|
|
8758
|
-
cwd: opts.cwd,
|
|
8759
|
-
env: opts.env,
|
|
8760
|
-
timeoutMs: opts.timeoutMs,
|
|
8761
|
-
detached: true,
|
|
8762
|
-
finalSeen,
|
|
8763
|
-
onSpawn: (pid) => {
|
|
8764
|
-
group = pid;
|
|
8765
|
-
},
|
|
8766
|
-
onStdout: (c) => {
|
|
8767
|
-
const text = c.toString();
|
|
8768
|
-
stdout += text;
|
|
8769
|
-
pending += text;
|
|
8770
|
-
const lines = pending.split(`
|
|
8771
|
-
`);
|
|
8772
|
-
pending = lines.pop() ?? "";
|
|
8773
|
-
for (const line of lines) {
|
|
8774
|
-
if (harness.isFinal(line))
|
|
8775
|
-
markFinal();
|
|
8776
|
-
const p = opts.onProgress && harness.progress(line);
|
|
8777
|
-
if (p)
|
|
8778
|
-
opts.onProgress?.(p);
|
|
8779
|
-
}
|
|
8780
|
-
},
|
|
8781
|
-
onStderr: (c) => opts.stderr.write(c)
|
|
8782
|
-
});
|
|
8783
|
-
if (exited.error)
|
|
8784
|
-
return {
|
|
8785
|
-
group,
|
|
8786
|
-
outcome: {
|
|
8787
|
-
ok: false,
|
|
8788
|
-
reason: `could not start ${opts.bin}: ${exited.error.message}`,
|
|
8789
|
-
steps: [],
|
|
8790
|
-
final: "",
|
|
8791
|
-
reportedModel: null,
|
|
8792
|
-
version: null,
|
|
8793
|
-
turns: null,
|
|
8794
|
-
cost: null
|
|
8795
|
-
}
|
|
8796
|
-
};
|
|
8797
|
-
return { group, outcome: harness.read(stdout, exited, opts.timeoutMs) };
|
|
8798
|
-
}
|
|
8799
|
-
|
|
8800
8878
|
// src/help.ts
|
|
8801
8879
|
var EXIT_CODES = `Exit codes:
|
|
8802
8880
|
0 Every run's script checks passed
|
|
@@ -8944,6 +9022,8 @@ firegoat.yaml reference:
|
|
|
8944
9022
|
checks: Optional shell commands, run in the agent's folder
|
|
8945
9023
|
- run: ... Afterwards; exit 0 = pass; they set eval's exit code
|
|
8946
9024
|
timeout: 600 Optional, seconds
|
|
9025
|
+
reward_points: 3 Optional positive integer (default 1): what a run
|
|
9026
|
+
earns when its checks pass; harder end goals get more
|
|
8947
9027
|
env: {NAME: value} Optional, on top of the top-level env:
|
|
8948
9028
|
install: <sh> | [...] Optional, runs after the top-level install
|
|
8949
9029
|
cleanup: <sh> | [...] Optional, runs before the top-level cleanup
|
|
@@ -9171,7 +9251,8 @@ firegoat.yaml, fixtures and build: stay in the current folder.
|
|
|
9171
9251
|
cleanup); env for build, install, checks and cleanup
|
|
9172
9252
|
--json The summary as JSON on stdout (or {error, exitCode} if eval
|
|
9173
9253
|
stops early); progress goes to stderr. The verdict comes
|
|
9174
|
-
last, after runs
|
|
9254
|
+
last, after runs and the reward points (Reward, below):
|
|
9255
|
+
totals {pass, fail, error}, tier
|
|
9175
9256
|
(S/A/B/C, as printed; null when no run was graded), status
|
|
9176
9257
|
("pass", "fail" or "error" = not graded) and exitCode
|
|
9177
9258
|
--no-upload Keep runs local even when logged in to firegoat.ai
|
|
@@ -9206,23 +9287,42 @@ Runs without a session file are not uploaded. FIREGOAT_BASE_URL points elsewhere
|
|
|
9206
9287
|
Results block: without a terminal (an agent, a pipe, CI) and without --json, eval
|
|
9207
9288
|
ends with the terminal's final timeline as one plain block (no color, at most 100
|
|
9208
9289
|
columns): the logo header, a row per run (status, a ramp of one block per agent turn
|
|
9209
|
-
ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, time), its
|
|
9210
|
-
checks and trace URL, the totals
|
|
9290
|
+
ending ▲ pass, ╳ fail or ╌ not graded, checks passed, cost, turns, tokens, time), its
|
|
9291
|
+
failed checks and trace URL, the totals, the tier ("GOATED · tier S" when all pass),
|
|
9292
|
+
"Reward points 7/9 · 12k tokens per point", and with more than one harness × model a
|
|
9293
|
+
table of each one's points, tokens per point, turns and time. It
|
|
9211
9294
|
sits between two lines of ═, after a "Results (paste the block ...)" line; the last
|
|
9212
9295
|
line says where it is saved, results.txt. Show it to your user as-is in a code block.
|
|
9213
9296
|
|
|
9297
|
+
Reward: value per token. A run earns its test's reward_points when its checks pass,
|
|
9298
|
+
else 0; not graded runs earn 0 and still count in the max. tokens = input (uncached)
|
|
9299
|
+
+ output + cacheRead + cacheWrite, as the harness reports them, never estimated:
|
|
9300
|
+
claude-code and grok-cli from the result line's modelUsage (all models, subagents
|
|
9301
|
+
too), codex-cli from turn.completed usage (its cached and cache-written tokens are
|
|
9302
|
+
taken out of input_tokens). Tokens per reward point = the tokens of every run,
|
|
9303
|
+
passed or not, / the points earned; null when no points were earned or any run's
|
|
9304
|
+
tokens are unknown. Tokenizers differ (claude, gpt, grok), so token counts across
|
|
9305
|
+
harnesses are approximate.
|
|
9306
|
+
|
|
9214
9307
|
Output: .firegoat/runs/<runId>/
|
|
9215
9308
|
out A symlink to $FIREGOAT_OUT,
|
|
9216
9309
|
~/.firegoat/runs/<hash>/<random>/out
|
|
9217
9310
|
summary.json firegoatVersion, runId, runsDir, dir (where eval
|
|
9218
9311
|
ran), git {branch, commit, dirty} (no git outside a
|
|
9219
9312
|
repo), harnesses, upload {origin, email} (null when
|
|
9220
|
-
not uploading), runs (every run.json),
|
|
9221
|
-
|
|
9313
|
+
not uploading), runs (every run.json),
|
|
9314
|
+
rewardPoints, rewardPointsMax, tokens (each part
|
|
9315
|
+
summed; null when any run's is), tokensPerRewardPoint
|
|
9316
|
+
(null with 0 points or unknown tokens), groups (per
|
|
9317
|
+
harness × model: harness, model, runs, the same four,
|
|
9318
|
+
turns, durationMs), then the verdict: totals, tier,
|
|
9319
|
+
status and exitCode
|
|
9222
9320
|
results.txt The results block, also with --json or in a terminal
|
|
9223
9321
|
../index.jsonl One line per eval: runId, startedAt, dir, branch,
|
|
9224
9322
|
commit, harnesses, passed, failed, notGraded, cost (null when
|
|
9225
|
-
any run's cost is unknown),
|
|
9323
|
+
any run's cost is unknown), rewardPoints,
|
|
9324
|
+
rewardPointsMax, tokens (the total, or null),
|
|
9325
|
+
uploaded, uploadFailed
|
|
9226
9326
|
<test>/<n>/run.json One run: status and reason (from the checks), harness,
|
|
9227
9327
|
model (asked for, or null) and reportedModel (what
|
|
9228
9328
|
the harness reported running, or null), harnessArgs
|
|
@@ -9237,7 +9337,11 @@ Output: .firegoat/runs/<runId>/
|
|
|
9237
9337
|
own file, else <test>/<n>/session.jsonl made from the
|
|
9238
9338
|
stream) and sessionId (its id, the trace's), stderr,
|
|
9239
9339
|
checksLog, cleanupLog; turns and cost (null when the
|
|
9240
|
-
harness reports none),
|
|
9340
|
+
harness reports none), rewardPoints (earned),
|
|
9341
|
+
rewardPointsMax (the test's reward_points), tokens
|
|
9342
|
+
{input, output, cacheRead, cacheWrite, total} (null
|
|
9343
|
+
parts when the harness reports none), durationMs,
|
|
9344
|
+
startedAt; upload ({status: "uploaded",
|
|
9241
9345
|
url} | {status: "failed", error} | {status:
|
|
9242
9346
|
"skipped", reason}, null when not uploading)
|
|
9243
9347
|
<test>/<n>/transcript.txt What the agent did, below
|
|
@@ -9415,6 +9519,46 @@ async function stopLeftovers(opts) {
|
|
|
9415
9519
|
return leftover;
|
|
9416
9520
|
}
|
|
9417
9521
|
|
|
9522
|
+
// src/reward.ts
|
|
9523
|
+
function earned(status, rewardPointsMax) {
|
|
9524
|
+
return status === "pass" ? rewardPointsMax : 0;
|
|
9525
|
+
}
|
|
9526
|
+
function sum(values) {
|
|
9527
|
+
let n = 0;
|
|
9528
|
+
for (const v of values) {
|
|
9529
|
+
if (v === null)
|
|
9530
|
+
return null;
|
|
9531
|
+
n += v;
|
|
9532
|
+
}
|
|
9533
|
+
return n;
|
|
9534
|
+
}
|
|
9535
|
+
function tally(runs) {
|
|
9536
|
+
const rewardPoints = runs.reduce((s, r) => s + r.rewardPoints, 0);
|
|
9537
|
+
const t = tokens({
|
|
9538
|
+
input: sum(runs.map((r) => r.tokens.input)),
|
|
9539
|
+
output: sum(runs.map((r) => r.tokens.output)),
|
|
9540
|
+
cacheRead: sum(runs.map((r) => r.tokens.cacheRead)),
|
|
9541
|
+
cacheWrite: sum(runs.map((r) => r.tokens.cacheWrite))
|
|
9542
|
+
});
|
|
9543
|
+
return {
|
|
9544
|
+
rewardPoints,
|
|
9545
|
+
rewardPointsMax: runs.reduce((s, r) => s + r.rewardPointsMax, 0),
|
|
9546
|
+
tokens: t,
|
|
9547
|
+
tokensPerRewardPoint: rewardPoints === 0 || t.total === null ? null : Math.round(t.total / rewardPoints)
|
|
9548
|
+
};
|
|
9549
|
+
}
|
|
9550
|
+
function byGroup(runs) {
|
|
9551
|
+
const groups = [];
|
|
9552
|
+
for (const r of runs) {
|
|
9553
|
+
const g = groups.find((x) => x.harness === r.harness && x.model === r.model);
|
|
9554
|
+
if (g)
|
|
9555
|
+
g.runs.push(r);
|
|
9556
|
+
else
|
|
9557
|
+
groups.push({ harness: r.harness, model: r.model, runs: [r] });
|
|
9558
|
+
}
|
|
9559
|
+
return groups;
|
|
9560
|
+
}
|
|
9561
|
+
|
|
9418
9562
|
// src/session.ts
|
|
9419
9563
|
import { randomUUID } from "node:crypto";
|
|
9420
9564
|
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -10230,16 +10374,56 @@ function ramp(r) {
|
|
|
10230
10374
|
const end = r.state === "pass" ? "▲" : r.state === "fail" ? "╳" : r.state === "error" ? "╌" : r.state === "running" ? "●" : "";
|
|
10231
10375
|
return (blocks + end).padEnd(8);
|
|
10232
10376
|
}
|
|
10377
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
10378
|
+
function compactTokens(n) {
|
|
10379
|
+
const unit = (v, suffix) => `${v < 9.95 ? (Math.round(v * 10) / 10).toString() : Math.round(v)}${suffix}`;
|
|
10380
|
+
if (n < 1000)
|
|
10381
|
+
return String(n);
|
|
10382
|
+
if (n < 999500)
|
|
10383
|
+
return unit(n / 1000, "k");
|
|
10384
|
+
return unit(n / 1e6, "M");
|
|
10385
|
+
}
|
|
10233
10386
|
function usageParts(r) {
|
|
10234
10387
|
const parts = [];
|
|
10235
10388
|
if (typeof r.cost === "number")
|
|
10236
10389
|
parts.push(`$${r.cost.toFixed(2)}`);
|
|
10237
10390
|
if (typeof r.reportedTurns === "number")
|
|
10238
|
-
parts.push(
|
|
10391
|
+
parts.push(plural(r.reportedTurns, "turn"));
|
|
10392
|
+
if (r.tokens.total !== null)
|
|
10393
|
+
parts.push(`${compactTokens(r.tokens.total)} tokens`);
|
|
10239
10394
|
if (r.seconds !== undefined)
|
|
10240
10395
|
parts.push(`${r.seconds}s`);
|
|
10241
10396
|
return parts;
|
|
10242
10397
|
}
|
|
10398
|
+
function rewardLine(t, rows) {
|
|
10399
|
+
const points = `Reward points ${t.rewardPoints}/${t.rewardPointsMax}`;
|
|
10400
|
+
const unknown = rows.filter((r) => r.tokens.total === null).length;
|
|
10401
|
+
if (unknown > 0)
|
|
10402
|
+
return `${points} · tokens unknown for ${plural(unknown, "run")}`;
|
|
10403
|
+
if (t.tokensPerRewardPoint === null)
|
|
10404
|
+
return `${points} · no points, so no tokens per point`;
|
|
10405
|
+
return `${points} · ${compactTokens(t.tokensPerRewardPoint)} tokens per point`;
|
|
10406
|
+
}
|
|
10407
|
+
function comparison(rows) {
|
|
10408
|
+
const groups = byGroup(rows.map((r) => ({ ...r, harness: r.harnessName })));
|
|
10409
|
+
const label = (g) => `${g.harness} ${g.model ?? "(default model)"}`;
|
|
10410
|
+
const width = Math.min(40, Math.max(5, ...groups.map((g) => label(g).length)));
|
|
10411
|
+
const line = (cells) => ` ${cells[0].padEnd(width)} ${cells[1].padEnd(8)} ${cells[2].padEnd(10)} ${cells[3].padEnd(6)} ${cells[4]}`;
|
|
10412
|
+
return [
|
|
10413
|
+
line(["Group", "Points", "Tokens/pt", "Turns", "Time"]),
|
|
10414
|
+
...groups.map((g) => {
|
|
10415
|
+
const t = tally(g.runs);
|
|
10416
|
+
const turns = totalTurns(g.runs);
|
|
10417
|
+
return line([
|
|
10418
|
+
cut(label(g), width),
|
|
10419
|
+
`${t.rewardPoints}/${t.rewardPointsMax}`,
|
|
10420
|
+
g.runs.some((r) => r.tokens.total === null) ? "unknown" : t.tokensPerRewardPoint === null ? "no points" : compactTokens(t.tokensPerRewardPoint),
|
|
10421
|
+
turns === null ? "unknown" : String(turns),
|
|
10422
|
+
duration(g.runs.reduce((s, r) => s + (r.seconds ?? 0), 0))
|
|
10423
|
+
]);
|
|
10424
|
+
})
|
|
10425
|
+
];
|
|
10426
|
+
}
|
|
10243
10427
|
function usage(r) {
|
|
10244
10428
|
return usageParts(r).join(" ");
|
|
10245
10429
|
}
|
|
@@ -10413,12 +10597,16 @@ function timeline(t, p, w) {
|
|
|
10413
10597
|
}
|
|
10414
10598
|
const tr = tier(rows);
|
|
10415
10599
|
const goated = c.pass === rows.length && rows.length > 0;
|
|
10416
|
-
const
|
|
10417
|
-
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` :
|
|
10600
|
+
const passFail = `${c.pass} PASS · ${c.fail} FAIL · ${c.error} NOT GRADED`;
|
|
10601
|
+
const left = goated ? `${rows.length}/${rows.length} runs passed their checks` : passFail;
|
|
10418
10602
|
const right = goated ? `GOATED · tier ${tr}` : tr ? `Tier ${tr}` : "";
|
|
10419
10603
|
out.push(` ${goated ? p(C.green, left) : left}${" ".repeat(Math.max(2, w - left.length - right.length - 3))}${goated ? p(C.ember, right) : right}`);
|
|
10420
10604
|
const turns = totalTurns(rows);
|
|
10421
|
-
|
|
10605
|
+
const reward = tally(rows);
|
|
10606
|
+
out.push(p(C.gray, ` Total ${totalCost(rows)}${turns === null ? "" : ` · ${plural(turns, "turn")}`}${reward.tokens.total === null ? "" : ` · ${compactTokens(reward.tokens.total)} tokens`} · ${duration(finished.seconds)}`));
|
|
10607
|
+
out.push(` ${rewardLine(reward, rows)}`);
|
|
10608
|
+
if (byGroup(rows.map((r) => ({ ...r, harness: r.harnessName }))).length > 1)
|
|
10609
|
+
out.push(...comparison(rows).map((l) => p(C.gray, l)));
|
|
10422
10610
|
out.push(p(C.gray, ` Runs ${shortPath(finished.runsDir)}/`));
|
|
10423
10611
|
out.push(` Checks are a score, not the verdict: judge each end_goal from ${p(C.cyan, "<test>/<n>/transcript.txt")}`);
|
|
10424
10612
|
return out;
|
|
@@ -10906,6 +11094,9 @@ async function runOne(opts) {
|
|
|
10906
11094
|
leftover: [],
|
|
10907
11095
|
turns: null,
|
|
10908
11096
|
cost: null,
|
|
11097
|
+
rewardPoints: 0,
|
|
11098
|
+
rewardPointsMax: test.rewardPoints,
|
|
11099
|
+
tokens: tokens(),
|
|
10909
11100
|
durationMs: 0,
|
|
10910
11101
|
startedAt: new Date(started).toISOString(),
|
|
10911
11102
|
upload: null
|
|
@@ -10937,6 +11128,7 @@ async function runOne(opts) {
|
|
|
10937
11128
|
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10938
11129
|
r.status = status;
|
|
10939
11130
|
r.reason = reason;
|
|
11131
|
+
r.rewardPoints = earned(status, r.rewardPointsMax);
|
|
10940
11132
|
r.durationMs = Date.now() - started;
|
|
10941
11133
|
writeRunJson(r);
|
|
10942
11134
|
return r;
|
|
@@ -11027,6 +11219,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
11027
11219
|
});
|
|
11028
11220
|
r.turns = outcome.turns;
|
|
11029
11221
|
r.cost = outcome.cost;
|
|
11222
|
+
r.tokens = outcome.tokens;
|
|
11030
11223
|
if (!outcome.ok)
|
|
11031
11224
|
return ["error", `not graded: ${outcome.reason}`];
|
|
11032
11225
|
if (!r.session)
|
|
@@ -11249,8 +11442,12 @@ async function evalCommand(args, root) {
|
|
|
11249
11442
|
const rows = plan.map((p) => ({
|
|
11250
11443
|
...p,
|
|
11251
11444
|
harness: showHarness ? p.harness : null,
|
|
11445
|
+
harnessName: p.harness,
|
|
11252
11446
|
state: "queued",
|
|
11253
|
-
turns: 0
|
|
11447
|
+
turns: 0,
|
|
11448
|
+
rewardPoints: 0,
|
|
11449
|
+
rewardPointsMax: tests.find((t) => t.name === p.test).rewardPoints,
|
|
11450
|
+
tokens: tokens()
|
|
11254
11451
|
}));
|
|
11255
11452
|
if (config.build.length > 0) {
|
|
11256
11453
|
const buildStarted = Date.now();
|
|
@@ -11308,6 +11505,8 @@ async function evalCommand(args, root) {
|
|
|
11308
11505
|
reason: r.reason,
|
|
11309
11506
|
checks: r.checks,
|
|
11310
11507
|
cost: r.cost,
|
|
11508
|
+
rewardPoints: r.rewardPoints,
|
|
11509
|
+
tokens: r.tokens,
|
|
11311
11510
|
seconds: Math.round(r.durationMs / 1000),
|
|
11312
11511
|
command: undefined
|
|
11313
11512
|
});
|
|
@@ -11345,6 +11544,7 @@ async function evalCommand(args, root) {
|
|
|
11345
11544
|
fail: EXIT.fail,
|
|
11346
11545
|
pass: EXIT.pass
|
|
11347
11546
|
}[status];
|
|
11547
|
+
const reward = tally(results);
|
|
11348
11548
|
const doc = {
|
|
11349
11549
|
firegoatVersion: package_default.version,
|
|
11350
11550
|
runId,
|
|
@@ -11354,6 +11554,15 @@ async function evalCommand(args, root) {
|
|
|
11354
11554
|
harnesses: harnessNames,
|
|
11355
11555
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
11356
11556
|
runs: results,
|
|
11557
|
+
...reward,
|
|
11558
|
+
groups: byGroup(results).map((g) => ({
|
|
11559
|
+
harness: g.harness,
|
|
11560
|
+
model: g.model,
|
|
11561
|
+
runs: g.runs.length,
|
|
11562
|
+
...tally(g.runs),
|
|
11563
|
+
turns: g.runs.every((r) => r.turns !== null) ? g.runs.reduce((s, r) => s + r.turns, 0) : null,
|
|
11564
|
+
durationMs: g.runs.reduce((s, r) => s + r.durationMs, 0)
|
|
11565
|
+
})),
|
|
11357
11566
|
totals,
|
|
11358
11567
|
tier: tier(rows),
|
|
11359
11568
|
status,
|
|
@@ -11372,6 +11581,9 @@ async function evalCommand(args, root) {
|
|
|
11372
11581
|
failed: totals.fail,
|
|
11373
11582
|
notGraded: totals.error,
|
|
11374
11583
|
cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
|
|
11584
|
+
rewardPoints: reward.rewardPoints,
|
|
11585
|
+
rewardPointsMax: reward.rewardPointsMax,
|
|
11586
|
+
tokens: reward.tokens.total,
|
|
11375
11587
|
uploaded,
|
|
11376
11588
|
uploadFailed: uploadsFailed
|
|
11377
11589
|
})}
|
|
@@ -12086,6 +12298,9 @@ tests:
|
|
|
12086
12298
|
grep -Eiq '^agent: .*(you.*goat|goat.*you)' "$FIREGOAT_TRANSCRIPT"
|
|
12087
12299
|
# Seconds before the agent run is stopped.
|
|
12088
12300
|
timeout: 600
|
|
12301
|
+
# What a run earns when its checks pass (default 1): give harder end goals
|
|
12302
|
+
# more. eval prints tokens per point earned, the value per token.
|
|
12303
|
+
# reward_points: 3
|
|
12089
12304
|
`;
|
|
12090
12305
|
}
|
|
12091
12306
|
var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
|