firegoat 0.30.0 → 0.32.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 +453 -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.32.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;
|
|
@@ -10528,6 +10716,30 @@ function runModel(r) {
|
|
|
10528
10716
|
reported: label(r.reportedModel, "Reported model")
|
|
10529
10717
|
};
|
|
10530
10718
|
}
|
|
10719
|
+
var MAX_INT = 2147483647;
|
|
10720
|
+
function count(v, what) {
|
|
10721
|
+
if (v === null)
|
|
10722
|
+
return null;
|
|
10723
|
+
if (!Number.isInteger(v) || v < 0 || v > MAX_INT)
|
|
10724
|
+
throw new Error(`${what} is ${v}, the upload takes an integer from 0 to ${MAX_INT}`);
|
|
10725
|
+
return v;
|
|
10726
|
+
}
|
|
10727
|
+
function runTokens(t) {
|
|
10728
|
+
return {
|
|
10729
|
+
input: count(t.input, "Input tokens"),
|
|
10730
|
+
output: count(t.output, "Output tokens"),
|
|
10731
|
+
cacheRead: count(t.cacheRead, "Cache read tokens"),
|
|
10732
|
+
cacheWrite: count(t.cacheWrite, "Cache write tokens"),
|
|
10733
|
+
total: count(t.total, "Total tokens")
|
|
10734
|
+
};
|
|
10735
|
+
}
|
|
10736
|
+
function runPoints(r) {
|
|
10737
|
+
const rewardPoints = count(r.rewardPoints, "Reward points");
|
|
10738
|
+
const rewardPointsMax = count(r.rewardPointsMax, "Reward points max");
|
|
10739
|
+
if (rewardPoints !== null && rewardPointsMax !== null && rewardPoints > rewardPointsMax)
|
|
10740
|
+
throw new Error(`Reward points ${rewardPoints} are over the run's max ${rewardPointsMax}`);
|
|
10741
|
+
return { rewardPoints, rewardPointsMax };
|
|
10742
|
+
}
|
|
10531
10743
|
function runGit(g) {
|
|
10532
10744
|
if (g.commit !== null && !COMMIT.test(g.commit))
|
|
10533
10745
|
throw new Error(`Git commit ${printable(g.commit, 80)} is not 4-64 lowercase hex characters`);
|
|
@@ -10592,9 +10804,13 @@ async function uploadRun(t, r) {
|
|
|
10592
10804
|
let project;
|
|
10593
10805
|
let model;
|
|
10594
10806
|
let git;
|
|
10807
|
+
let tokens;
|
|
10808
|
+
let points;
|
|
10595
10809
|
try {
|
|
10596
10810
|
project = projectName(t.project);
|
|
10597
10811
|
model = runModel(r);
|
|
10812
|
+
tokens = runTokens(r.tokens);
|
|
10813
|
+
points = runPoints(r);
|
|
10598
10814
|
git = t.git ? runGit(t.git) : undefined;
|
|
10599
10815
|
} catch (e) {
|
|
10600
10816
|
return { status: "failed", error: e.message };
|
|
@@ -10618,6 +10834,8 @@ async function uploadRun(t, r) {
|
|
|
10618
10834
|
duration_ms: r.durationMs,
|
|
10619
10835
|
harness: r.harness,
|
|
10620
10836
|
model,
|
|
10837
|
+
tokens,
|
|
10838
|
+
...points,
|
|
10621
10839
|
...git && { git }
|
|
10622
10840
|
},
|
|
10623
10841
|
files: Object.fromEntries(FILES.map((f) => [f, bytes[f].byteLength]))
|
|
@@ -10906,6 +11124,9 @@ async function runOne(opts) {
|
|
|
10906
11124
|
leftover: [],
|
|
10907
11125
|
turns: null,
|
|
10908
11126
|
cost: null,
|
|
11127
|
+
rewardPoints: 0,
|
|
11128
|
+
rewardPointsMax: test.rewardPoints,
|
|
11129
|
+
tokens: tokens(),
|
|
10909
11130
|
durationMs: 0,
|
|
10910
11131
|
startedAt: new Date(started).toISOString(),
|
|
10911
11132
|
upload: null
|
|
@@ -10937,6 +11158,7 @@ async function runOne(opts) {
|
|
|
10937
11158
|
r.leftover = await stopLeftovers({ group: agent.group, workdir });
|
|
10938
11159
|
r.status = status;
|
|
10939
11160
|
r.reason = reason;
|
|
11161
|
+
r.rewardPoints = earned(status, r.rewardPointsMax);
|
|
10940
11162
|
r.durationMs = Date.now() - started;
|
|
10941
11163
|
writeRunJson(r);
|
|
10942
11164
|
return r;
|
|
@@ -11027,6 +11249,7 @@ async function attempt(opts, r, steps, env, stepEnv, homeEnvFile, agent) {
|
|
|
11027
11249
|
});
|
|
11028
11250
|
r.turns = outcome.turns;
|
|
11029
11251
|
r.cost = outcome.cost;
|
|
11252
|
+
r.tokens = outcome.tokens;
|
|
11030
11253
|
if (!outcome.ok)
|
|
11031
11254
|
return ["error", `not graded: ${outcome.reason}`];
|
|
11032
11255
|
if (!r.session)
|
|
@@ -11249,8 +11472,12 @@ async function evalCommand(args, root) {
|
|
|
11249
11472
|
const rows = plan.map((p) => ({
|
|
11250
11473
|
...p,
|
|
11251
11474
|
harness: showHarness ? p.harness : null,
|
|
11475
|
+
harnessName: p.harness,
|
|
11252
11476
|
state: "queued",
|
|
11253
|
-
turns: 0
|
|
11477
|
+
turns: 0,
|
|
11478
|
+
rewardPoints: 0,
|
|
11479
|
+
rewardPointsMax: tests.find((t) => t.name === p.test).rewardPoints,
|
|
11480
|
+
tokens: tokens()
|
|
11254
11481
|
}));
|
|
11255
11482
|
if (config.build.length > 0) {
|
|
11256
11483
|
const buildStarted = Date.now();
|
|
@@ -11308,6 +11535,8 @@ async function evalCommand(args, root) {
|
|
|
11308
11535
|
reason: r.reason,
|
|
11309
11536
|
checks: r.checks,
|
|
11310
11537
|
cost: r.cost,
|
|
11538
|
+
rewardPoints: r.rewardPoints,
|
|
11539
|
+
tokens: r.tokens,
|
|
11311
11540
|
seconds: Math.round(r.durationMs / 1000),
|
|
11312
11541
|
command: undefined
|
|
11313
11542
|
});
|
|
@@ -11345,6 +11574,7 @@ async function evalCommand(args, root) {
|
|
|
11345
11574
|
fail: EXIT.fail,
|
|
11346
11575
|
pass: EXIT.pass
|
|
11347
11576
|
}[status];
|
|
11577
|
+
const reward = tally(results);
|
|
11348
11578
|
const doc = {
|
|
11349
11579
|
firegoatVersion: package_default.version,
|
|
11350
11580
|
runId,
|
|
@@ -11354,6 +11584,15 @@ async function evalCommand(args, root) {
|
|
|
11354
11584
|
harnesses: harnessNames,
|
|
11355
11585
|
upload: upload && { origin: upload.origin, email: upload.email },
|
|
11356
11586
|
runs: results,
|
|
11587
|
+
...reward,
|
|
11588
|
+
groups: byGroup(results).map((g) => ({
|
|
11589
|
+
harness: g.harness,
|
|
11590
|
+
model: g.model,
|
|
11591
|
+
runs: g.runs.length,
|
|
11592
|
+
...tally(g.runs),
|
|
11593
|
+
turns: g.runs.every((r) => r.turns !== null) ? g.runs.reduce((s, r) => s + r.turns, 0) : null,
|
|
11594
|
+
durationMs: g.runs.reduce((s, r) => s + r.durationMs, 0)
|
|
11595
|
+
})),
|
|
11357
11596
|
totals,
|
|
11358
11597
|
tier: tier(rows),
|
|
11359
11598
|
status,
|
|
@@ -11372,6 +11611,9 @@ async function evalCommand(args, root) {
|
|
|
11372
11611
|
failed: totals.fail,
|
|
11373
11612
|
notGraded: totals.error,
|
|
11374
11613
|
cost: results.every((r) => r.cost !== null) ? results.reduce((sum, r) => sum + r.cost, 0) : null,
|
|
11614
|
+
rewardPoints: reward.rewardPoints,
|
|
11615
|
+
rewardPointsMax: reward.rewardPointsMax,
|
|
11616
|
+
tokens: reward.tokens.total,
|
|
11375
11617
|
uploaded,
|
|
11376
11618
|
uploadFailed: uploadsFailed
|
|
11377
11619
|
})}
|
|
@@ -12086,6 +12328,9 @@ tests:
|
|
|
12086
12328
|
grep -Eiq '^agent: .*(you.*goat|goat.*you)' "$FIREGOAT_TRANSCRIPT"
|
|
12087
12329
|
# Seconds before the agent run is stopped.
|
|
12088
12330
|
timeout: 600
|
|
12331
|
+
# What a run earns when its checks pass (default 1): give harder end goals
|
|
12332
|
+
# more. eval prints tokens per point earned, the value per token.
|
|
12333
|
+
# reward_points: 3
|
|
12089
12334
|
`;
|
|
12090
12335
|
}
|
|
12091
12336
|
var AGENT_PROMPT = "Scan my project and setup 2-3 agent experience prompts to test general usage of the app. Use firegoat skills";
|