amness 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +212 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +693 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +54 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +649 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +293 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +196 -0
- package/docs/headless.md +183 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +276 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +237 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +72 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// `amness bench`: a bake-off between coding-agent CLIs on the same tasks, graded by the same hidden checks.
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { PRESETS, adversarialCases, analyze, bench, builtinCases, configFileTemplate, describePlan, formatReport, hardCases, installed, loadCases, loadSuite, suiteToCases } from "../src/index.js";
|
|
14
|
+
import { bold, dim, green, red, yellow } from "./ui.js";
|
|
15
|
+
export const BENCH_HELP = `usage: amness bench [flags]
|
|
16
|
+
Run the same tasks through several coding-agent command-line tools (Amness, Claude Code, Codex, or any command) and grade every result with the same hidden check.
|
|
17
|
+
|
|
18
|
+
--init [file] write a starter config (default bench.json) with Amness, Amness with a finish gate, Claude Code and Codex, then edit it
|
|
19
|
+
--config <file> the contestants and settings (see --init). Without it, --contestants picks built-in presets
|
|
20
|
+
--contestants <a,b> presets: ${Object.keys(PRESETS).join(", ")} (default: amness,claude,codex)
|
|
21
|
+
--model <name> one model name for every contestant that takes one; or --models amness=gpt-6-luna,claude=claude-sonnet-5-5,codex=gpt-6-luna
|
|
22
|
+
--builtin <name> code (default, 13 easy tasks), hard (6 harder tasks) or adversarial
|
|
23
|
+
--cases <dir|file> your own tasks: a folder of *.json, or a .ts/.js module exporting a list of cases
|
|
24
|
+
--suite <file.jsonl> a SWE-bench-format file; needs --test-cmd (see amness eval --help) --limit <n> --install <cmd>
|
|
25
|
+
--only <a,b> only these tasks
|
|
26
|
+
--trials <n> attempts per task and contestant (default 3) --jobs <n> runs at once (default 1; the tools use your API limits)
|
|
27
|
+
--timeout <seconds> per run (default 900)
|
|
28
|
+
--check see which of the tools are installed (no model call)
|
|
29
|
+
--probe one tiny real task per contestant, to confirm the tool starts, is graded, and its tokens and cost are read correctly (costs a few cents)
|
|
30
|
+
--dry-run show exactly what would run, change nothing
|
|
31
|
+
--out <file> where to save the raw results (re-read with: amness eval --report <file>)
|
|
32
|
+
--yes skip the cost confirmation
|
|
33
|
+
Safety: each run happens in a throw-away temp folder, but the tools run with their file and shell tools enabled and are NOT sandboxed from the rest of your machine. Run benchmarks in a VM or container if the tasks are not yours.`;
|
|
34
|
+
async function pickCases(val, has) {
|
|
35
|
+
let cases;
|
|
36
|
+
const arg = val("--cases"), suite = val("--suite");
|
|
37
|
+
if (suite) {
|
|
38
|
+
if (!val("--test-cmd"))
|
|
39
|
+
throw new Error('a suite needs --test-cmd, for example --test-cmd "python -m pytest -x -q {tests}"');
|
|
40
|
+
cases = suiteToCases(loadSuite(resolve(suite)).slice(0, Number(val("--limit") ?? 1e9)), { testCmd: val("--test-cmd"), installCmd: val("--install") });
|
|
41
|
+
}
|
|
42
|
+
else if (arg && /\.(m?[jt]s)$/.test(arg)) {
|
|
43
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(arg)).href + `?v=${Date.now()}`));
|
|
44
|
+
cases = mod.default ?? mod.cases;
|
|
45
|
+
if (!Array.isArray(cases))
|
|
46
|
+
throw new Error(`${arg} must export a list of cases`);
|
|
47
|
+
}
|
|
48
|
+
else if (arg)
|
|
49
|
+
cases = loadCases(resolve(arg));
|
|
50
|
+
else {
|
|
51
|
+
const b = val("--builtin") ?? "code";
|
|
52
|
+
if (!["code", "hard", "adversarial"].includes(b))
|
|
53
|
+
throw new Error(`unknown built-in suite "${b}"`);
|
|
54
|
+
cases = b === "hard" ? hardCases : b === "adversarial" ? adversarialCases : builtinCases;
|
|
55
|
+
}
|
|
56
|
+
if (val("--only")) {
|
|
57
|
+
const want = val("--only").split(",");
|
|
58
|
+
cases = cases.filter((c) => want.includes(c.name));
|
|
59
|
+
if (!cases.length)
|
|
60
|
+
throw new Error(`no task matches --only ${val("--only")}`);
|
|
61
|
+
}
|
|
62
|
+
void has;
|
|
63
|
+
return cases;
|
|
64
|
+
}
|
|
65
|
+
export async function runBench(argv, deps) {
|
|
66
|
+
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; }, has = (f) => argv.includes(f);
|
|
67
|
+
if (has("--help") || has("-h")) {
|
|
68
|
+
console.log(BENCH_HELP);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (has("--init")) {
|
|
73
|
+
const i = argv.indexOf("--init"), file = resolve(argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : "bench.json");
|
|
74
|
+
if (existsSync(file)) {
|
|
75
|
+
console.error(red("✗ ") + `${file} already exists`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
writeFileSync(file, JSON.stringify(configFileTemplate(deps.amnessCli), null, 2) + "\n");
|
|
79
|
+
console.log(green("wrote ") + file + "\n" + dim("Edit it: set a model for each contestant, remove the ones you do not have, change trials/jobs/timeout. Then: amness bench --config " + file + " --dry-run"));
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
let cfg;
|
|
83
|
+
if (val("--config"))
|
|
84
|
+
cfg = JSON.parse(readFileSync(resolve(val("--config")), "utf8"));
|
|
85
|
+
else {
|
|
86
|
+
const models = Object.fromEntries((val("--models") ?? "").split(",").filter(Boolean).map((p) => p.split("=")));
|
|
87
|
+
const names = (val("--contestants") ?? "amness,claude,codex").split(",").map((s) => s.trim()).filter(Boolean);
|
|
88
|
+
cfg = { contestants: names.map((n) => { if (!PRESETS[n])
|
|
89
|
+
throw new Error(`unknown preset "${n}". Presets: ${Object.keys(PRESETS).join(", ")}. For anything else use --config.`); return PRESETS[n]({ model: models[n] ?? val("--model"), amnessCli: deps.amnessCli }); }) };
|
|
90
|
+
}
|
|
91
|
+
if (!cfg.contestants?.length)
|
|
92
|
+
throw new Error("no contestants");
|
|
93
|
+
for (const k of cfg.contestants) {
|
|
94
|
+
if (!k.name || !k.command)
|
|
95
|
+
throw new Error("every contestant needs a name and a command");
|
|
96
|
+
if (k.args === undefined)
|
|
97
|
+
k.args = [];
|
|
98
|
+
}
|
|
99
|
+
if (new Set(cfg.contestants.map((k) => k.name)).size !== cfg.contestants.length)
|
|
100
|
+
throw new Error("contestant names must be different");
|
|
101
|
+
if (val("--trials"))
|
|
102
|
+
cfg.trials = Number(val("--trials"));
|
|
103
|
+
if (val("--jobs"))
|
|
104
|
+
cfg.jobs = Number(val("--jobs"));
|
|
105
|
+
if (val("--timeout"))
|
|
106
|
+
cfg.timeoutSec = Number(val("--timeout"));
|
|
107
|
+
cfg.trials ??= 3;
|
|
108
|
+
cfg.jobs ??= 1;
|
|
109
|
+
cfg.logDir ??= join(".amness", "bench", "logs");
|
|
110
|
+
if (has("--check")) {
|
|
111
|
+
let bad = 0;
|
|
112
|
+
for (const k of cfg.contestants) {
|
|
113
|
+
const ok = installed(k.command);
|
|
114
|
+
if (!ok)
|
|
115
|
+
bad++;
|
|
116
|
+
console.log(`${ok ? green("found ") : red("missing ")} ${k.name.padEnd(14)} ${k.command}${ok ? "" : dim(" (not on PATH)")}`);
|
|
117
|
+
}
|
|
118
|
+
return bad ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
const cases = has("--probe") ? [{ name: "probe", files: {}, task: "Create a file named hello.txt that contains exactly: hi", verify: "node -e \"process.exit(require('fs').readFileSync('hello.txt','utf8').trim()==='hi'?0:1)\"", check: async (d) => { try {
|
|
121
|
+
return readFileSync(join(d, "hello.txt"), "utf8").trim() === "hi" || "hello.txt is wrong";
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "no hello.txt";
|
|
125
|
+
} } }] : await pickCases(val, has);
|
|
126
|
+
if (has("--probe")) {
|
|
127
|
+
cfg.trials = 1;
|
|
128
|
+
cfg.jobs = 1;
|
|
129
|
+
cfg.timeoutSec = Math.min(cfg.timeoutSec ?? 300, 300);
|
|
130
|
+
}
|
|
131
|
+
for (const l of describePlan(cases, cfg))
|
|
132
|
+
console.log(l);
|
|
133
|
+
const missing = cfg.contestants.filter((k) => !installed(k.command));
|
|
134
|
+
for (const k of missing)
|
|
135
|
+
console.log(yellow("! ") + `${k.name}: "${k.command}" is not on PATH`);
|
|
136
|
+
if (has("--dry-run"))
|
|
137
|
+
return 0;
|
|
138
|
+
if (missing.length === cfg.contestants.length) {
|
|
139
|
+
console.error(red("✗ ") + "none of the contestants can start");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const runs = cases.length * cfg.contestants.length * cfg.trials;
|
|
143
|
+
if (!has("--yes") && !(await deps.confirm(`This starts up to ${runs} agent run(s) that spend real money on your accounts (the tools bill you themselves). Continue?`)))
|
|
144
|
+
return 1;
|
|
145
|
+
mkdirSync(join(".amness", "bench"), { recursive: true });
|
|
146
|
+
const { rows, analysis } = await bench(cases, cfg, { onRow: (r) => console.error(` ${r.pass ? green("PASS") : red("FAIL")} ${r.case.padEnd(24)} ${r.variant.padEnd(14)} ${dim(`${(r.ms / 1000).toFixed(0)}s${r.tokens ? ` · ${r.tokens} tok` : ""}${r.metrics?.["cost-usd"] !== undefined ? ` · $${r.metrics["cost-usd"].toFixed(3)}` : ""}${r.note ? " · " + r.note : ""}`)}`) });
|
|
147
|
+
const out = resolve(val("--out") ?? join(".amness", "bench", new Date().toISOString().replace(/[:.]/g, "-") + ".json"));
|
|
148
|
+
mkdirSync(join(out, ".."), { recursive: true });
|
|
149
|
+
writeFileSync(out, JSON.stringify({ at: new Date().toISOString(), baseline: analysis.baseline, k: Math.min(3, cfg.trials), contestants: cfg.contestants, trials: cfg.trials, rows }, null, 1));
|
|
150
|
+
console.log("\n" + formatReport(analyze(rows, { k: Math.min(3, cfg.trials) })));
|
|
151
|
+
console.log("\n" + bold("what each contestant was") + "\n" + cfg.contestants.map((k) => ` ${k.name}: ${k.model ? "model " + k.model : "default model"}${k.note ? "; " + k.note : ""}`).join("\n"));
|
|
152
|
+
console.log("\n" + dim(`raw results: ${out} (amness eval --report ${out}). Raw output of every run: ${cfg.logDir}`));
|
|
153
|
+
console.log(dim("Read this fairly: the tools differ in prompts, default models and permissions, and tokens or cost are shown only when the tool reports them."));
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.error(red("✗ ") + e.message);
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare const PROVIDERS: {
|
|
2
|
+
readonly anthropic: {
|
|
3
|
+
readonly label: "Anthropic (Claude)";
|
|
4
|
+
readonly baseUrl: "https://api.anthropic.com";
|
|
5
|
+
readonly model: "claude-opus-5-5";
|
|
6
|
+
readonly needsKey: true;
|
|
7
|
+
};
|
|
8
|
+
readonly openai: {
|
|
9
|
+
readonly label: "OpenAI";
|
|
10
|
+
readonly baseUrl: "https://api.openai.com/v1";
|
|
11
|
+
readonly model: "gpt-6.1-sol";
|
|
12
|
+
readonly needsKey: true;
|
|
13
|
+
};
|
|
14
|
+
readonly openrouter: {
|
|
15
|
+
readonly label: "OpenRouter";
|
|
16
|
+
readonly baseUrl: "https://openrouter.ai/api/v1";
|
|
17
|
+
readonly model: "openai/gpt-4.1";
|
|
18
|
+
readonly needsKey: true;
|
|
19
|
+
};
|
|
20
|
+
readonly ollama: {
|
|
21
|
+
readonly label: "Ollama (local)";
|
|
22
|
+
readonly baseUrl: "http://localhost:11434/v1";
|
|
23
|
+
readonly model: "qwen2.5-coder:7b";
|
|
24
|
+
readonly needsKey: false;
|
|
25
|
+
};
|
|
26
|
+
readonly custom: {
|
|
27
|
+
readonly label: "Custom OpenAI-compatible";
|
|
28
|
+
readonly baseUrl: "http://localhost:8000/v1";
|
|
29
|
+
readonly model: "my-model";
|
|
30
|
+
readonly needsKey: false;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export type ProviderId = keyof typeof PROVIDERS;
|
|
34
|
+
export type Config = {
|
|
35
|
+
provider: ProviderId;
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
model: string;
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
tavilyKey?: string;
|
|
40
|
+
maxTokens: number;
|
|
41
|
+
effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** OpenAI wire API: auto (default) picks Responses for reasoning models and self-corrects */
|
|
42
|
+
api?: "auto" | "responses" | "chat"; /** full-screen colour theme (see /theme) */
|
|
43
|
+
theme?: string; /** which UI to start in */
|
|
44
|
+
ui?: "full" | "inline"; /** while idle, re-send the conversation now and then so the provider keeps its prompt cache (Claude: the cache expires after 5 minutes) */
|
|
45
|
+
keepWarm?: boolean; /** semantic memory retrieval via an OpenAI-compatible embeddings endpoint (sends note text to that provider) */
|
|
46
|
+
embeddings?: boolean; /** which shell runs the agent's commands: auto (default), bash, zsh, sh, pwsh, powershell or cmd (see /shell) */
|
|
47
|
+
shell?: string; /** a system prompt id from .amness/prompts (see /prompt) */
|
|
48
|
+
prompt?: string; /** a loop strategy: plan-first or explore-then-act (see /strategy) */
|
|
49
|
+
strategy?: string; /** most model requests in flight at once, shared by the lead agent and its subagents (default 4); halves automatically on a 429 and ramps back up */
|
|
50
|
+
maxConcurrent?: number; /** OpenAI Responses only: server-side compaction once the prompt passes this many tokens */
|
|
51
|
+
compactAt?: number; /** Claude only: advisory token budget per turn (task_budget, beta; at least 20000) */
|
|
52
|
+
taskBudget?: number; /** a second model reviews risky calls before you are asked (extra model call per risky action) */
|
|
53
|
+
approvalReviewer?: boolean; /** errors from an installed language server are appended to edit results */
|
|
54
|
+
lsp?: boolean; /** an independent reviewer checks finished work against acceptance criteria (extra model calls) */
|
|
55
|
+
review?: boolean; /** OS sandbox for shell commands */
|
|
56
|
+
sandbox?: "off" | "workspace" | "strict"; /** model ids (same provider) for other roles; unset roles use `model` */
|
|
57
|
+
models?: {
|
|
58
|
+
explore?: string;
|
|
59
|
+
compact?: string;
|
|
60
|
+
review?: string;
|
|
61
|
+
cheap?: string;
|
|
62
|
+
}; /** route exploration steps to models.cheap when that pays off (cache-aware) */
|
|
63
|
+
router?: boolean; /** model id to answer from when the main one is overloaded or unreachable */
|
|
64
|
+
fallback?: string;
|
|
65
|
+
};
|
|
66
|
+
export declare const home: () => string;
|
|
67
|
+
export declare const defaults: () => Config;
|
|
68
|
+
export declare function loadConfig(): Config;
|
|
69
|
+
export declare function saveConfig(c: Config): void;
|
|
70
|
+
export declare const ready: (c: Config) => boolean;
|
|
71
|
+
export declare const mask: (k?: string) => string;
|
|
72
|
+
export declare const CLAUDE_MODELS: {
|
|
73
|
+
id: string;
|
|
74
|
+
hint: string;
|
|
75
|
+
}[];
|
|
76
|
+
export declare const OPENAI_MODELS: {
|
|
77
|
+
id: string;
|
|
78
|
+
hint: string;
|
|
79
|
+
}[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const PROVIDERS = {
|
|
5
|
+
anthropic: { label: "Anthropic (Claude)", baseUrl: "https://api.anthropic.com", model: "claude-opus-5-5", needsKey: true },
|
|
6
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", model: "gpt-6.1-sol", needsKey: true },
|
|
7
|
+
openrouter: { label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", model: "openai/gpt-4.1", needsKey: true },
|
|
8
|
+
ollama: { label: "Ollama (local)", baseUrl: "http://localhost:11434/v1", model: "qwen2.5-coder:7b", needsKey: false },
|
|
9
|
+
custom: { label: "Custom OpenAI-compatible", baseUrl: "http://localhost:8000/v1", model: "my-model", needsKey: false },
|
|
10
|
+
};
|
|
11
|
+
export const home = () => process.env.AMNESS_HOME ?? join(homedir(), ".amness");
|
|
12
|
+
const file = () => join(home(), "config.json");
|
|
13
|
+
export const defaults = () => ({ provider: "openai", baseUrl: PROVIDERS.openai.baseUrl, model: PROVIDERS.openai.model, maxTokens: 300_000 });
|
|
14
|
+
export function loadConfig() {
|
|
15
|
+
let c = defaults();
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(file()))
|
|
18
|
+
c = { ...c, ...JSON.parse(readFileSync(file(), "utf8")) };
|
|
19
|
+
}
|
|
20
|
+
catch { /* corrupt config: start fresh */ }
|
|
21
|
+
// environment wins, so CI and one-off runs work without touching the saved file
|
|
22
|
+
const claude = c.provider === "anthropic";
|
|
23
|
+
const key = claude ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
|
|
24
|
+
if (key)
|
|
25
|
+
c.apiKey = key;
|
|
26
|
+
if (process.env.TAVILY_API_KEY)
|
|
27
|
+
c.tavilyKey = process.env.TAVILY_API_KEY;
|
|
28
|
+
if (process.env.OPENAI_BASE_URL && !claude)
|
|
29
|
+
c.baseUrl = process.env.OPENAI_BASE_URL;
|
|
30
|
+
if (process.env.AMNESS_MODEL)
|
|
31
|
+
c.model = process.env.AMNESS_MODEL;
|
|
32
|
+
return c;
|
|
33
|
+
}
|
|
34
|
+
export function saveConfig(c) {
|
|
35
|
+
mkdirSync(home(), { recursive: true });
|
|
36
|
+
writeFileSync(file(), JSON.stringify(c, null, 2));
|
|
37
|
+
chmodSync(file(), 0o600); // may hold API keys
|
|
38
|
+
}
|
|
39
|
+
export const ready = (c) => !PROVIDERS[c.provider].needsKey || !!c.apiKey;
|
|
40
|
+
export const mask = (k) => (k ? k.slice(0, 3) + "…" + k.slice(-4) : "not set");
|
|
41
|
+
export const CLAUDE_MODELS = [
|
|
42
|
+
{ id: "claude-opus-5-5", hint: "most capable everyday model · $4/$20 per M tokens (default)" },
|
|
43
|
+
{ id: "claude-sonnet-5-5", hint: "fast and cheaper · $2/$10" },
|
|
44
|
+
{ id: "claude-haiku-5-5", hint: "cheapest · $0.10/$0.50 · no refusal fallback" },
|
|
45
|
+
{ id: "claude-fable-5-1", hint: "hardest tasks · $10/$50" },
|
|
46
|
+
];
|
|
47
|
+
export const OPENAI_MODELS = [
|
|
48
|
+
{ id: "gpt-6.1-sol", hint: "near-flagship for complex work · $2/$10 per M tokens (default)" },
|
|
49
|
+
{ id: "gpt-6-astra", hint: "flagship, hardest tasks · $10/$50" },
|
|
50
|
+
{ id: "gpt-6.1-terra", hint: "fast + efficient, computer use · $1/$5" },
|
|
51
|
+
{ id: "gpt-6-luna", hint: "cheap, high-volume · $0.10/$0.50" },
|
|
52
|
+
{ id: "gpt-5.6-luna", hint: "previous generation" },
|
|
53
|
+
{ id: "gpt-4.1", hint: "non-reasoning · Chat Completions" },
|
|
54
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Approve, type Event, type Model } from "../src/index.ts";
|
|
2
|
+
export declare const DEMO_TASK = "sumTo(n) should return 1+2+...+n but the tests fail. Fix it.";
|
|
3
|
+
export declare function runDemo(o: {
|
|
4
|
+
approve: Approve;
|
|
5
|
+
onEvent: (e: Event) => void;
|
|
6
|
+
delayMs?: number;
|
|
7
|
+
model?: (m: Model) => Model;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
dir: string;
|
|
10
|
+
result: import("../src/harness.ts").RunResult;
|
|
11
|
+
events: Event[];
|
|
12
|
+
}>;
|
|
13
|
+
export declare const explain: Record<string, string>;
|
package/dist/cli/demo.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Guided demo: a scripted "model" that makes realistic mistakes so every harness guard fires, with no API key.
|
|
2
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createCodeAgent } from "../src/index.js";
|
|
6
|
+
let id = 0;
|
|
7
|
+
const call = (name, args, note) => ({
|
|
8
|
+
role: "assistant", content: note ?? null, usage: { total_tokens: 420 },
|
|
9
|
+
tool_calls: [{ id: `demo${++id}`, function: { name, arguments: JSON.stringify(args) } }],
|
|
10
|
+
});
|
|
11
|
+
export const DEMO_TASK = "sumTo(n) should return 1+2+...+n but the tests fail. Fix it.";
|
|
12
|
+
export async function runDemo(o) {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "amness-demo-"));
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
writeFileSync(join(dir, "sum.js"), "exports.sumTo = (n) => { let s = 0; for (let i = 1; i < n; i++) s += i; return s; };\n");
|
|
16
|
+
writeFileSync(join(dir, "test.js"), "const a = require('assert'); const { sumTo } = require('./sum'); a.equal(sumTo(3), 6); a.equal(sumTo(1), 1); console.log('tests passed');\n");
|
|
17
|
+
const script = [
|
|
18
|
+
{ role: "assistant", content: "I'll start by looking at the code and the failing test.", usage: { total_tokens: 300 } }, // intent, no action → nudge
|
|
19
|
+
call("read_file", {}), // missing arg → repair
|
|
20
|
+
call("edit_file", { path: "sum.js", old: "i < n", new: "i <= n" }), // edit before read → blocked
|
|
21
|
+
call("list_files", {}),
|
|
22
|
+
call("read_file", { path: "sum.js" }),
|
|
23
|
+
call("sh", { cmd: "rm -rf node_modules && git clean -fdx" }), // dangerous → approval prompt
|
|
24
|
+
call("finish", { summary: "Looks fine to me." }), // lies → verify gate runs tests → rejected
|
|
25
|
+
call("edit_file", { path: "sum.js", old: "i < n", new: "i <= n" }),
|
|
26
|
+
call("sh", { cmd: "node test.js" }),
|
|
27
|
+
call("finish", { summary: "Fixed off-by-one in sumTo (i < n → i <= n); test.js passes." }),
|
|
28
|
+
];
|
|
29
|
+
const scripted = async () => { await new Promise((r) => setTimeout(r, o.delayMs ?? 500)); return script.shift(); };
|
|
30
|
+
const events = [];
|
|
31
|
+
const agent = createCodeAgent({
|
|
32
|
+
model: o.model ? o.model(scripted) : scripted, cwd: dir, verify: "node test.js", approve: o.approve,
|
|
33
|
+
onEvent: (e) => (events.push(e), o.onEvent(e)),
|
|
34
|
+
});
|
|
35
|
+
const result = await agent.run(DEMO_TASK);
|
|
36
|
+
return { dir, result, events };
|
|
37
|
+
}
|
|
38
|
+
export const explain = {
|
|
39
|
+
nudge: "model said what it would do but did nothing → pushed back to act",
|
|
40
|
+
repair: "malformed tool call → exact error returned, model self-corrected",
|
|
41
|
+
denied: "dangerous command → human approval required",
|
|
42
|
+
rejected: "model claimed success → real test run disagreed, finish blocked",
|
|
43
|
+
tool: "tool calls executed",
|
|
44
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Best-guess command that proves the project works (used as the finish gate). */
|
|
4
|
+
export function detectVerify(dir) {
|
|
5
|
+
try {
|
|
6
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
7
|
+
const t = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).scripts?.test;
|
|
8
|
+
if (t && !/no test specified/.test(t))
|
|
9
|
+
return "npm test";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch { /* ignore */ }
|
|
13
|
+
if (existsSync(join(dir, "Cargo.toml")))
|
|
14
|
+
return "cargo test";
|
|
15
|
+
if (existsSync(join(dir, "go.mod")))
|
|
16
|
+
return "go test ./...";
|
|
17
|
+
if (["pytest.ini", "pyproject.toml", "setup.py", "tox.ini"].some((f) => existsSync(join(dir, f))))
|
|
18
|
+
return "pytest -q";
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Model } from "../src/index.ts";
|
|
2
|
+
export declare const EVAL_HELP: string;
|
|
3
|
+
export declare function runEval(argv: string[], deps: {
|
|
4
|
+
model: () => Model;
|
|
5
|
+
modelFor?: (id: string) => Model;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
confirm: (q: string) => Promise<boolean>;
|
|
8
|
+
}): Promise<number>;
|
|
9
|
+
export declare const TUNE_HELP = "usage: amness tune [flags]\n Try switching guards off (and changing a limit), one at a time, on part of your tasks; keep a change only if it also holds up on tasks the search never saw. Rolled back otherwise.\n Writes nothing unless something survives both checks. Calls your model: it asks first.\n\n --cases <dir> your own tasks (default: the 13 built-in tasks)\n --guards <a,b> which guards to try removing (default: nudge, loop, stuck, wrapup, condense, stagnation)\n --stagnation <n,n> also try these stagnationTurns values\n --trials <n> attempts per task (default 2)\n --max-steps <n> steps per session (default 30)\n --max-tokens <n> token cap per session\n --jobs <n> sessions at once (default 1)\n --seed <n> seed for the split and the intervals\n --profile-model <p> the model name or pattern to write the profile for (default: the configured model)\n --out <file> where to write the profile (default .amness/profile.json)\n --yes skip the cost confirmation";
|
|
10
|
+
export declare function runTune(argv: string[], deps: {
|
|
11
|
+
model: () => Model;
|
|
12
|
+
modelName: string;
|
|
13
|
+
maxTokens: number;
|
|
14
|
+
confirm: (q: string) => Promise<boolean>;
|
|
15
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// `amness eval`: compare harness variants on a task suite, with statistics you can act on.
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { BUILTIN_METRICS, gateCompare, importPlugins, tune, writeProfile, adversarialCases, hardCases, analyze, builtinCases, evaluate, recommend, leakAudit, loadSuite, suiteToCases, formatReport, harnessVariant, loadCases, selfTest, variantNames, VARIANT_HELP } from "../src/index.js";
|
|
13
|
+
import { bold, dim, green, red } from "./ui.js";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
async function casesFromModule(file) {
|
|
16
|
+
const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(file).href + `?v=${Date.now()}`)), list = mod.default ?? mod.cases;
|
|
17
|
+
if (!Array.isArray(list) || list.some((c) => !c || typeof c.name !== "string" || typeof c.task !== "string" || !c.files || !c.check))
|
|
18
|
+
throw new Error(`${file}: must export a list of cases ({ name, files, task, check })`);
|
|
19
|
+
return list;
|
|
20
|
+
}
|
|
21
|
+
export const EVAL_HELP = `usage: amness eval [flags]
|
|
22
|
+
Run the coding agent on a suite of tasks under several harness variants and compare them statistically.
|
|
23
|
+
|
|
24
|
+
--variants <spec> preset or comma list (default: ab). presets: ab · ablate · all
|
|
25
|
+
variants: ${VARIANT_HELP}
|
|
26
|
+
--trials <n> attempts per task and variant (default 3)
|
|
27
|
+
--suite <file.jsonl> a benchmark in SWE-bench format (instance_id, repo, base_commit, problem_statement, test_patch, FAIL_TO_PASS...); needs --test-cmd
|
|
28
|
+
--test-cmd <cmd> how to run the hidden tests; \`{tests}\` is replaced by the instance's FAIL_TO_PASS ids, e.g. "python -m pytest -x -q {tests}"
|
|
29
|
+
--install <cmd> run once per instance after checkout (dependencies)
|
|
30
|
+
--limit <n> only the first n instances of a suite
|
|
31
|
+
--audit contamination checks on a suite (fix lines in the problem statement or already in the base checkout); runs no model
|
|
32
|
+
--seed <n> seed for the bootstrap intervals (reproducible reports)
|
|
33
|
+
--builtin <name> which built-in suite: code (default, 13 easy tasks), hard (6 tasks with hidden edge cases, a misleading symptom and an exploration question) or adversarial (prompt-injection attempts; a run passes only if the injected command never ran)
|
|
34
|
+
--cases <dir|file> your own tasks: a folder of *.json ({ name, files, task, verify, check }), or a .ts/.js module exporting a list of cases (checks may be functions)
|
|
35
|
+
--metrics <a,b> extra numbers to record per run and compare (diff-lines, tool-calls, tokens-per-step, overclaim, or a metric a plugin defines)
|
|
36
|
+
--gate <file> compare with a saved results file (from --out) and exit 1 on a clear regression: pass rate down by more than --gate-tol points (default 5) or cost per solved task up by more than --gate-cost percent (default 25)
|
|
37
|
+
--plugin <file,...> load plugins from these files: their tasks join the suite and their variants can be named in --variants (also full+plugins: every loaded plugin active)
|
|
38
|
+
--only <a,b> run only these built-in tasks
|
|
39
|
+
--baseline <name> variant the others are compared with (default: the first)
|
|
40
|
+
--cheap-model <id> the cheaper model for the full+route variant (same provider)
|
|
41
|
+
--jobs <n> sessions run at once (default 1)
|
|
42
|
+
--max-tokens <n> token cap per session (default: your configured budget)
|
|
43
|
+
--max-steps <n> steps per session (default 30)
|
|
44
|
+
--profile-out <f> after an ablation, write the recommended profile (guards the data says to switch off) for --profile-model
|
|
45
|
+
--profile-model <p> the model name or pattern the profile applies to (default: the configured model)
|
|
46
|
+
--out <file> where to save raw results (default .amness/evals/<time>.json)
|
|
47
|
+
--report <file> re-analyse saved results; runs nothing
|
|
48
|
+
--selftest check that every task is solvable and its held-out check is sound; runs no model
|
|
49
|
+
--yes skip the cost confirmation`;
|
|
50
|
+
export async function runEval(argv, deps) {
|
|
51
|
+
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; }, has = (f) => argv.includes(f);
|
|
52
|
+
if (has("--help") || has("-h")) {
|
|
53
|
+
console.log(EVAL_HELP);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
if (has("--report")) {
|
|
58
|
+
const saved = JSON.parse(readFileSync(resolve(val("--report")), "utf8"));
|
|
59
|
+
console.log(formatReport(analyze(saved.rows, { baseline: val("--baseline") ?? saved.baseline, k: saved.k })));
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
const builtin = val("--builtin") ?? "code";
|
|
63
|
+
if (!["code", "adversarial", "hard"].includes(builtin))
|
|
64
|
+
throw new Error(`unknown built-in suite "${builtin}" (code, hard or adversarial)`);
|
|
65
|
+
const plugins = [];
|
|
66
|
+
for (const f of (val("--plugin") ?? "").split(",").filter(Boolean)) {
|
|
67
|
+
const r = await importPlugins(f);
|
|
68
|
+
for (const w of r.warnings)
|
|
69
|
+
console.error(red("! ") + w);
|
|
70
|
+
plugins.push(...r.plugins);
|
|
71
|
+
}
|
|
72
|
+
const casesArg = val("--cases"), isModule = !!casesArg && /\.(m?[jt]s)$/.test(casesArg);
|
|
73
|
+
let cases = isModule ? await casesFromModule(resolve(casesArg)) : casesArg ? loadCases(resolve(casesArg)) : builtin === "adversarial" ? adversarialCases : builtin === "hard" ? hardCases : builtinCases;
|
|
74
|
+
if (val("--suite")) {
|
|
75
|
+
const inst = loadSuite(resolve(val("--suite"))).slice(0, Number(val("--limit") ?? 1e9));
|
|
76
|
+
if (has("--audit")) {
|
|
77
|
+
const leaks = await leakAudit(inst);
|
|
78
|
+
for (const l of leaks)
|
|
79
|
+
console.log(`${red("leak")} ${l.id}: ${l.problem}`);
|
|
80
|
+
console.log(leaks.length ? `${leaks.length} instance(s) with problems` : green("no contamination found in " + inst.length + " instance(s)"));
|
|
81
|
+
return leaks.length ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
if (!val("--test-cmd"))
|
|
84
|
+
throw new Error('a suite needs --test-cmd, e.g. --test-cmd "python -m pytest -x -q {tests}"');
|
|
85
|
+
cases = suiteToCases(inst, { testCmd: val("--test-cmd"), installCmd: val("--install") });
|
|
86
|
+
}
|
|
87
|
+
for (const p of plugins)
|
|
88
|
+
cases.push(...(p.cases ?? []));
|
|
89
|
+
const metricFns = {};
|
|
90
|
+
for (const p of plugins)
|
|
91
|
+
Object.assign(metricFns, p.metrics ?? {});
|
|
92
|
+
for (const m of (val("--metrics") ?? "").split(",").filter(Boolean)) {
|
|
93
|
+
const fn = BUILTIN_METRICS[m] ?? metricFns[m];
|
|
94
|
+
if (!fn)
|
|
95
|
+
throw new Error(`unknown metric "${m}". Built in: ${Object.keys(BUILTIN_METRICS).join(", ")}${Object.keys(metricFns).length ? "; from plugins: " + Object.keys(metricFns).join(", ") : ""}`);
|
|
96
|
+
metricFns[m] = fn;
|
|
97
|
+
}
|
|
98
|
+
const wanted = (val("--metrics") ?? "").split(",").filter(Boolean), useMetrics = Object.fromEntries(Object.entries(metricFns).filter(([k]) => wanted.includes(k) || plugins.some((p) => p.metrics && k in p.metrics)));
|
|
99
|
+
if (val("--only")) {
|
|
100
|
+
const want = val("--only").split(",");
|
|
101
|
+
cases = cases.filter((c) => want.includes(c.name));
|
|
102
|
+
if (!cases.length)
|
|
103
|
+
throw new Error(`no task matches --only ${val("--only")}`);
|
|
104
|
+
}
|
|
105
|
+
if (has("--selftest")) {
|
|
106
|
+
const res = await selfTest(cases);
|
|
107
|
+
for (const r of res)
|
|
108
|
+
console.log(`${r.ok ? green("ok ") : red("bad")} ${r.case}${r.why ? dim(" " + r.why) : ""}`);
|
|
109
|
+
return res.every((r) => r.ok) ? 0 : 1;
|
|
110
|
+
}
|
|
111
|
+
const names = variantNames(val("--variants") ?? "ab"), trials = Math.max(1, Number(val("--trials") ?? 3)), jobs = Math.max(1, Number(val("--jobs") ?? 1));
|
|
112
|
+
const maxTokens = Number(val("--max-tokens") ?? deps.maxTokens), maxSteps = Number(val("--max-steps") ?? 30);
|
|
113
|
+
const model = deps.model(), cheap = val("--cheap-model") && deps.modelFor ? deps.modelFor(val("--cheap-model")) : undefined, variants = names.map((n) => harnessVariant(model, n, { maxTokens, maxSteps, cheap, plugins, modelFor: deps.modelFor }));
|
|
114
|
+
const runs = cases.length * variants.length * trials;
|
|
115
|
+
console.log(`${bold("plan")} ${cases.length} tasks × ${variants.length} variants (${names.join(", ")}) × ${trials} trials = ${bold(String(runs))} agent sessions, up to ${maxTokens.toLocaleString()} tokens each`);
|
|
116
|
+
if (!has("--yes") && !(await deps.confirm(`This calls your model up to ${(runs * maxTokens).toLocaleString()} tokens in the worst case. Continue?`)))
|
|
117
|
+
return 1;
|
|
118
|
+
const { rows, analysis } = await evaluate(cases, variants, {
|
|
119
|
+
metrics: Object.keys(useMetrics).length ? useMetrics : undefined, trials, jobs, baseline: val("--baseline"), k: Math.min(3, trials), seed: val("--seed") ? Number(val("--seed")) : undefined,
|
|
120
|
+
onRow: (r) => console.error(` ${r.pass ? green("PASS") : red("FAIL")} ${r.case.padEnd(22)} ${r.variant.padEnd(18)} ${dim(`${r.steps} steps · ${r.tokens} tok · ${(r.ms / 1000).toFixed(1)}s${r.note ? " · " + r.note : ""}`)}`),
|
|
121
|
+
});
|
|
122
|
+
const out = resolve(val("--out") ?? join(".amness", "evals", new Date().toISOString().replace(/[:.]/g, "-") + ".json"));
|
|
123
|
+
mkdirSync(join(out, ".."), { recursive: true });
|
|
124
|
+
writeFileSync(out, JSON.stringify({ at: new Date().toISOString(), baseline: analysis.baseline, k: Math.min(3, trials), variants: names, trials, rows }, null, 1));
|
|
125
|
+
if (val("--profile-out")) {
|
|
126
|
+
const rec = recommend(analysis, names.includes("full") ? "full" : analysis.baseline), pm = val("--profile-model") ?? "*";
|
|
127
|
+
console.log("\nguard verdicts:");
|
|
128
|
+
for (const v of rec.verdicts)
|
|
129
|
+
console.log(` ${v.action === "remove" ? "remove" : v.action === "keep" ? "KEEP " : "keep? "} ${v.guard.padEnd(11)} ${v.why}`);
|
|
130
|
+
console.log(` ${rec.strength}`);
|
|
131
|
+
writeFileSync(resolve(val("--profile-out")), JSON.stringify({ models: { [pm]: rec.profile } }, null, 2) + "\n");
|
|
132
|
+
console.log(`profile written to ${val("--profile-out")}: copy it to .amness/profile.json`);
|
|
133
|
+
}
|
|
134
|
+
console.log("\n" + formatReport(analysis) + "\n\n" + dim(`raw results: ${out} (re-analyse with: amness eval --report ${out})`));
|
|
135
|
+
if (val("--gate")) {
|
|
136
|
+
const base = JSON.parse(readFileSync(resolve(val("--gate")), "utf8")), verdicts = gateCompare(rows, base.rows, { tol: val("--gate-tol") ? Number(val("--gate-tol")) / 100 : undefined, costPct: val("--gate-cost") ? Number(val("--gate-cost")) : undefined });
|
|
137
|
+
console.log("\n" + bold("regression gate") + dim(` against ${val("--gate")}`));
|
|
138
|
+
for (const g of verdicts) {
|
|
139
|
+
console.log(` ${g.ok ? green("ok ") : red("FAIL")} ${g.variant}`);
|
|
140
|
+
for (const l of g.lines)
|
|
141
|
+
console.log(" " + l);
|
|
142
|
+
}
|
|
143
|
+
if (verdicts.some((g) => !g.ok))
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error(red("✗ " + e.message));
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export const TUNE_HELP = `usage: amness tune [flags]
|
|
154
|
+
Try switching guards off (and changing a limit), one at a time, on part of your tasks; keep a change only if it also holds up on tasks the search never saw. Rolled back otherwise.
|
|
155
|
+
Writes nothing unless something survives both checks. Calls your model: it asks first.
|
|
156
|
+
|
|
157
|
+
--cases <dir> your own tasks (default: the 13 built-in tasks)
|
|
158
|
+
--guards <a,b> which guards to try removing (default: nudge, loop, stuck, wrapup, condense, stagnation)
|
|
159
|
+
--stagnation <n,n> also try these stagnationTurns values
|
|
160
|
+
--trials <n> attempts per task (default 2)
|
|
161
|
+
--max-steps <n> steps per session (default 30)
|
|
162
|
+
--max-tokens <n> token cap per session
|
|
163
|
+
--jobs <n> sessions at once (default 1)
|
|
164
|
+
--seed <n> seed for the split and the intervals
|
|
165
|
+
--profile-model <p> the model name or pattern to write the profile for (default: the configured model)
|
|
166
|
+
--out <file> where to write the profile (default .amness/profile.json)
|
|
167
|
+
--yes skip the cost confirmation`;
|
|
168
|
+
export async function runTune(argv, deps) {
|
|
169
|
+
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
|
|
170
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
171
|
+
console.log(TUNE_HELP);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const cases = val("--cases") ? loadCases(resolve(val("--cases"))) : builtinCases, model = deps.model(), maxTokens = Number(val("--max-tokens") ?? deps.maxTokens), maxSteps = Number(val("--max-steps") ?? 30);
|
|
176
|
+
const guards = val("--guards")?.split(",").map((s) => s.trim()), stag = val("--stagnation")?.split(",").map(Number), trials = Math.max(1, Number(val("--trials") ?? 2));
|
|
177
|
+
const n = (guards?.length ?? 6) + (stag?.length ?? 0), runs = Math.round(cases.length * 0.66) * 2 * trials * n + Math.max(1, Math.round(cases.length * 0.34)) * 2 * trials;
|
|
178
|
+
console.log(`${bold("plan")} up to ${runs} agent sessions (${cases.length} tasks, ${n} candidate changes, ${trials} trial(s)), up to ${maxTokens.toLocaleString()} tokens each`);
|
|
179
|
+
if (!argv.includes("--yes") && !(await deps.confirm(`This calls your model up to ${(runs * maxTokens).toLocaleString()} tokens in the worst case. Continue?`)))
|
|
180
|
+
return 1;
|
|
181
|
+
const r = await tune({ cases, trials, guards, stagnationTurns: stag, jobs: Number(val("--jobs") ?? 1), seed: val("--seed") ? Number(val("--seed")) : undefined,
|
|
182
|
+
makeVariant: (profile, name) => ({ ...harnessVariant(model, "full", { maxTokens, maxSteps, profile }), name }),
|
|
183
|
+
onStep: (s) => console.log(` ${s.accepted ? green("keep") : red("drop")} ${s.stage === "heldout" ? "[held-out] " : ""}${s.change} ${dim(s.why)}`) });
|
|
184
|
+
console.log(dim(`\n${r.strength}; held-out tasks: ${r.heldOut.join(", ") || "none"}`));
|
|
185
|
+
if (!r.accepted.length) {
|
|
186
|
+
console.log("nothing survived both checks; no profile written.");
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
const out = resolve(val("--out") ?? join(".amness", "profile.json"));
|
|
190
|
+
writeProfile(out, val("--profile-model") ?? deps.modelName, r.profile);
|
|
191
|
+
console.log(`kept: ${r.accepted.join(", ")}\nprofile written to ${out}`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
console.error(red("✗ " + e.message));
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
}
|