amness 0.0.0-stage → 0.1.2
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 +237 -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 +694 -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 +111 -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/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -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 +650 -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 +292 -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 +205 -0
- package/docs/headless.md +188 -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 +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -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,111 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { deleteSecret, getSecret, noStore, setSecret } from "./keystore.js";
|
|
5
|
+
export const PROVIDERS = {
|
|
6
|
+
anthropic: { label: "Anthropic (Claude)", baseUrl: "https://api.anthropic.com", model: "claude-opus-5-5", needsKey: true },
|
|
7
|
+
openai: { label: "OpenAI", baseUrl: "https://api.openai.com/v1", model: "gpt-6.1-sol", needsKey: true },
|
|
8
|
+
openrouter: { label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", model: "openai/gpt-4.1", needsKey: true },
|
|
9
|
+
ollama: { label: "Ollama (local)", baseUrl: "http://localhost:11434/v1", model: "qwen2.5-coder:7b", needsKey: false },
|
|
10
|
+
custom: { label: "Custom OpenAI-compatible", baseUrl: "http://localhost:8000/v1", model: "my-model", needsKey: false },
|
|
11
|
+
};
|
|
12
|
+
export const home = () => process.env.AMNESS_HOME ?? join(homedir(), ".amness");
|
|
13
|
+
const file = () => join(home(), "config.json");
|
|
14
|
+
export const defaults = () => ({ provider: "openai", baseUrl: PROVIDERS.openai.baseUrl, model: PROVIDERS.openai.model, maxTokens: 300_000 });
|
|
15
|
+
const SECRETS = ["apiKey", "tavilyKey"];
|
|
16
|
+
/** An environment variable, or the file NAME_FILE points to (Docker and Kubernetes secrets are mounted as files, for example /run/secrets/openai). */
|
|
17
|
+
const envSecret = (name) => {
|
|
18
|
+
const v = process.env[name];
|
|
19
|
+
if (v)
|
|
20
|
+
return v;
|
|
21
|
+
const f = process.env[`${name}_FILE`];
|
|
22
|
+
if (!f)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(f, "utf8").trim() || undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
let fromEnv = {};
|
|
32
|
+
export function loadConfig() {
|
|
33
|
+
let c = defaults(), onDisk = {};
|
|
34
|
+
try {
|
|
35
|
+
if (existsSync(file()))
|
|
36
|
+
onDisk = JSON.parse(readFileSync(file(), "utf8"));
|
|
37
|
+
c = { ...c, ...onDisk };
|
|
38
|
+
}
|
|
39
|
+
catch { /* corrupt config: start fresh */ }
|
|
40
|
+
// keys live in the OS credential store when there is one; a key still in the file (an older version wrote it there) is moved over, and removed from the file only after it reads back
|
|
41
|
+
let migrated = false;
|
|
42
|
+
for (const k of SECRETS) {
|
|
43
|
+
const stored = getSecret(k);
|
|
44
|
+
if (stored) {
|
|
45
|
+
c[k] = stored;
|
|
46
|
+
if (onDisk[k]) {
|
|
47
|
+
delete onDisk[k];
|
|
48
|
+
migrated = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (onDisk[k] && setSecret(k, onDisk[k])) {
|
|
52
|
+
delete onDisk[k];
|
|
53
|
+
migrated = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (migrated)
|
|
57
|
+
writeConfigFile({ ...defaults(), ...onDisk, ...Object.fromEntries(SECRETS.map((k) => [k, undefined])) });
|
|
58
|
+
// environment wins, so CI and one-off runs work without touching the saved key. It is never written back (see saveConfig).
|
|
59
|
+
const claude = c.provider === "anthropic";
|
|
60
|
+
fromEnv = {};
|
|
61
|
+
const key = envSecret(claude ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY");
|
|
62
|
+
if (key)
|
|
63
|
+
c.apiKey = fromEnv.apiKey = key;
|
|
64
|
+
const tavily = envSecret("TAVILY_API_KEY");
|
|
65
|
+
if (tavily)
|
|
66
|
+
c.tavilyKey = fromEnv.tavilyKey = tavily;
|
|
67
|
+
if (process.env.OPENAI_BASE_URL && !claude)
|
|
68
|
+
c.baseUrl = process.env.OPENAI_BASE_URL;
|
|
69
|
+
if (process.env.AMNESS_MODEL)
|
|
70
|
+
c.model = process.env.AMNESS_MODEL;
|
|
71
|
+
return c;
|
|
72
|
+
}
|
|
73
|
+
function writeConfigFile(c) {
|
|
74
|
+
mkdirSync(home(), { recursive: true });
|
|
75
|
+
writeFileSync(file(), JSON.stringify(c, null, 2), { mode: 0o600 }); // owner-only from the first byte
|
|
76
|
+
chmodSync(file(), 0o600); // and for a file that already existed
|
|
77
|
+
}
|
|
78
|
+
export function saveConfig(c) {
|
|
79
|
+
const out = { ...c };
|
|
80
|
+
for (const k of SECRETS) {
|
|
81
|
+
const v = c[k];
|
|
82
|
+
if (v && v === fromEnv[k]) {
|
|
83
|
+
delete out[k];
|
|
84
|
+
continue;
|
|
85
|
+
} // this value came from an environment variable: do not store it
|
|
86
|
+
if (!v) {
|
|
87
|
+
deleteSecret(k);
|
|
88
|
+
delete out[k];
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (setSecret(k, v) || noStore())
|
|
92
|
+
delete out[k]; // stored in the OS credential store; otherwise it stays in the file (mode 600), except in a container, where it is kept in memory only
|
|
93
|
+
}
|
|
94
|
+
writeConfigFile(out);
|
|
95
|
+
}
|
|
96
|
+
export const ready = (c) => !PROVIDERS[c.provider].needsKey || !!c.apiKey;
|
|
97
|
+
export const mask = (k) => (k ? k.slice(0, 3) + "…" + k.slice(-4) : "not set");
|
|
98
|
+
export const CLAUDE_MODELS = [
|
|
99
|
+
{ id: "claude-opus-5-5", hint: "most capable everyday model · $4/$20 per M tokens (default)" },
|
|
100
|
+
{ id: "claude-sonnet-5-5", hint: "fast and cheaper · $2/$10" },
|
|
101
|
+
{ id: "claude-haiku-5-5", hint: "cheapest · $0.10/$0.50 · no refusal fallback" },
|
|
102
|
+
{ id: "claude-fable-5-1", hint: "hardest tasks · $10/$50" },
|
|
103
|
+
];
|
|
104
|
+
export const OPENAI_MODELS = [
|
|
105
|
+
{ id: "gpt-6.1-sol", hint: "near-flagship for complex work · $2/$10 per M tokens (default)" },
|
|
106
|
+
{ id: "gpt-6-astra", hint: "flagship, hardest tasks · $10/$50" },
|
|
107
|
+
{ id: "gpt-6.1-terra", hint: "fast + efficient, computer use · $1/$5" },
|
|
108
|
+
{ id: "gpt-6-luna", hint: "cheap, high-volume · $0.10/$0.50" },
|
|
109
|
+
{ id: "gpt-5.6-luna", hint: "previous generation" },
|
|
110
|
+
{ id: "gpt-4.1", hint: "non-reasoning · Chat Completions" },
|
|
111
|
+
];
|
|
@@ -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>;
|