toolroll 0.9.81 → 0.9.83
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/CHANGELOG.md +35 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/folder-look.d.ts +107 -0
- package/dist/folder-look.js +302 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- package/dist/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +123 -1
- package/dist/operate.js +344 -29
- package/dist/project-identity.d.ts +37 -0
- package/dist/project-identity.js +106 -0
- package/dist/project-knowledge.d.ts +4 -4
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +52 -19
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +16 -0
- package/dist/release.js +16 -0
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +35 -8
- package/dist/server/chat.js +4 -0
- package/dist/server/flows.js +9 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +3 -0
- package/dist/server/pages.js +62 -88
- package/dist/server/render-pages.d.ts +2 -16
- package/dist/server/render-pages.js +1 -55
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.js +14 -3
- package/dist/server/tasks.js +15 -4
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eval set's trusted oracle (docs/plans/evals-nightly.md, part C, "Grading: a trusted oracle outside the fence").
|
|
3
|
+
*
|
|
4
|
+
* The plane's runner starts the oracle as a separate process from the installed release (`runOracle`, which spawns
|
|
5
|
+
* eval-oracle-main.js), only after the attempt's whole process tree is gone, and hands it two folders: the installed
|
|
6
|
+
* case (`evals/cases/<id>/`, from the plane's own package) and the graded files the runner extracted from the built
|
|
7
|
+
* commit's verified Git objects. The oracle owns the grading (`gradeCase`):
|
|
8
|
+
*
|
|
9
|
+
* 1. It copies only the case's named files from the graded folder into a fresh grading folder, reading each with
|
|
10
|
+
* O_NOFOLLOW and refusing links and anything but a regular file; then the case's own `probe.mjs` from the
|
|
11
|
+
* installed case (an edited launcher in the build is never the one run).
|
|
12
|
+
* 2. It starts the probe with the installed node and a fixed argv inside the native fence (the release check's:
|
|
13
|
+
* Seatbelt or bubblewrap via the agent fence), writing only the grading folder, with no network and the installed
|
|
14
|
+
* eval set and the plane's state unreadable, under a time limit.
|
|
15
|
+
* 3. It sends a fresh nonce and inputs it chose from that nonce, reads at most 64 KiB of output, and passes the
|
|
16
|
+
* attempt only when the output names that nonce with the values it computed itself (`oracle.mjs` `expected`).
|
|
17
|
+
* An exit code alone never passes: an early `process.exit(0)` leaves no answer.
|
|
18
|
+
*
|
|
19
|
+
* The build's code never runs in the oracle's own process, and nothing the build wrote (a result file, a verdict) is
|
|
20
|
+
* ever read as one: the verdict travels from the oracle to the runner over the oracle's own pipe.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
24
|
+
import { closeSync, constants, lstatSync, mkdirSync, mkdtempSync, openSync, realpathSync, rmSync, writeSync } from "node:fs";
|
|
25
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
+
import { checkLaunchRoot, fenceFor, linuxFenceAvailable, macosFenceAvailable, runFolders } from "./agent-fence.js";
|
|
28
|
+
import { startEgressProxy, proxyEnv } from "./egress.js";
|
|
29
|
+
import { run, OVERFLOW_CODE } from "./exec.js";
|
|
30
|
+
import { readNoFollow } from "./eval-gate.js";
|
|
31
|
+
/** The most a graded program may print, and the most of each graded file the oracle copies. */
|
|
32
|
+
export const ORACLE_OUTPUT_BYTES = 64 * 1024;
|
|
33
|
+
const GRADED_FILE_BYTES = 256 * 1024;
|
|
34
|
+
const EVALS_ROOT = fileURLToPath(new URL("../evals/", import.meta.url));
|
|
35
|
+
/** A random source seeded by the nonce (mulberry32 over its digest): the inputs are fresh for every grading. */
|
|
36
|
+
export function seededRandom(nonce) {
|
|
37
|
+
let state = createHash("sha256").update(nonce).digest().readUInt32LE(0);
|
|
38
|
+
return () => {
|
|
39
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
40
|
+
let t = state;
|
|
41
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
42
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
43
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** One value in a canonical form (object keys sorted), so two answers compare by content. */
|
|
47
|
+
export function canonical(value) {
|
|
48
|
+
return JSON.stringify(value, (_key, inner) => inner !== null && typeof inner === "object" && !Array.isArray(inner)
|
|
49
|
+
? Object.fromEntries(Object.keys(inner).sort().map(key => [key, inner[key]]))
|
|
50
|
+
: inner);
|
|
51
|
+
}
|
|
52
|
+
/** The default: the installed node, `probe.mjs`, inside the release check's fence — writes only the grading folder, no
|
|
53
|
+
* network (a proxy of its own that allows no host), the installed eval set and whatever else `deny` names unreadable. No
|
|
54
|
+
* fence on this machine: nothing is graded (the attempt is an error, never a pass). */
|
|
55
|
+
export const fencedGradeRunner = async ({ cwd, stdin, timeoutMs, deny }) => {
|
|
56
|
+
if (!macosFenceAvailable() && !linuxFenceAvailable())
|
|
57
|
+
throw new Error("this machine can't fence the graded program");
|
|
58
|
+
const egress = await startEgressProxy({ hosts: [] });
|
|
59
|
+
try {
|
|
60
|
+
const network = { mode: "allowlist", proxyPort: egress.port, brokerPorts: [], loopback: "proxy-only", socketRoots: [cwd], token: egress.secret };
|
|
61
|
+
const launch = fenceFor("check", { databaseFile: null, worktree: cwd, boundary: null, binding: null, runFolders: runFolders(null), poolRoots: [], extra: [...deny],
|
|
62
|
+
writes: { writes: { subpaths: [cwd], prefixes: [] }, readOnly: [] }, network });
|
|
63
|
+
const result = await run(process.execPath, ["probe.mjs"], { cwd, stdin, timeoutMs, maxBuffer: ORACLE_OUTPUT_BYTES, processGroup: true, envAllowlist: ["PATH"],
|
|
64
|
+
env: { ...proxyEnv(egress, "proxy-only"), HOME: cwd, TMPDIR: cwd }, fence: launch.fence, fenceGrants: launch.fenceGrants, confinement: "required" });
|
|
65
|
+
return { code: result.code, stdout: result.stdout, timedOut: result.timedOut };
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await egress.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** Copy a file the oracle trusts nothing about: a regular file, never through a link, bounded; into a path that must
|
|
72
|
+
* not exist yet. */
|
|
73
|
+
function copyIn(from, to, maxBytes) {
|
|
74
|
+
const bytes = readNoFollow(from, maxBytes);
|
|
75
|
+
if (bytes === null) {
|
|
76
|
+
try {
|
|
77
|
+
return lstatSync(from).isSymbolicLink() ? "is a link, not a file" : "isn't a regular file it can read";
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return "is missing";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
84
|
+
const fd = openSync(to, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
85
|
+
try {
|
|
86
|
+
writeSync(fd, bytes);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const inside = (root, path) => { const rel = relative(root, path); return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel); };
|
|
94
|
+
/** Grade one attempt, in the oracle's own process. `caseDir` must be a case of the installed eval set (`evalsRoot`). */
|
|
95
|
+
export async function gradeCase(input) {
|
|
96
|
+
const evalsRoot = realpathSync(input.evalsRoot ?? EVALS_ROOT);
|
|
97
|
+
let caseDir, inputDir;
|
|
98
|
+
try {
|
|
99
|
+
caseDir = realpathSync(input.caseDir);
|
|
100
|
+
inputDir = realpathSync(input.inputDir);
|
|
101
|
+
if (!inside(evalsRoot, caseDir) || lstatSync(input.caseDir).isSymbolicLink())
|
|
102
|
+
return { outcome: "error", why: "the case isn't one of the installed eval set" };
|
|
103
|
+
if (lstatSync(input.inputDir).isSymbolicLink() || !lstatSync(inputDir).isDirectory())
|
|
104
|
+
return { outcome: "error", why: "the graded folder isn't a folder" };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { outcome: "error", why: "the case or the graded folder can't be read" };
|
|
108
|
+
}
|
|
109
|
+
for (const name of ["oracle.mjs", "probe.mjs"]) {
|
|
110
|
+
try {
|
|
111
|
+
if (!lstatSync(join(caseDir, name)).isFile())
|
|
112
|
+
return { outcome: "error", why: `the case's ${name} isn't a file` };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { outcome: "error", why: `the case has no ${name}` };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let oracle;
|
|
119
|
+
try {
|
|
120
|
+
oracle = await import(pathToFileURL(join(caseDir, "oracle.mjs")).href);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { outcome: "error", why: "the case's oracle can't be loaded" };
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(oracle.files) || typeof oracle.inputs !== "function" || typeof oracle.expected !== "function")
|
|
126
|
+
return { outcome: "error", why: "the case's oracle is malformed" };
|
|
127
|
+
const grading = realpathSync(mkdtempSync(join(checkLaunchRoot(), "so-eval-grade-")));
|
|
128
|
+
try {
|
|
129
|
+
for (const file of oracle.files) {
|
|
130
|
+
if (typeof file !== "string" || file === "probe.mjs" || file.split("/").some(part => part === "" || part === "." || part === "..") || isAbsolute(file))
|
|
131
|
+
return { outcome: "error", why: "the case's oracle names a file it can't grade" };
|
|
132
|
+
const problem = copyIn(join(inputDir, ...file.split("/")), join(grading, ...file.split("/")), GRADED_FILE_BYTES);
|
|
133
|
+
if (problem !== null)
|
|
134
|
+
return { outcome: "fail", why: `the build's ${file} ${problem}` };
|
|
135
|
+
}
|
|
136
|
+
const probeProblem = copyIn(join(caseDir, "probe.mjs"), join(grading, "probe.mjs"), GRADED_FILE_BYTES);
|
|
137
|
+
if (probeProblem !== null)
|
|
138
|
+
return { outcome: "error", why: `the case's probe ${probeProblem}` };
|
|
139
|
+
const nonce = randomBytes(16).toString("hex");
|
|
140
|
+
let inputs, expected;
|
|
141
|
+
try {
|
|
142
|
+
inputs = oracle.inputs(seededRandom(nonce));
|
|
143
|
+
expected = inputs.map(one => canonical(oracle.expected(structuredClone(one))));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { outcome: "error", why: "the case's oracle failed to work out its answers" };
|
|
147
|
+
}
|
|
148
|
+
let ran;
|
|
149
|
+
try {
|
|
150
|
+
ran = await (input.runner ?? fencedGradeRunner)({ cwd: grading, stdin: `${JSON.stringify({ nonce, inputs })}\n`, timeoutMs: input.timeoutMs,
|
|
151
|
+
deny: [evalsRoot, inputDir, ...(input.deny ?? [])] });
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return { outcome: "error", why: `the graded program couldn't be started: ${error instanceof Error ? error.message.slice(0, 160) : String(error)}` };
|
|
155
|
+
}
|
|
156
|
+
if (ran.timedOut)
|
|
157
|
+
return { outcome: "fail", why: "the graded program timed out" };
|
|
158
|
+
if (ran.code === OVERFLOW_CODE)
|
|
159
|
+
return { outcome: "fail", why: `the graded program printed more than ${ORACLE_OUTPUT_BYTES} bytes` };
|
|
160
|
+
// The answer: a line naming this grading's nonce, with one output per input. The last such line counts.
|
|
161
|
+
let outputs = null;
|
|
162
|
+
for (const line of ran.stdout.slice(0, ORACLE_OUTPUT_BYTES).split("\n")) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(line);
|
|
165
|
+
if (parsed !== null && parsed["nonce"] === nonce && Array.isArray(parsed["outputs"]))
|
|
166
|
+
outputs = parsed["outputs"];
|
|
167
|
+
}
|
|
168
|
+
catch { /* not the answer */ }
|
|
169
|
+
}
|
|
170
|
+
if (outputs === null)
|
|
171
|
+
return { outcome: "fail", why: ran.code === 0 ? "the graded program exited without an answer (an exit code alone never passes)" : "the graded program crashed without an answer" };
|
|
172
|
+
// An answer and then a crash or a nonzero exit is still a crash (Codex round 1 #11): it must answer AND finish cleanly.
|
|
173
|
+
if (ran.code !== 0)
|
|
174
|
+
return { outcome: "fail", why: `the graded program exited with ${ran.code} after answering` };
|
|
175
|
+
if (outputs.length !== expected.length)
|
|
176
|
+
return { outcome: "fail", why: "the graded program answered a different number of inputs" };
|
|
177
|
+
const wrong = expected.findIndex((one, index) => canonical(outputs[index]) !== one);
|
|
178
|
+
return wrong === -1 ? { outcome: "pass", why: null } : { outcome: "fail", why: `wrong answer for input ${wrong + 1} of ${expected.length}` };
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
rmSync(grading, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** The oracle process's entry file, from the installed release (the sources under a test). */
|
|
185
|
+
export function oracleEntry() {
|
|
186
|
+
const source = import.meta.url.endsWith(".ts");
|
|
187
|
+
const entry = fileURLToPath(new URL(source ? "./eval-oracle-main.ts" : "./eval-oracle-main.js", import.meta.url));
|
|
188
|
+
return { file: process.execPath, args: [...(source ? ["--import", import.meta.resolve("tsx")] : []), entry] };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Grade in a separate process from the installed release (the plane's runner calls this once the attempt's process
|
|
192
|
+
* tree is gone). Its verdict comes back over its own stdout; anything else — it crashed, timed out, said nothing
|
|
193
|
+
* readable — is an error the plane recorded, never a pass.
|
|
194
|
+
*/
|
|
195
|
+
export async function runOracle(input, entry = oracleEntry()) {
|
|
196
|
+
const child = spawn(entry.file, entry.args, { stdio: ["pipe", "pipe", "ignore"], env: { PATH: process.env["PATH"] ?? "/usr/bin:/bin", HOME: process.env["HOME"] ?? "/" }, detached: true });
|
|
197
|
+
let stdout = "";
|
|
198
|
+
child.stdout.on("data", (chunk) => { if (stdout.length < 16 * 1024)
|
|
199
|
+
stdout += chunk.toString("utf8"); });
|
|
200
|
+
child.stdin.end(`${JSON.stringify({ caseDir: input.caseDir, inputDir: input.inputDir, timeoutMs: input.timeoutMs, deny: input.deny ?? [] })}\n`);
|
|
201
|
+
const code = await new Promise(done => {
|
|
202
|
+
const timer = setTimeout(() => { try {
|
|
203
|
+
process.kill(-(child.pid ?? 0), "SIGKILL");
|
|
204
|
+
}
|
|
205
|
+
catch { /* gone */ } }, input.timeoutMs + 60_000);
|
|
206
|
+
child.once("error", () => { clearTimeout(timer); done(null); });
|
|
207
|
+
child.once("close", exit => { clearTimeout(timer); done(exit); });
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(stdout.trim().split("\n").pop() ?? "");
|
|
211
|
+
const outcome = parsed["outcome"];
|
|
212
|
+
if (code === 0 && (outcome === "pass" || outcome === "fail" || outcome === "error"))
|
|
213
|
+
return { outcome, why: typeof parsed["why"] === "string" ? parsed["why"].slice(0, 300) : null };
|
|
214
|
+
}
|
|
215
|
+
catch { /* below */ }
|
|
216
|
+
return { outcome: "error", why: "the oracle process failed" };
|
|
217
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type EvalCase } from "./evals.js";
|
|
2
|
+
/** The installed CLI, as the world's commands run it (the sources under a test). */
|
|
3
|
+
export declare function evalCliEntry(): {
|
|
4
|
+
file: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
export type WorldReport = {
|
|
8
|
+
evalStage: "world-start" | "done";
|
|
9
|
+
why: string | null;
|
|
10
|
+
};
|
|
11
|
+
/** Make the world and run its build to the end. Never throws: a failure is the report. */
|
|
12
|
+
export declare function runEvalWorld(one: EvalCase, input: {
|
|
13
|
+
worldDir: string;
|
|
14
|
+
databaseFile: string;
|
|
15
|
+
deny: readonly string[];
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}): Promise<WorldReport>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One eval attempt's world (docs/plans/evals-nightly.md, part C, "Where it runs"): what `toolroll evals attempt` does
|
|
3
|
+
* in its own child process of the installed binary. Everything is throwaway and the world's own — its database, its
|
|
4
|
+
* repository made from the case's fixture, its worktrees, its approver and worker — driven through the installed CLI
|
|
5
|
+
* exactly as a person's project is (the real-model pilot's choreography, scripts/pilot.mjs). The world never learns
|
|
6
|
+
* the plane's database: every command names the world's own (`--db`).
|
|
7
|
+
*
|
|
8
|
+
* Its builds run under the project's own fence, with the installed eval set (the hidden checks) and anything else the
|
|
9
|
+
* runner names unreadable (`project sandbox --read-deny`), and every provider dispatch takes a slot of the runner's
|
|
10
|
+
* gate (eval-gate.ts) through TOOLROLL_EVAL_GATE, which the runner set and this process never changes.
|
|
11
|
+
*
|
|
12
|
+
* The last line on stdout says how far it got: `world-start` (the world failed before the agent could start: the
|
|
13
|
+
* runner records an error only if the world's database agrees nothing reached a provider), or `done`.
|
|
14
|
+
*/
|
|
15
|
+
import { closeSync, constants, mkdirSync, openSync, writeSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { run } from "./exec.js";
|
|
19
|
+
import { EVALS_ROOT, worldSecret } from "./evals.js";
|
|
20
|
+
/** The installed CLI, as the world's commands run it (the sources under a test). */
|
|
21
|
+
export function evalCliEntry() {
|
|
22
|
+
const source = import.meta.url.endsWith(".ts");
|
|
23
|
+
const entry = fileURLToPath(new URL(source ? "./bin.ts" : "./bin.js", import.meta.url));
|
|
24
|
+
return { file: process.execPath, args: [...(source ? ["--import", import.meta.resolve("tsx")] : []), entry] };
|
|
25
|
+
}
|
|
26
|
+
/** Git for the world's own fixture repository, before any agent exists: no system or global config, no hooks. */
|
|
27
|
+
const GIT_ENV = { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0" };
|
|
28
|
+
const GIT_ARGS = ["-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false"];
|
|
29
|
+
function writeNew(path, body, mode = 0o644) {
|
|
30
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
31
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
32
|
+
try {
|
|
33
|
+
writeSync(fd, body);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Make the world and run its build to the end. Never throws: a failure is the report. */
|
|
40
|
+
export async function runEvalWorld(one, input) {
|
|
41
|
+
const { worldDir, databaseFile } = input;
|
|
42
|
+
const repo = join(worldDir, "repo"), pool = join(worldDir, "pool");
|
|
43
|
+
const cli = evalCliEntry();
|
|
44
|
+
const say = async (argv, timeoutMs = 120_000) => {
|
|
45
|
+
const result = await run(cli.file, [...cli.args, ...argv, "--db", databaseFile, "--json"], { cwd: worldDir, timeoutMs, maxBuffer: 4 * 1024 * 1024 });
|
|
46
|
+
let body = {};
|
|
47
|
+
try {
|
|
48
|
+
body = JSON.parse(result.stdout);
|
|
49
|
+
}
|
|
50
|
+
catch { /* below */ }
|
|
51
|
+
if (result.code !== 0 || body["ok"] === false)
|
|
52
|
+
throw new Error(`${argv.slice(0, 2).join(" ")}: ${String(body["message"] ?? result.stderr).slice(0, 300)}`);
|
|
53
|
+
return body;
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(repo, { recursive: true });
|
|
57
|
+
for (const [path, body] of Object.entries(one.fixture))
|
|
58
|
+
writeNew(join(repo, ...path.split("/")), body);
|
|
59
|
+
// The worker later commits the agent's changes through checkoutGit. Keep the world's synthetic identity in its
|
|
60
|
+
// repository, where that helper carries it, instead of supplying it only to this first fixture commit.
|
|
61
|
+
for (const argv of [["init", "-q", "-b", "main"], ["config", "user.name", "Toolroll eval"], ["config", "user.email", "eval@toolroll.invalid"],
|
|
62
|
+
["config", "user.useConfigOnly", "true"], ["add", "-A"], ["commit", "-q", "-m", `Eval fixture ${one.id}`]]) {
|
|
63
|
+
const git = await run("git", [...GIT_ARGS, ...argv], { cwd: repo, env: GIT_ENV, timeoutMs: 30_000 });
|
|
64
|
+
if (git.code !== 0)
|
|
65
|
+
throw new Error(`git ${argv[0]}: ${git.stderr.slice(0, 200)}`);
|
|
66
|
+
}
|
|
67
|
+
const password = worldSecret();
|
|
68
|
+
const auth = ["--as", "eval", "--token", password];
|
|
69
|
+
await say(["approver", "add", "eval", "--password", password]);
|
|
70
|
+
const registered = await say(["runner", "register", "worker", "--repo", repo, ...auth]);
|
|
71
|
+
if (typeof registered["token"] !== "string")
|
|
72
|
+
throw new Error("the world's worker got no token");
|
|
73
|
+
for (const phase of ["plan", "build", "repair"])
|
|
74
|
+
await say(["config", "set", phase, "--provider", one.pin.provider, "--model", one.pin.model, ...auth]);
|
|
75
|
+
await say(["mode", "set", "--repo", repo, "--name", "standard", "--days", "1", ...auth]);
|
|
76
|
+
await say(["task", "add", one.title, "--id", "work", "--repo", repo]);
|
|
77
|
+
// The hidden checks and the plane's own state stay out of the agent's reach (sealed into the scope filed next).
|
|
78
|
+
await say(["project", "sandbox", "--repo", repo, "--read-deny", [EVALS_ROOT.replace(/\/$/, ""), ...input.deny].join(","), ...auth]);
|
|
79
|
+
await say(["verify", "set", "--repo", repo, "--command", "node --test", "--timeout-seconds", "120", "--yes", ...auth]);
|
|
80
|
+
await say(["task", "scope", "work", "--goal", one.goal, "--touches", one.touches.join(","), "--acceptance", `${one.goal.replace(/[|;]/g, ",").slice(0, 600)}|check`,
|
|
81
|
+
"--not", "Changing package or dependency files, or anything the goal doesn't ask for."]);
|
|
82
|
+
const shown = await say(["task", "show", "work"]);
|
|
83
|
+
const digest = shown["scope"]?.["digest"];
|
|
84
|
+
if (typeof digest !== "string")
|
|
85
|
+
throw new Error("the world's task has no scope to approve");
|
|
86
|
+
await say(["task", "approve", "work", "--yes", "--digest", digest, ...auth]);
|
|
87
|
+
writeNew(join(dirname(databaseFile), "worker-token"), String(registered["token"]), 0o600);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return { evalStage: "world-start", why: error instanceof Error ? error.message : String(error) };
|
|
91
|
+
}
|
|
92
|
+
// The agent's part: the worker builds (or parks with its question) until there is nothing left to do. Its outcome is
|
|
93
|
+
// read from the world's records by the runner, never from here.
|
|
94
|
+
await run(cli.file, [...cli.args, "watch", "--runner", "worker", "--token-file", join(dirname(databaseFile), "worker-token"), "--repo", repo, "--pool", pool,
|
|
95
|
+
"--for", "1000", "--tick-every", "200", "--reconcile-every", "1000", "--bridge-every", "3600000", "--db", databaseFile, "--json"], { cwd: worldDir, timeoutMs: input.timeoutMs, maxBuffer: 4 * 1024 * 1024 });
|
|
96
|
+
return { evalStage: "done", why: null };
|
|
97
|
+
}
|
package/dist/evals.d.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { type OracleVerdict } from "./eval-oracle.js";
|
|
2
|
+
import { type ReportedAmount } from "./reported-usage.js";
|
|
3
|
+
import { Store } from "./store.js";
|
|
4
|
+
export declare const EVALS_ROOT: string;
|
|
5
|
+
export declare const EVAL_K = 3;
|
|
6
|
+
/** An errored attempt gets one replacement when the plane confirms the error; at most this many a campaign. */
|
|
7
|
+
export declare const EVAL_MAX_REPLACEMENTS = 3;
|
|
8
|
+
/** Attempts at once within one campaign. */
|
|
9
|
+
export declare const EVAL_CONCURRENCY = 2;
|
|
10
|
+
export type EvalCaseKind = "build" | "ask";
|
|
11
|
+
export type EvalCase = {
|
|
12
|
+
id: string;
|
|
13
|
+
version: string;
|
|
14
|
+
dir: string;
|
|
15
|
+
kind: EvalCaseKind;
|
|
16
|
+
title: string;
|
|
17
|
+
/** What the task asks, as the agent's brief gets it. */
|
|
18
|
+
goal: string;
|
|
19
|
+
touches: string[];
|
|
20
|
+
pin: {
|
|
21
|
+
provider: "claude";
|
|
22
|
+
model: string;
|
|
23
|
+
};
|
|
24
|
+
maxDispatches: number;
|
|
25
|
+
timeoutMinutes: number;
|
|
26
|
+
/** build: the files the oracle copies from the built commit. ask: the options the question must offer (patterns). */
|
|
27
|
+
grade: {
|
|
28
|
+
files: string[];
|
|
29
|
+
} | {
|
|
30
|
+
options: string[];
|
|
31
|
+
};
|
|
32
|
+
/** The person who validated the expected outcome, or null: a draft, counted in no canonical campaign. */
|
|
33
|
+
validatedBy: string | null;
|
|
34
|
+
fixture: Record<string, string>;
|
|
35
|
+
};
|
|
36
|
+
export type EvalSet = {
|
|
37
|
+
ok: true;
|
|
38
|
+
root: string;
|
|
39
|
+
manifestDigest: string;
|
|
40
|
+
cases: EvalCase[];
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
problem: string;
|
|
44
|
+
};
|
|
45
|
+
/** Why a pin isn't an exact model id, or null. An alias, an empty or odd value, or a non-Claude provider is no pin. */
|
|
46
|
+
export declare function exactModelProblem(pin: {
|
|
47
|
+
provider?: unknown;
|
|
48
|
+
model?: unknown;
|
|
49
|
+
} | null | undefined): string | null;
|
|
50
|
+
/** The installed eval set: the manifest's cases, each read whole and versioned. Any case that can't be read, and any
|
|
51
|
+
* folder the manifest doesn't list, refuses the whole set. */
|
|
52
|
+
export declare function loadEvalSet(root?: string): EvalSet;
|
|
53
|
+
export type CampaignAsk = {
|
|
54
|
+
cases?: string[];
|
|
55
|
+
k?: number;
|
|
56
|
+
diagnostic?: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type CampaignPlan = {
|
|
59
|
+
canonical: boolean;
|
|
60
|
+
k: number;
|
|
61
|
+
cases: EvalCase[];
|
|
62
|
+
};
|
|
63
|
+
/** What a campaign runs. Canonical (nothing asked): every case, k = 3, refused while any case is a draft. Diagnostic
|
|
64
|
+
* (anything asked): a report only. Either way every case runs under an exact model pin. */
|
|
65
|
+
export declare function planCampaign(set: Extract<EvalSet, {
|
|
66
|
+
ok: true;
|
|
67
|
+
}>, ask: CampaignAsk): {
|
|
68
|
+
ok: true;
|
|
69
|
+
plan: CampaignPlan;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
problem: string;
|
|
73
|
+
};
|
|
74
|
+
export type Admission = {
|
|
75
|
+
ok: true;
|
|
76
|
+
id: string;
|
|
77
|
+
} | {
|
|
78
|
+
ok: false;
|
|
79
|
+
reason: "person-only" | "release-busy" | "already-running";
|
|
80
|
+
message: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Admit a campaign (eval or reviewer): a person proved now, nothing releasing or deploying, and no other canonical
|
|
84
|
+
* campaign running (canonical campaigns settle in the order they were admitted). One transaction: the proof, the checks
|
|
85
|
+
* and the row, with the runner that owns it (this process, on this computer) for recovery.
|
|
86
|
+
*/
|
|
87
|
+
export declare function admitEvalCampaign(store: Store, input: {
|
|
88
|
+
kind: "eval" | "reviewer";
|
|
89
|
+
canonical: boolean;
|
|
90
|
+
manifestDigest: string;
|
|
91
|
+
buildDigest: string;
|
|
92
|
+
version: string;
|
|
93
|
+
k: number;
|
|
94
|
+
sealed: unknown;
|
|
95
|
+
as: string;
|
|
96
|
+
token: string;
|
|
97
|
+
now: Date;
|
|
98
|
+
id?: string;
|
|
99
|
+
runner?: {
|
|
100
|
+
host: string;
|
|
101
|
+
pid: number;
|
|
102
|
+
started?: string | null;
|
|
103
|
+
};
|
|
104
|
+
}): Admission;
|
|
105
|
+
/** Record one attempt's start (its identity is unique). Refused when the campaign isn't running. */
|
|
106
|
+
export declare function startEvalAttempt(store: Store, input: {
|
|
107
|
+
campaign: string;
|
|
108
|
+
caseId: string;
|
|
109
|
+
caseVersion: string;
|
|
110
|
+
n: number;
|
|
111
|
+
replaces: number;
|
|
112
|
+
modelPinned: string;
|
|
113
|
+
now: Date;
|
|
114
|
+
}): {
|
|
115
|
+
ok: true;
|
|
116
|
+
attempt: number;
|
|
117
|
+
} | {
|
|
118
|
+
ok: false;
|
|
119
|
+
why: string;
|
|
120
|
+
};
|
|
121
|
+
export type EvalOutcome = "pass" | "fail" | "error" | "unevaluated";
|
|
122
|
+
/** One provider dispatch an attempt's world made: its record (`run:<id>`, `chat_turn:<id>`) or the gate slot no record
|
|
123
|
+
* accounts for (`slot:<n>`), what it was billed as, and the model it reported. */
|
|
124
|
+
export type EvalDispatchRecord = {
|
|
125
|
+
kind: string;
|
|
126
|
+
worldRef: string;
|
|
127
|
+
recorded: boolean;
|
|
128
|
+
amount: ReportedAmount;
|
|
129
|
+
modelReported: string | null;
|
|
130
|
+
};
|
|
131
|
+
export type AttemptResult = {
|
|
132
|
+
outcome: EvalOutcome;
|
|
133
|
+
why: string | null;
|
|
134
|
+
modelReported: string | null;
|
|
135
|
+
dispatches: number | null;
|
|
136
|
+
apiMicrousd: number | null;
|
|
137
|
+
subscriptionMicrousd: number | null;
|
|
138
|
+
usageUnknown: boolean;
|
|
139
|
+
turns: number | null;
|
|
140
|
+
tokensIn: number | null;
|
|
141
|
+
cacheRead: number | null;
|
|
142
|
+
cacheWrite: number | null;
|
|
143
|
+
tokensOut: number | null;
|
|
144
|
+
toolCalls: number | null;
|
|
145
|
+
wallMs: number;
|
|
146
|
+
invocations: EvalDispatchRecord[];
|
|
147
|
+
};
|
|
148
|
+
/** Record a finished attempt, once: its outcome, each dispatch as it was billed, and its totals. */
|
|
149
|
+
export declare function recordEvalAttempt(store: Store, attempt: number, result: AttemptResult, now: Date): void;
|
|
150
|
+
/** End a campaign, once: complete or incomplete. */
|
|
151
|
+
export declare function finishEvalCampaign(store: Store, id: string, state: "complete" | "incomplete", why: string | null, now: Date): void;
|
|
152
|
+
/** What a campaign spent, from its attempts as recorded: API dollars (null when any dispatch's money is unknown),
|
|
153
|
+
* subscription usage (null when any is unknown or unreported), and how many attempts had unknown spending. Never one
|
|
154
|
+
* total of the three. */
|
|
155
|
+
export declare function campaignSpending(store: Store, id: string): {
|
|
156
|
+
apiMicrousd: number | null;
|
|
157
|
+
subscriptionMicrousd: number | null;
|
|
158
|
+
unknownAttempts: number;
|
|
159
|
+
attempts: number;
|
|
160
|
+
};
|
|
161
|
+
export type WorldDispatches = {
|
|
162
|
+
invocations: EvalDispatchRecord[];
|
|
163
|
+
modelReported: string | null;
|
|
164
|
+
dispatches: number | null;
|
|
165
|
+
/** Asked past the case's limit (the gate refused it), or more dispatches than the limit recorded. */
|
|
166
|
+
overLimit: boolean;
|
|
167
|
+
/** A dispatch the gate and the records don't agree on: a record with no slot (it went around the gate), a slot with no
|
|
168
|
+
* record, or slots that can't be read. Its spending is unknown and the attempt is an error. */
|
|
169
|
+
unaccounted: boolean;
|
|
170
|
+
apiMicrousd: number | null;
|
|
171
|
+
subscriptionMicrousd: number | null;
|
|
172
|
+
usageUnknown: boolean;
|
|
173
|
+
turns: number | null;
|
|
174
|
+
tokensIn: number | null;
|
|
175
|
+
cacheRead: number | null;
|
|
176
|
+
cacheWrite: number | null;
|
|
177
|
+
tokensOut: number | null;
|
|
178
|
+
toolCalls: number | null;
|
|
179
|
+
};
|
|
180
|
+
/** Every provider dispatch a world made, from its own records (its database, opened read only; null when there is none
|
|
181
|
+
* or it can't be read) and its gate's slots: each with what it was billed as and the model it reported. A slot no
|
|
182
|
+
* record accounts for, and a record no slot does, is a dispatch with unknown money and an unknown model. */
|
|
183
|
+
export declare function collectWorld(world: Store | null, gateDir: string, maxDispatches: number, evidenceRoot: string, now: Date): WorldDispatches;
|
|
184
|
+
/** The installed build's identity: its version, a digest of every file of the code that is running (dist when
|
|
185
|
+
* installed, the sources in a checkout), and the service definitions that say which runtime the plane runs (a deploy
|
|
186
|
+
* rewrites them to another runtime's folder). A change between attempts, or before the end, stops the campaign as
|
|
187
|
+
* incomplete. */
|
|
188
|
+
export declare function installedBuildDigest(version: string, home?: string): string;
|
|
189
|
+
/** How an attempt's world is started: the child command (the installed binary's `evals attempt`, or a test's world). */
|
|
190
|
+
export type AttemptCommand = (input: {
|
|
191
|
+
campaign: string;
|
|
192
|
+
caseId: string;
|
|
193
|
+
n: number;
|
|
194
|
+
worldDir: string;
|
|
195
|
+
databaseFile: string;
|
|
196
|
+
gateDir: string;
|
|
197
|
+
}) => {
|
|
198
|
+
file: string;
|
|
199
|
+
args: string[];
|
|
200
|
+
env: Record<string, string>;
|
|
201
|
+
};
|
|
202
|
+
export type AttemptDeps = {
|
|
203
|
+
command: AttemptCommand;
|
|
204
|
+
/** The oracle (eval-oracle.ts runOracle by default: a separate process from the installed release). */
|
|
205
|
+
oracle?: (input: {
|
|
206
|
+
caseDir: string;
|
|
207
|
+
inputDir: string;
|
|
208
|
+
timeoutMs: number;
|
|
209
|
+
deny?: readonly string[];
|
|
210
|
+
}) => Promise<OracleVerdict>;
|
|
211
|
+
/** Where worlds are made (a folder the plane owns). */
|
|
212
|
+
workRoot: string;
|
|
213
|
+
/** How long the world's processes get to end after its child does, before the attempt is an error. */
|
|
214
|
+
settleMs?: number;
|
|
215
|
+
/** Called the moment the whole process tree is proven gone, before anything is read or graded (tests). */
|
|
216
|
+
onTreeGone?: () => void;
|
|
217
|
+
keepWorlds?: boolean;
|
|
218
|
+
now?: () => Date;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* One attempt: record its start, make the world, run its child to the end of its whole process tree, read what it
|
|
222
|
+
* did, grade it, record it. What the world dispatched is collected and recorded on every road out, the runner's own
|
|
223
|
+
* failure included, before the world is removed. Outcomes (plan, "Grading"): an error only where the plane itself
|
|
224
|
+
* recorded one (the world failed before the agent started, the provider refused or was down, the oracle failed, the
|
|
225
|
+
* world's processes wouldn't end, a dispatch the gate and the records don't agree on); the agent timing out, crashing
|
|
226
|
+
* or going past its dispatch limit is a fail; a reported model other than the pin is unevaluated.
|
|
227
|
+
*/
|
|
228
|
+
export declare function runEvalAttempt(store: Store, input: {
|
|
229
|
+
campaign: string;
|
|
230
|
+
one: EvalCase;
|
|
231
|
+
n: number;
|
|
232
|
+
replaces: number;
|
|
233
|
+
}, deps: AttemptDeps): Promise<{
|
|
234
|
+
attempt: number;
|
|
235
|
+
result: AttemptResult;
|
|
236
|
+
stopCampaign: string | null;
|
|
237
|
+
} | {
|
|
238
|
+
attempt: null;
|
|
239
|
+
why: string;
|
|
240
|
+
}>;
|
|
241
|
+
export type CampaignDeps = AttemptDeps & {
|
|
242
|
+
/** The installed build's identity now (installedBuildDigest): a change stops the campaign as incomplete. */
|
|
243
|
+
buildDigest: () => string;
|
|
244
|
+
/** The installed eval set now (loadEvalSet): a change stops the campaign as incomplete too. */
|
|
245
|
+
evalSet?: () => EvalSet;
|
|
246
|
+
concurrency?: number;
|
|
247
|
+
};
|
|
248
|
+
/** Run an admitted campaign to its end: every sealed attempt (EVAL_CONCURRENCY at once), one replacement for an
|
|
249
|
+
* attempt the plane recorded an error for (at most EVAL_MAX_REPLACEMENTS a campaign), then complete — or incomplete
|
|
250
|
+
* when the installed build or eval set changed (checked before every attempt and again at the end), a deploy is
|
|
251
|
+
* draining, or the runner itself failed. */
|
|
252
|
+
export declare function runEvalCampaign(store: Store, campaign: string, cases: readonly EvalCase[], deps: CampaignDeps): Promise<{
|
|
253
|
+
state: "complete" | "incomplete";
|
|
254
|
+
why: string | null;
|
|
255
|
+
}>;
|
|
256
|
+
/**
|
|
257
|
+
* Recover campaigns whose runner stopped (Codex round 1 #12): a running campaign whose runner was this computer's and
|
|
258
|
+
* whose process is gone. Each unfinished attempt is recorded as an error with whatever its world recorded (its spending
|
|
259
|
+
* unknown where the records can't say), once its world's process group is proven gone; then the campaign ends
|
|
260
|
+
* incomplete. A campaign whose runner is alive, ran on another computer, or still has a world running stays as it is.
|
|
261
|
+
*/
|
|
262
|
+
export declare function recoverEvalCampaigns(store: Store, now: Date, options?: {
|
|
263
|
+
host?: string;
|
|
264
|
+
alive?: (pid: number, started: string | null) => boolean;
|
|
265
|
+
}): {
|
|
266
|
+
id: string;
|
|
267
|
+
recovered: boolean;
|
|
268
|
+
why: string;
|
|
269
|
+
}[];
|
|
270
|
+
/** A process's start time as the system reports it (`ps -o lstart`, whole seconds, in UTC and the C locale), or null
|
|
271
|
+
* when it can't be read: the identity a saved pid is compared by, so a later process that reuses the number is never
|
|
272
|
+
* taken for the one recorded. */
|
|
273
|
+
export declare function processStartOf(pid: number): string | null;
|
|
274
|
+
/** Whether a campaign's runner may still be running (PR #263 review): its pid is held, and by the same process — its
|
|
275
|
+
* start time, where one was recorded, matches. A pid now held by a process that started at another time is someone
|
|
276
|
+
* else's. A start that can't be read now counts as alive (a person ends such a campaign: `evals recover --abandon`). */
|
|
277
|
+
export declare function runnerMayBeAlive(pid: number, started: string | null): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* End a stuck campaign by a person's word (PR #263 review): a campaign recovery can't settle (its runner can't be
|
|
280
|
+
* proven gone, or a world's processes can't). A person proved now with their password (a lead token or remembered
|
|
281
|
+
* login is no proof); in one transaction every unfinished attempt is recorded as an error with unknown spending, the
|
|
282
|
+
* campaign's spending is marked unknown, and it ends incomplete. No process is signalled and no world is read or
|
|
283
|
+
* removed: whatever is still running is left to the person.
|
|
284
|
+
*/
|
|
285
|
+
export declare function abandonEvalCampaign(store: Store, input: {
|
|
286
|
+
id: string;
|
|
287
|
+
as: string;
|
|
288
|
+
token: string;
|
|
289
|
+
now: Date;
|
|
290
|
+
}): {
|
|
291
|
+
ok: true;
|
|
292
|
+
attempts: number;
|
|
293
|
+
} | {
|
|
294
|
+
ok: false;
|
|
295
|
+
reason: "person-only" | "not-running";
|
|
296
|
+
message: string;
|
|
297
|
+
};
|
|
298
|
+
/** A random hex id, for the world's throwaway approver password. */
|
|
299
|
+
export declare const worldSecret: () => string;
|