@otto-code/brain 0.7.5
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/bin/otto-brain +2 -0
- package/dist/bench/context-corpus.d.ts +58 -0
- package/dist/bench/context-corpus.js +210 -0
- package/dist/bench/corpus.d.ts +31 -0
- package/dist/bench/corpus.js +379 -0
- package/dist/bench/curated-repos.d.ts +35 -0
- package/dist/bench/curated-repos.js +41 -0
- package/dist/bench/health.d.ts +50 -0
- package/dist/bench/health.js +112 -0
- package/dist/bench/index.d.ts +103 -0
- package/dist/bench/index.js +209 -0
- package/dist/bench/mine.d.ts +32 -0
- package/dist/bench/mine.js +101 -0
- package/dist/bench/repo-task.d.ts +93 -0
- package/dist/bench/repo-task.js +380 -0
- package/dist/bench/repo.d.ts +125 -0
- package/dist/bench/repo.js +195 -0
- package/dist/bench/rescore.d.ts +41 -0
- package/dist/bench/rescore.js +127 -0
- package/dist/bench/tasks.d.ts +124 -0
- package/dist/bench/tasks.js +1249 -0
- package/dist/bench/verify.d.ts +142 -0
- package/dist/bench/verify.js +350 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +72 -0
- package/dist/commands/bench.d.ts +20 -0
- package/dist/commands/bench.js +222 -0
- package/dist/commands/calibrate.d.ts +19 -0
- package/dist/commands/calibrate.js +75 -0
- package/dist/commands/catalog.d.ts +32 -0
- package/dist/commands/catalog.js +48 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +90 -0
- package/dist/commands/lifecycle.d.ts +29 -0
- package/dist/commands/lifecycle.js +194 -0
- package/dist/commands/pull.d.ts +22 -0
- package/dist/commands/pull.js +135 -0
- package/dist/commands/report.d.ts +10 -0
- package/dist/commands/report.js +34 -0
- package/dist/commands/rescore.d.ts +11 -0
- package/dist/commands/rescore.js +48 -0
- package/dist/commands/runtime.d.ts +21 -0
- package/dist/commands/runtime.js +62 -0
- package/dist/commands/scan.d.ts +24 -0
- package/dist/commands/scan.js +55 -0
- package/dist/commands/search.d.ts +31 -0
- package/dist/commands/search.js +135 -0
- package/dist/commands/share.d.ts +26 -0
- package/dist/commands/share.js +148 -0
- package/dist/commands/sweep.d.ts +21 -0
- package/dist/commands/sweep.js +69 -0
- package/dist/commands/ui.d.ts +9 -0
- package/dist/commands/ui.js +28 -0
- package/dist/config/env.d.ts +10 -0
- package/dist/config/env.js +48 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +9 -0
- package/dist/config/otto-home.d.ts +2 -0
- package/dist/config/otto-home.js +24 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/private-files.d.ts +3 -0
- package/dist/config/private-files.js +32 -0
- package/dist/config/profiles.d.ts +34 -0
- package/dist/config/profiles.js +116 -0
- package/dist/config/schema.d.ts +1035 -0
- package/dist/config/schema.js +191 -0
- package/dist/config/store.d.ts +11 -0
- package/dist/config/store.js +73 -0
- package/dist/gguf.d.ts +61 -0
- package/dist/gguf.js +231 -0
- package/dist/gpu.d.ts +12 -0
- package/dist/gpu.js +49 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/models/dirs.d.ts +9 -0
- package/dist/models/dirs.js +21 -0
- package/dist/models/download.d.ts +31 -0
- package/dist/models/download.js +105 -0
- package/dist/models/enrich.d.ts +36 -0
- package/dist/models/enrich.js +68 -0
- package/dist/models/hf.d.ts +62 -0
- package/dist/models/hf.js +114 -0
- package/dist/models/index.d.ts +15 -0
- package/dist/models/index.js +49 -0
- package/dist/models/manage.d.ts +27 -0
- package/dist/models/manage.js +112 -0
- package/dist/models/pick.d.ts +3 -0
- package/dist/models/pick.js +36 -0
- package/dist/models/scan.d.ts +13 -0
- package/dist/models/scan.js +185 -0
- package/dist/ops/archive.d.ts +34 -0
- package/dist/ops/archive.js +105 -0
- package/dist/ops/calibrate.d.ts +49 -0
- package/dist/ops/calibrate.js +74 -0
- package/dist/ops/report.d.ts +26 -0
- package/dist/ops/report.js +532 -0
- package/dist/ops/results.d.ts +207 -0
- package/dist/ops/results.js +250 -0
- package/dist/ops/sweep.d.ts +77 -0
- package/dist/ops/sweep.js +126 -0
- package/dist/output/index.d.ts +5 -0
- package/dist/output/index.js +5 -0
- package/dist/output/render.d.ts +4 -0
- package/dist/output/render.js +96 -0
- package/dist/output/types.d.ts +52 -0
- package/dist/output/types.js +9 -0
- package/dist/output/with-output.d.ts +3 -0
- package/dist/output/with-output.js +42 -0
- package/dist/run.d.ts +5 -0
- package/dist/run.js +19 -0
- package/dist/runtime/args.d.ts +21 -0
- package/dist/runtime/args.js +71 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.js +41 -0
- package/dist/runtime/lmstudio.d.ts +10 -0
- package/dist/runtime/lmstudio.js +88 -0
- package/dist/runtime/managed.d.ts +26 -0
- package/dist/runtime/managed.js +148 -0
- package/dist/service/model-selector.d.ts +36 -0
- package/dist/service/model-selector.js +123 -0
- package/dist/service/pid-lock.d.ts +18 -0
- package/dist/service/pid-lock.js +50 -0
- package/dist/service/router.d.ts +144 -0
- package/dist/service/router.js +746 -0
- package/dist/service/scheduler.d.ts +74 -0
- package/dist/service/scheduler.js +110 -0
- package/dist/service/serve.d.ts +41 -0
- package/dist/service/serve.js +278 -0
- package/dist/service/supervisor.d.ts +67 -0
- package/dist/service/supervisor.js +222 -0
- package/dist/service/tailscale.d.ts +19 -0
- package/dist/service/tailscale.js +62 -0
- package/dist/service/tls.d.ts +77 -0
- package/dist/service/tls.js +192 -0
- package/dist/sysmon.d.ts +50 -0
- package/dist/sysmon.js +103 -0
- package/dist/tui/app.d.ts +208 -0
- package/dist/tui/app.js +1801 -0
- package/dist/tui/screen.d.ts +72 -0
- package/dist/tui/screen.js +176 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.js +7 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/dist/vram.d.ts +67 -0
- package/dist/vram.js +132 -0
- package/package.json +58 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
/**
|
|
7
|
+
* Repository control for SWE-bench-style tasks.
|
|
8
|
+
*
|
|
9
|
+
* One working copy is reused for every task rather than re-cloning: reset the
|
|
10
|
+
* tracked files, keep the ignored ones. That is deliberate and cheap -
|
|
11
|
+
* `git clean -fd` (crucially WITHOUT -x) removes the model's stray files while
|
|
12
|
+
* preserving node_modules and dist. Measured on otto-code: reset under a
|
|
13
|
+
* second, warm install 1.7s, build 6.1s, full protocol suite 4.3s.
|
|
14
|
+
*
|
|
15
|
+
* Tasks therefore serialise on the working copy, which costs nothing real:
|
|
16
|
+
* only one model fits in VRAM at a time, so they were already serialised.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_TIMEOUT = 300000;
|
|
19
|
+
export function run(command, args, { cwd, timeout = DEFAULT_TIMEOUT, env } = {}) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
execFile(command, args, {
|
|
22
|
+
cwd,
|
|
23
|
+
timeout,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
26
|
+
env: env || process.env,
|
|
27
|
+
shell: process.platform === "win32", // npm/npx are .cmd shims on Windows
|
|
28
|
+
}, (error, stdout, stderr) => {
|
|
29
|
+
resolve({
|
|
30
|
+
ok: !error,
|
|
31
|
+
code: error?.code ?? 0,
|
|
32
|
+
timedOut: Boolean(error?.killed),
|
|
33
|
+
stdout: String(stdout),
|
|
34
|
+
stderr: String(stderr),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export class Repo {
|
|
40
|
+
/**
|
|
41
|
+
* @param {object} options
|
|
42
|
+
* @param {string} options.dir working copy
|
|
43
|
+
* @param {string} options.workspace npm workspace name, e.g. @otto-code/protocol
|
|
44
|
+
* @param {string} options.workspaceDir its path relative to dir
|
|
45
|
+
*/
|
|
46
|
+
constructor({ dir, workspace, workspaceDir }) {
|
|
47
|
+
this.dir = path.resolve(dir);
|
|
48
|
+
this.workspace = workspace;
|
|
49
|
+
this.workspaceDir = workspaceDir;
|
|
50
|
+
this.lastLockHash = null;
|
|
51
|
+
}
|
|
52
|
+
git(args, options = {}) {
|
|
53
|
+
return run("git", args, { cwd: this.dir, ...options });
|
|
54
|
+
}
|
|
55
|
+
/** Discard all local modifications and untracked files, keeping ignored ones. */
|
|
56
|
+
async reset(sha) {
|
|
57
|
+
const hard = await this.git(["reset", "--hard", sha]);
|
|
58
|
+
if (!hard.ok)
|
|
59
|
+
throw new Error(`git reset --hard ${sha} failed: ${hard.stderr.trim()}`);
|
|
60
|
+
// No -x: node_modules and dist are ignored and must survive.
|
|
61
|
+
const clean = await this.git(["clean", "-fd"]);
|
|
62
|
+
if (!clean.ok)
|
|
63
|
+
throw new Error(`git clean failed: ${clean.stderr.trim()}`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/** Bring specific paths in from another commit - used to apply the oracle. */
|
|
67
|
+
async checkoutPaths(sha, paths) {
|
|
68
|
+
if (!paths.length)
|
|
69
|
+
return true;
|
|
70
|
+
const result = await this.git(["checkout", sha, "--", ...paths]);
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
throw new Error(`git checkout ${sha} -- <paths> failed: ${result.stderr.trim()}`);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
async headSha() {
|
|
76
|
+
const result = await this.git(["rev-parse", "HEAD"]);
|
|
77
|
+
return result.stdout.trim();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Install only when the lockfile actually changed since the last install.
|
|
81
|
+
* Across neighbouring commits this is almost always a no-op.
|
|
82
|
+
*/
|
|
83
|
+
async ensureDependencies() {
|
|
84
|
+
const lock = path.join(this.dir, "package-lock.json");
|
|
85
|
+
let hash = "none";
|
|
86
|
+
try {
|
|
87
|
+
hash = crypto.createHash("sha1").update(fs.readFileSync(lock)).digest("hex");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* no lockfile: fall through and install once */
|
|
91
|
+
}
|
|
92
|
+
if (hash === this.lastLockHash)
|
|
93
|
+
return { installed: false };
|
|
94
|
+
// --ignore-scripts: the root `prepare` hook runs lefthook (git hooks),
|
|
95
|
+
// which fails here and is not wanted in a benchmark harness anyway.
|
|
96
|
+
const result = await run("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
97
|
+
cwd: this.dir,
|
|
98
|
+
timeout: 900000,
|
|
99
|
+
});
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
throw new Error(`npm install failed: ${(result.stderr || result.stdout).slice(-500)}`);
|
|
102
|
+
}
|
|
103
|
+
this.lastLockHash = hash;
|
|
104
|
+
return { installed: true };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build the workspace. Required, not optional: some tests import through the
|
|
108
|
+
* package's own `exports` map into dist/, and fail at import time otherwise.
|
|
109
|
+
*/
|
|
110
|
+
async build() {
|
|
111
|
+
const result = await run("npm", ["run", "build", "--workspace", this.workspace], {
|
|
112
|
+
cwd: this.dir,
|
|
113
|
+
timeout: 600000,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
ok: result.ok,
|
|
117
|
+
output: (result.stderr || result.stdout).slice(-2000),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run the workspace test suite and return per-test outcomes.
|
|
122
|
+
*
|
|
123
|
+
* `files` restricts the run to those test paths (passed to vitest as filters).
|
|
124
|
+
* The SWE-bench oracle only ever scores its own test files, so scoping to them
|
|
125
|
+
* avoids running the whole workspace suite - faster, and it keeps the harness
|
|
126
|
+
* within the "never run the full suite" rule.
|
|
127
|
+
*
|
|
128
|
+
* vitest's --outputFile needs a native path; a POSIX-style /tmp path silently
|
|
129
|
+
* produces no file on Windows.
|
|
130
|
+
*/
|
|
131
|
+
async test({ timeout = 600000, files, } = {}) {
|
|
132
|
+
const outFile = path.join(os.tmpdir(), `vitest-${process.pid}-${Date.now()}.json`);
|
|
133
|
+
const cwd = path.join(this.dir, this.workspaceDir);
|
|
134
|
+
const args = ["vitest", "run", "--reporter=json", `--outputFile=${outFile}`];
|
|
135
|
+
if (files && files.length)
|
|
136
|
+
args.push(...files);
|
|
137
|
+
const result = await run("npx", args, { cwd, timeout });
|
|
138
|
+
let report;
|
|
139
|
+
try {
|
|
140
|
+
report = JSON.parse(fs.readFileSync(outFile, "utf8"));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
parsed: false,
|
|
146
|
+
passed: new Set(),
|
|
147
|
+
failed: new Set(),
|
|
148
|
+
fileFailures: [],
|
|
149
|
+
error: (result.stderr || result.stdout).slice(-1500),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
fs.rmSync(outFile, { force: true });
|
|
154
|
+
}
|
|
155
|
+
return { ...parseVitest(report, this.dir), ok: result.ok, parsed: true };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Turn a vitest JSON report into pass/fail sets keyed by a stable test id.
|
|
160
|
+
*
|
|
161
|
+
* A file that fails to import has no assertion results at all - those are
|
|
162
|
+
* recorded separately, because "no tests ran" is not the same as "tests failed"
|
|
163
|
+
* and must not be mistaken for a passing baseline.
|
|
164
|
+
*/
|
|
165
|
+
export function parseVitest(report, repoDir) {
|
|
166
|
+
const passed = new Set();
|
|
167
|
+
const failed = new Set();
|
|
168
|
+
const fileFailures = [];
|
|
169
|
+
for (const suite of report.testResults || []) {
|
|
170
|
+
const file = path.relative(repoDir, suite.name || "").replace(/\\/g, "/");
|
|
171
|
+
const assertions = suite.assertionResults || [];
|
|
172
|
+
if (!assertions.length && suite.status !== "passed") {
|
|
173
|
+
fileFailures.push({ file, message: (suite.message || "").slice(0, 500) });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const assertion of assertions) {
|
|
177
|
+
const id = `${file}::${assertion.fullName}`;
|
|
178
|
+
if (assertion.status === "passed")
|
|
179
|
+
passed.add(id);
|
|
180
|
+
else
|
|
181
|
+
failed.add(id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
passed,
|
|
186
|
+
failed,
|
|
187
|
+
fileFailures,
|
|
188
|
+
totals: {
|
|
189
|
+
total: report.numTotalTests ?? passed.size + failed.size,
|
|
190
|
+
passed: report.numPassedTests ?? passed.size,
|
|
191
|
+
failed: report.numFailedTests ?? failed.size,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=repo.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-grade archived runs with the current scorer.
|
|
3
|
+
*
|
|
4
|
+
* Only tasks whose score is a pure function of the stored exchange can be
|
|
5
|
+
* replayed. Timing-based tasks (depth scaling) cannot - their result depends on
|
|
6
|
+
* the hardware state at the time - and are reported as not re-scorable rather
|
|
7
|
+
* than silently reused or zeroed.
|
|
8
|
+
*/
|
|
9
|
+
declare const RESCORABLE: Set<string>;
|
|
10
|
+
/** A re-scored task entry within a run. */
|
|
11
|
+
export interface RescoreTaskResult {
|
|
12
|
+
taskId: string;
|
|
13
|
+
score: number | null;
|
|
14
|
+
summary: string;
|
|
15
|
+
detail?: unknown;
|
|
16
|
+
skipped?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The re-scored result of one archived run. */
|
|
19
|
+
export interface RescoreRunResult {
|
|
20
|
+
archiveId: string;
|
|
21
|
+
tasks: RescoreTaskResult[];
|
|
22
|
+
}
|
|
23
|
+
/** Progress event emitted while re-scoring. */
|
|
24
|
+
export interface RescoreProgress {
|
|
25
|
+
phase: "start" | "done";
|
|
26
|
+
archiveId?: string;
|
|
27
|
+
tasks?: RescoreTaskResult[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @returns {{archiveId:string, tasks:Array, changed:Array}}
|
|
31
|
+
*/
|
|
32
|
+
export declare function rescoreRun(archiveId: string, { execute }?: {
|
|
33
|
+
execute?: boolean;
|
|
34
|
+
}): Promise<RescoreRunResult>;
|
|
35
|
+
/** Re-score every archived run and report where the new scorer disagrees. */
|
|
36
|
+
export declare function rescoreAll({ execute, onProgress, }?: {
|
|
37
|
+
execute?: boolean;
|
|
38
|
+
onProgress?: (event: RescoreProgress) => void;
|
|
39
|
+
}): Promise<RescoreRunResult[]>;
|
|
40
|
+
export { RESCORABLE };
|
|
41
|
+
//# sourceMappingURL=rescore.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as archive from "../ops/archive.js";
|
|
2
|
+
import { TOOL_CASES } from "./tasks.js";
|
|
3
|
+
import { verifyPython, verifyToolCall } from "./verify.js";
|
|
4
|
+
/**
|
|
5
|
+
* Re-grade archived runs with the current scorer.
|
|
6
|
+
*
|
|
7
|
+
* Only tasks whose score is a pure function of the stored exchange can be
|
|
8
|
+
* replayed. Timing-based tasks (depth scaling) cannot - their result depends on
|
|
9
|
+
* the hardware state at the time - and are reported as not re-scorable rather
|
|
10
|
+
* than silently reused or zeroed.
|
|
11
|
+
*/
|
|
12
|
+
const RESCORABLE = new Set(["long-horizon", "tool-calling"]);
|
|
13
|
+
const LONG_HORIZON_FILES = ["lru.py", "metrics.py", "test_lru.py"];
|
|
14
|
+
function firstUserContent(request) {
|
|
15
|
+
const messages = request?.messages || [];
|
|
16
|
+
const user = messages.find((m) => m.role === "user");
|
|
17
|
+
return typeof user?.content === "string" ? user.content : "";
|
|
18
|
+
}
|
|
19
|
+
async function rescoreLongHorizon(exchanges, { execute }) {
|
|
20
|
+
const last = exchanges[exchanges.length - 1];
|
|
21
|
+
const choice = last?.response?.choices?.[0] || {};
|
|
22
|
+
const content = choice.message?.content || "";
|
|
23
|
+
const reasoning = choice.message?.reasoning_content || "";
|
|
24
|
+
if (!content) {
|
|
25
|
+
return {
|
|
26
|
+
score: 0,
|
|
27
|
+
summary: reasoning ? `no content - all tokens went to reasoning` : "empty response",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const verification = await verifyPython(content, {
|
|
31
|
+
expectedFiles: LONG_HORIZON_FILES,
|
|
32
|
+
execute,
|
|
33
|
+
});
|
|
34
|
+
const fileScore = (LONG_HORIZON_FILES.length - verification.filesMissing.length) / LONG_HORIZON_FILES.length;
|
|
35
|
+
const compileScore = verification.compiled ? 1 : 0;
|
|
36
|
+
let testScore = 0;
|
|
37
|
+
if (verification.testsRun && verification.testsTotal) {
|
|
38
|
+
testScore = (verification.testsPassed ?? 0) / verification.testsTotal;
|
|
39
|
+
}
|
|
40
|
+
else if (!execute && verification.compiled) {
|
|
41
|
+
testScore = null;
|
|
42
|
+
}
|
|
43
|
+
const parts = [fileScore * 0.3, compileScore * 0.3];
|
|
44
|
+
let weight = 0.6;
|
|
45
|
+
if (testScore !== null) {
|
|
46
|
+
parts.push(testScore * 0.4);
|
|
47
|
+
weight = 1.0;
|
|
48
|
+
}
|
|
49
|
+
const penalty = Math.min(0.2, verification.placeholders * 0.1);
|
|
50
|
+
const score = Math.max(0, parts.reduce((a, b) => a + b, 0) / weight - penalty);
|
|
51
|
+
const bits = [
|
|
52
|
+
`${LONG_HORIZON_FILES.length - verification.filesMissing.length}/${LONG_HORIZON_FILES.length} files`,
|
|
53
|
+
verification.compiled ? "compiles" : "syntax errors",
|
|
54
|
+
];
|
|
55
|
+
if (verification.testsRun)
|
|
56
|
+
bits.push(`${verification.testsPassed}/${verification.testsTotal} tests pass`);
|
|
57
|
+
else if (!execute)
|
|
58
|
+
bits.push("tests not executed");
|
|
59
|
+
if (verification.nameCollisions?.length)
|
|
60
|
+
bits.push(`${verification.nameCollisions.length} name collisions`);
|
|
61
|
+
return { score, summary: bits.join(", ") };
|
|
62
|
+
}
|
|
63
|
+
function rescoreToolCalling(exchanges) {
|
|
64
|
+
const outcomes = [];
|
|
65
|
+
for (const exchange of exchanges) {
|
|
66
|
+
const prompt = firstUserContent(exchange.request);
|
|
67
|
+
// Match the archived exchange back to its case by prompt text, so a
|
|
68
|
+
// reordering of TOOL_CASES cannot silently mis-grade history.
|
|
69
|
+
const testCase = TOOL_CASES.find((c) => c.prompt === prompt);
|
|
70
|
+
if (!testCase)
|
|
71
|
+
continue;
|
|
72
|
+
const message = exchange.response?.choices?.[0]?.message || {};
|
|
73
|
+
const verdict = verifyToolCall(message.tool_calls, testCase.expect);
|
|
74
|
+
outcomes.push({ id: testCase.id, ok: verdict.ok, reason: verdict.reason || null });
|
|
75
|
+
}
|
|
76
|
+
if (!outcomes.length)
|
|
77
|
+
return { score: null, summary: "no matching cases in archive" };
|
|
78
|
+
const passed = outcomes.filter((o) => o.ok).length;
|
|
79
|
+
const failed = outcomes
|
|
80
|
+
.filter((o) => !o.ok)
|
|
81
|
+
.map((o) => `${o.id.replace(/^tools\//, "")} (${o.reason ?? "failed"})`);
|
|
82
|
+
return {
|
|
83
|
+
score: passed / outcomes.length,
|
|
84
|
+
summary: `${passed}/${outcomes.length} tool calls correct` +
|
|
85
|
+
(failed.length ? ` - missed: ${failed.join("; ")}` : ""),
|
|
86
|
+
detail: outcomes,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @returns {{archiveId:string, tasks:Array, changed:Array}}
|
|
91
|
+
*/
|
|
92
|
+
export async function rescoreRun(archiveId, { execute = true } = {}) {
|
|
93
|
+
const byTask = archive.load(archiveId);
|
|
94
|
+
const tasks = [];
|
|
95
|
+
for (const [taskId, exchanges] of Object.entries(byTask)) {
|
|
96
|
+
if (!RESCORABLE.has(taskId)) {
|
|
97
|
+
tasks.push({
|
|
98
|
+
taskId,
|
|
99
|
+
score: null,
|
|
100
|
+
summary: "not re-scorable from a transcript",
|
|
101
|
+
skipped: true,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (taskId === "long-horizon") {
|
|
106
|
+
tasks.push({ taskId, ...(await rescoreLongHorizon(exchanges, { execute })) });
|
|
107
|
+
}
|
|
108
|
+
else if (taskId === "tool-calling") {
|
|
109
|
+
tasks.push({ taskId, ...rescoreToolCalling(exchanges) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { archiveId, tasks };
|
|
113
|
+
}
|
|
114
|
+
/** Re-score every archived run and report where the new scorer disagrees. */
|
|
115
|
+
export async function rescoreAll({ execute = true, onProgress = () => { }, } = {}) {
|
|
116
|
+
const runs = archive.list();
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const id of runs) {
|
|
119
|
+
onProgress({ phase: "start", archiveId: id });
|
|
120
|
+
const result = await rescoreRun(id, { execute });
|
|
121
|
+
results.push(result);
|
|
122
|
+
onProgress({ phase: "done", ...result });
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
export { RESCORABLE };
|
|
127
|
+
//# sourceMappingURL=rescore.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ToolCall, ToolCallExpectation } from "./verify.js";
|
|
2
|
+
/**
|
|
3
|
+
* Benchmark tasks for agentic coding on this machine.
|
|
4
|
+
*
|
|
5
|
+
* Every task scores 0..1 from something checkable - a tool call with the right
|
|
6
|
+
* name and arguments, code the interpreter accepts, tests that actually pass -
|
|
7
|
+
* rather than a subjective read of the output.
|
|
8
|
+
*/
|
|
9
|
+
/** A single JSON-schema parameter of a tool. */
|
|
10
|
+
export interface ToolParameterSpec {
|
|
11
|
+
type: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
/** The parameters object of a tool function. */
|
|
15
|
+
export interface ToolFunctionParameters {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: Record<string, ToolParameterSpec>;
|
|
18
|
+
required: string[];
|
|
19
|
+
}
|
|
20
|
+
/** An OpenAI-style function tool. */
|
|
21
|
+
export interface ToolFunction {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: ToolFunctionParameters;
|
|
25
|
+
}
|
|
26
|
+
/** A tool exposed to the model. */
|
|
27
|
+
export interface Tool {
|
|
28
|
+
type: string;
|
|
29
|
+
function: ToolFunction;
|
|
30
|
+
}
|
|
31
|
+
/** A message sent to the chat endpoint. */
|
|
32
|
+
export interface ChatRequestMessage {
|
|
33
|
+
role: string;
|
|
34
|
+
content: string | null;
|
|
35
|
+
tool_calls?: ToolCall[];
|
|
36
|
+
tool_call_id?: string;
|
|
37
|
+
}
|
|
38
|
+
/** The payload POSTed to /v1/chat/completions. */
|
|
39
|
+
export interface ChatPayload {
|
|
40
|
+
messages: ChatRequestMessage[];
|
|
41
|
+
tools?: Tool[];
|
|
42
|
+
max_tokens?: number;
|
|
43
|
+
temperature?: number;
|
|
44
|
+
}
|
|
45
|
+
/** A message returned by the model. */
|
|
46
|
+
export interface ChatMessage {
|
|
47
|
+
role?: string;
|
|
48
|
+
content?: string | null;
|
|
49
|
+
reasoning_content?: string;
|
|
50
|
+
tool_calls?: ToolCall[];
|
|
51
|
+
}
|
|
52
|
+
/** Token accounting reported by the endpoint. */
|
|
53
|
+
export interface ChatUsage {
|
|
54
|
+
prompt_tokens?: number;
|
|
55
|
+
completion_tokens?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Timing information reported by llama-server. */
|
|
58
|
+
export interface ChatTimings {
|
|
59
|
+
prompt_ms?: number;
|
|
60
|
+
prompt_per_second?: number;
|
|
61
|
+
predicted_per_second?: number;
|
|
62
|
+
}
|
|
63
|
+
/** One choice in a chat completion response. */
|
|
64
|
+
export interface ChatChoice {
|
|
65
|
+
message?: ChatMessage;
|
|
66
|
+
finish_reason?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A chat completion response (with an `error` slot for a failed request). */
|
|
69
|
+
export interface ChatResponse {
|
|
70
|
+
choices?: ChatChoice[];
|
|
71
|
+
usage?: ChatUsage;
|
|
72
|
+
timings?: ChatTimings;
|
|
73
|
+
error?: string;
|
|
74
|
+
}
|
|
75
|
+
/** The function each task uses to talk to the endpoint. */
|
|
76
|
+
export interface ChatFn {
|
|
77
|
+
(payload: ChatPayload): Promise<ChatResponse>;
|
|
78
|
+
}
|
|
79
|
+
/** Context handed to each task's run(). */
|
|
80
|
+
export interface TaskRunContext {
|
|
81
|
+
chat: ChatFn;
|
|
82
|
+
execute?: boolean;
|
|
83
|
+
depths?: number[];
|
|
84
|
+
concurrency?: number;
|
|
85
|
+
/**
|
|
86
|
+
* The model's reasoning-token budget, so tasks can size their response cap
|
|
87
|
+
* above it. A thinking model given a max_tokens smaller than its budget spends
|
|
88
|
+
* the whole allowance reasoning and returns empty content. Null/unset means
|
|
89
|
+
* unknown (e.g. an arbitrary --endpoint), and tasks fall back to a safe cap.
|
|
90
|
+
*/
|
|
91
|
+
reasoningBudget?: number | null;
|
|
92
|
+
/**
|
|
93
|
+
* The model's loaded context window (tokens). Long-horizon tasks report peak
|
|
94
|
+
* prompt-token usage as a fraction of this, so a run says not just whether the
|
|
95
|
+
* model fixed the bug but how much context it held while doing it. Null/unset
|
|
96
|
+
* means unknown (e.g. an arbitrary --endpoint).
|
|
97
|
+
*/
|
|
98
|
+
contextWindow?: number | null;
|
|
99
|
+
}
|
|
100
|
+
/** What a task returns after running. */
|
|
101
|
+
export interface TaskOutcome {
|
|
102
|
+
score: number;
|
|
103
|
+
summary: string;
|
|
104
|
+
detail?: unknown;
|
|
105
|
+
}
|
|
106
|
+
/** A single benchmark task. */
|
|
107
|
+
export interface Task {
|
|
108
|
+
id: string;
|
|
109
|
+
category: string;
|
|
110
|
+
weight: number;
|
|
111
|
+
description: string;
|
|
112
|
+
run(ctx: TaskRunContext): Promise<TaskOutcome>;
|
|
113
|
+
}
|
|
114
|
+
/** A single tool-calling test case. */
|
|
115
|
+
export interface ToolCase {
|
|
116
|
+
id: string;
|
|
117
|
+
prompt: string;
|
|
118
|
+
expect: ToolCallExpectation;
|
|
119
|
+
}
|
|
120
|
+
export declare const TOOLS: Tool[];
|
|
121
|
+
export declare const TOOL_CASES: ToolCase[];
|
|
122
|
+
export declare const LONG_TASK_PROMPT: string;
|
|
123
|
+
export declare const TASKS: Task[];
|
|
124
|
+
//# sourceMappingURL=tasks.d.ts.map
|