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,67 @@
|
|
|
1
|
+
import { type Analysis, type Case, type Row } from "./evals.ts";
|
|
2
|
+
export type ParseMode = "claude-json" | "codex-jsonl" | "amness-json" | "none";
|
|
3
|
+
export type Contestant = {
|
|
4
|
+
name: string;
|
|
5
|
+
/** the program to run (resolved through PATH; on Windows .cmd and .ps1 shims work too) */ command: string;
|
|
6
|
+
/** arguments; the placeholders {task} {cwd} {model} {verify} are replaced */ args?: string[];
|
|
7
|
+
/** send the prompt on standard input instead of in an argument (recommended: no quoting problems) */ stdin?: boolean;
|
|
8
|
+
model?: string;
|
|
9
|
+
env?: Record<string, string>; /** how to read tokens and cost from the output */
|
|
10
|
+
parse?: ParseMode;
|
|
11
|
+
/** kill the run after this many seconds (default: the bench's timeout) */ timeoutSec?: number;
|
|
12
|
+
/** a note shown in the report, for example "uses my Claude login, loads my CLAUDE.md" */ note?: string;
|
|
13
|
+
};
|
|
14
|
+
export type BenchConfig = {
|
|
15
|
+
contestants: Contestant[];
|
|
16
|
+
/** the prompt given to every contestant; {task} is the task text, {verify} the check command the task offers (default below) */ promptTemplate?: string;
|
|
17
|
+
/** make a git repository in each task folder first (agents often expect one; default true) */ gitInit?: boolean;
|
|
18
|
+
timeoutSec?: number;
|
|
19
|
+
trials?: number;
|
|
20
|
+
jobs?: number; /** where to keep each run's raw output for audit */
|
|
21
|
+
logDir?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const DEFAULT_PROMPT = "{task}\n\nWhen you believe you are done, check your work by running: {verify}";
|
|
24
|
+
export declare const PRESETS: Record<string, (o: {
|
|
25
|
+
model?: string;
|
|
26
|
+
amnessCli?: string;
|
|
27
|
+
}) => Contestant>;
|
|
28
|
+
export type ParsedUsage = {
|
|
29
|
+
input?: number;
|
|
30
|
+
output?: number;
|
|
31
|
+
cached?: number;
|
|
32
|
+
costUsd?: number;
|
|
33
|
+
turns?: number;
|
|
34
|
+
text?: string;
|
|
35
|
+
isError?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function parseOutput(mode: ParseMode | undefined, stdout: string): ParsedUsage;
|
|
38
|
+
/** Quote an argument for cmd.exe / a POSIX shell when a shell is needed (Windows .cmd shims). */
|
|
39
|
+
export declare const shellQuote: (a: string, win?: boolean) => string;
|
|
40
|
+
export declare function commandLine(c: Contestant, vars: Record<string, string>): {
|
|
41
|
+
file: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
};
|
|
44
|
+
export type ContestantRun = {
|
|
45
|
+
code: number | null;
|
|
46
|
+
stdout: string;
|
|
47
|
+
stderr: string;
|
|
48
|
+
ms: number;
|
|
49
|
+
timedOut: boolean;
|
|
50
|
+
startError?: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function runContestant(c: Contestant, vars: Record<string, string>, prompt: string, cwd: string, timeoutSec: number): Promise<ContestantRun>;
|
|
53
|
+
export type BenchProgress = (r: Row & {
|
|
54
|
+
stderrTail?: string;
|
|
55
|
+
}) => void;
|
|
56
|
+
export declare function bench(cases: Case[], cfg: BenchConfig, o?: {
|
|
57
|
+
onRow?: BenchProgress;
|
|
58
|
+
baseline?: string;
|
|
59
|
+
seed?: number;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
rows: Row[];
|
|
62
|
+
analysis: Analysis;
|
|
63
|
+
}>;
|
|
64
|
+
/** What each contestant would run, with the prompt, without running anything. */
|
|
65
|
+
export declare function describePlan(cases: Case[], cfg: BenchConfig): string[];
|
|
66
|
+
export declare const installed: (command: string) => boolean;
|
|
67
|
+
export declare const configFileTemplate: (amnessCli?: string) => BenchConfig;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Bake-off: run the same tasks through different coding-agent CLIs (Amness, Claude Code, Codex, anything with a command line) and grade every result with the same hidden check.
|
|
2
|
+
// What it can say honestly: pass rate with a confidence range, cost, tokens and time per solved task. What it cannot do: make the contestants equal. Prompts, tools, default models and
|
|
3
|
+
// permissions differ, so the report names every setting that was used. Each contestant is described as data (a command template), so you can change anything, and add your own.
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { analyze } from "./evals.js";
|
|
9
|
+
import { runShell } from "./code.js";
|
|
10
|
+
export const DEFAULT_PROMPT = "{task}\n\nWhen you believe you are done, check your work by running: {verify}";
|
|
11
|
+
// ------------------------------------------------------------------ presets (verify the flags with your installed version: `claude --help`, `codex exec --help`)
|
|
12
|
+
export const PRESETS = {
|
|
13
|
+
amness: ({ model, amnessCli }) => ({ name: "amness", command: "node", args: [amnessCli ?? resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..", "cli", "amness.ts"), "run", "--json", "--yes", "--max-tokens", "400000", ...(model ? ["--model", model] : []), "-"], stdin: true, parse: "amness-json", model, note: "headless `amness run`; the default harness without a finish gate" }),
|
|
14
|
+
"amness-gate": ({ model, amnessCli }) => ({ ...PRESETS.amness({ model, amnessCli }), name: "amness-gate", args: [...PRESETS.amness({ model, amnessCli }).args.slice(0, -1), "--verify", "{verify}", "-"], note: "headless `amness run` with --verify: it may not finish until the check passes (the other contestants only get the check in the prompt)" }),
|
|
15
|
+
claude: ({ model }) => ({ name: "claude", command: "claude", args: ["-p", "--output-format", "json", "--permission-mode", "acceptEdits", "--allowedTools", "Bash,Read,Edit,Write", ...(model ? ["--model", model] : [])], stdin: true, parse: "claude-json", model, note: "Claude Code print mode with YOUR login, settings, CLAUDE.md and hooks (use claude-bare for a clean room)" }),
|
|
16
|
+
"claude-bare": ({ model }) => ({ name: "claude-bare", command: "claude", args: ["--bare", "-p", "--output-format", "json", "--permission-mode", "acceptEdits", "--allowedTools", "Bash,Read,Edit,Write", ...(model ? ["--model", model] : [])], stdin: true, parse: "claude-json", model, note: "Claude Code --bare: no user settings, hooks, CLAUDE.md or memory; needs ANTHROPIC_API_KEY (no subscription login)" }),
|
|
17
|
+
codex: ({ model }) => ({ name: "codex", command: "codex", args: ["exec", "--json", "--skip-git-repo-check", "--ephemeral", "--ignore-user-config", "--ignore-rules", "--sandbox", "workspace-write", ...(model ? ["--model", model] : []), "-"], stdin: true, parse: "codex-jsonl", model, note: "codex exec in a workspace-write sandbox, ignoring your user config and rules" }),
|
|
18
|
+
};
|
|
19
|
+
const jsonLines = (s) => s.split(/\r?\n/).flatMap((l) => { const t = l.trim(); if (!t.startsWith("{") && !t.startsWith("["))
|
|
20
|
+
return []; try {
|
|
21
|
+
return [JSON.parse(t)];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
} });
|
|
26
|
+
const num = (x) => (typeof x === "number" && Number.isFinite(x) ? x : undefined);
|
|
27
|
+
export function parseOutput(mode, stdout) {
|
|
28
|
+
if (!mode || mode === "none")
|
|
29
|
+
return {};
|
|
30
|
+
if (mode === "claude-json") {
|
|
31
|
+
// one JSON object (print mode), or a stream whose last line is the result message
|
|
32
|
+
let objs = jsonLines(stdout);
|
|
33
|
+
if (!objs.length) {
|
|
34
|
+
try {
|
|
35
|
+
objs = [JSON.parse(stdout)];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const r = [...objs.flat()].reverse().find((o) => o && (o.type === "result" || "total_cost_usd" in o)) ?? objs.at(-1) ?? {}, u = r.usage ?? {};
|
|
42
|
+
const cached = (num(u.cache_read_input_tokens) ?? 0), fresh = (num(u.input_tokens) ?? 0) + (num(u.cache_creation_input_tokens) ?? 0);
|
|
43
|
+
return { input: fresh + cached, cached, output: num(u.output_tokens), costUsd: num(r.total_cost_usd), turns: num(r.num_turns), text: typeof r.result === "string" ? r.result : undefined, isError: !!r.is_error };
|
|
44
|
+
}
|
|
45
|
+
if (mode === "amness-json") {
|
|
46
|
+
const r = [...jsonLines(stdout)].reverse().find((o) => o.kind === "result");
|
|
47
|
+
return r ? { input: num(r.tokens), output: undefined, turns: num(r.steps), text: typeof r.text === "string" ? r.text : undefined, isError: r.finished === false } : {};
|
|
48
|
+
}
|
|
49
|
+
// codex-jsonl: the token event is not documented, so this reads what is commonly there: a cumulative `total_token_usage` (last one wins), else the sum of per-turn `usage` objects.
|
|
50
|
+
let total, sum = { i: 0, o: 0, c: 0 }, any = false, text;
|
|
51
|
+
const walk = (x) => {
|
|
52
|
+
if (!x || typeof x !== "object")
|
|
53
|
+
return;
|
|
54
|
+
if (x.total_token_usage && typeof x.total_token_usage === "object")
|
|
55
|
+
total = x.total_token_usage;
|
|
56
|
+
else if (x.usage && typeof x.usage === "object" && (num(x.usage.input_tokens) !== undefined || num(x.usage.output_tokens) !== undefined)) {
|
|
57
|
+
any = true;
|
|
58
|
+
sum.i += num(x.usage.input_tokens) ?? 0;
|
|
59
|
+
sum.o += num(x.usage.output_tokens) ?? 0;
|
|
60
|
+
sum.c += num(x.usage.cached_input_tokens) ?? 0;
|
|
61
|
+
}
|
|
62
|
+
if ((x.type === "agent_message" || x.item?.type === "agent_message") && typeof (x.text ?? x.item?.text) === "string")
|
|
63
|
+
text = x.text ?? x.item.text;
|
|
64
|
+
for (const v of Object.values(x))
|
|
65
|
+
if (v && typeof v === "object")
|
|
66
|
+
walk(v);
|
|
67
|
+
};
|
|
68
|
+
for (const o of jsonLines(stdout))
|
|
69
|
+
walk(o);
|
|
70
|
+
if (total)
|
|
71
|
+
return { input: num(total.input_tokens), output: num(total.output_tokens), cached: num(total.cached_input_tokens), text };
|
|
72
|
+
return any ? { input: sum.i, output: sum.o, cached: sum.c, text } : { text };
|
|
73
|
+
}
|
|
74
|
+
// ------------------------------------------------------------------ running one contestant
|
|
75
|
+
/** Quote an argument for cmd.exe / a POSIX shell when a shell is needed (Windows .cmd shims). */
|
|
76
|
+
export const shellQuote = (a, win = process.platform === "win32") => (win ? (/^[\w@%+=:,./\\-]+$/.test(a) ? a : `"${a.replace(/"/g, '\\"')}"`) : a);
|
|
77
|
+
const fill = (s, v) => s.replace(/\{(task|cwd|model|verify)\}/g, (_, k) => v[k] ?? "");
|
|
78
|
+
export function commandLine(c, vars) { return { file: c.command, args: (c.args ?? []).map((a) => fill(a, vars)) }; }
|
|
79
|
+
function killTree(p) {
|
|
80
|
+
if (!p.pid)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
if (process.platform === "win32")
|
|
84
|
+
spawnSync("taskkill", ["/pid", String(p.pid), "/T", "/F"], { stdio: "ignore" });
|
|
85
|
+
else
|
|
86
|
+
process.kill(-p.pid, "SIGKILL");
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
try {
|
|
90
|
+
p.kill("SIGKILL");
|
|
91
|
+
}
|
|
92
|
+
catch { /* gone */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function runContestant(c, vars, prompt, cwd, timeoutSec) {
|
|
96
|
+
const { file, args } = commandLine(c, vars), win = process.platform === "win32", t0 = Date.now();
|
|
97
|
+
return new Promise((res) => {
|
|
98
|
+
let out = "", err = "", timedOut = false, done = false;
|
|
99
|
+
const p = spawn(file, win ? args.map((a) => shellQuote(a, true)) : args, { cwd, env: { ...process.env, ...c.env }, shell: win, detached: !win, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
100
|
+
const finish = (r) => { if (done)
|
|
101
|
+
return; done = true; clearTimeout(timer); res({ code: null, stdout: out, stderr: err, ms: Date.now() - t0, timedOut, ...r }); };
|
|
102
|
+
const timer = setTimeout(() => { timedOut = true; killTree(p); setTimeout(() => finish({ code: null }), 2000).unref(); }, timeoutSec * 1000);
|
|
103
|
+
p.stdout.on("data", (d) => { if (out.length < 20_000_000)
|
|
104
|
+
out += d; });
|
|
105
|
+
p.stderr.on("data", (d) => { if (err.length < 2_000_000)
|
|
106
|
+
err += d; });
|
|
107
|
+
p.on("error", (e) => finish({ startError: e.message }));
|
|
108
|
+
p.on("close", (code) => finish({ code }));
|
|
109
|
+
p.stdin.on("error", () => { });
|
|
110
|
+
if (c.stdin)
|
|
111
|
+
p.stdin.end(prompt);
|
|
112
|
+
else
|
|
113
|
+
p.stdin.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export async function bench(cases, cfg, o = {}) {
|
|
117
|
+
const rows = [], trials = cfg.trials ?? 1, template = cfg.promptTemplate ?? DEFAULT_PROMPT, logRoot = cfg.logDir ? resolve(cfg.logDir) : undefined;
|
|
118
|
+
const work = [];
|
|
119
|
+
for (const c of cases)
|
|
120
|
+
for (const k of cfg.contestants)
|
|
121
|
+
for (let trial = 0; trial < trials; trial++)
|
|
122
|
+
work.push(async () => {
|
|
123
|
+
const dir = mkdtempSync(join(tmpdir(), "amness-bench-")), t0 = Date.now(), row = { case: c.name, variant: k.name, trial, pass: false, steps: 0, tokens: 0, ms: 0 };
|
|
124
|
+
try {
|
|
125
|
+
for (const [f, content] of Object.entries(c.files)) {
|
|
126
|
+
mkdirSync(dirname(join(dir, f)), { recursive: true });
|
|
127
|
+
writeFileSync(join(dir, f), content);
|
|
128
|
+
}
|
|
129
|
+
if (c.setup) {
|
|
130
|
+
try {
|
|
131
|
+
await c.setup(dir);
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
row.excluded = e.message.slice(0, 300);
|
|
135
|
+
rows.push(row);
|
|
136
|
+
o.onRow?.(row);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (cfg.gitInit !== false) {
|
|
141
|
+
const g = (...a) => spawnSync("git", a, { cwd: dir, stdio: "ignore" });
|
|
142
|
+
g("init", "-q");
|
|
143
|
+
g("add", "-A");
|
|
144
|
+
g("-c", "user.email=bench@example.com", "-c", "user.name=bench", "commit", "-qm", "start", "--allow-empty");
|
|
145
|
+
}
|
|
146
|
+
const verify = c.verify ?? "(no check command was given for this task)", vars = { task: c.task, cwd: dir, model: k.model ?? "", verify }, prompt = fill(template, vars);
|
|
147
|
+
const r = await runContestant(k, vars, prompt, dir, k.timeoutSec ?? cfg.timeoutSec ?? 900), u = parseOutput(k.parse, r.stdout);
|
|
148
|
+
row.ms = r.ms;
|
|
149
|
+
row.steps = u.turns ?? 0;
|
|
150
|
+
row.tokens = (u.input ?? 0) + (u.output ?? 0);
|
|
151
|
+
row.prompt = u.input;
|
|
152
|
+
row.cached = u.cached;
|
|
153
|
+
const j = typeof c.check === "string" ? ((await runShell(c.check, dir, 120_000)).startsWith("exit=0") || "check command failed") : await c.check(dir);
|
|
154
|
+
row.pass = j === true;
|
|
155
|
+
row.metrics = { ...(u.costUsd !== undefined && { "cost-usd": u.costUsd }), secs: r.ms / 1000 };
|
|
156
|
+
row.note = r.startError ? `could not start ${k.command}: ${r.startError}` : r.timedOut ? `timed out after ${k.timeoutSec ?? cfg.timeoutSec ?? 900}s` : j !== true ? String(j || "failed") : u.isError ? "the tool reported an error but the check passed" : undefined;
|
|
157
|
+
if (logRoot) {
|
|
158
|
+
const f = join(logRoot, k.name, `${c.name}-${trial}`);
|
|
159
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
160
|
+
writeFileSync(f + ".out.txt", r.stdout);
|
|
161
|
+
writeFileSync(f + ".err.txt", r.stderr);
|
|
162
|
+
writeFileSync(f + ".cmd.json", JSON.stringify({ command: k.command, args: commandLine(k, vars).args, exit: r.code, ms: r.ms, usage: u, check: j }, null, 1));
|
|
163
|
+
}
|
|
164
|
+
void t0;
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
row.note = "crashed: " + e.message;
|
|
168
|
+
}
|
|
169
|
+
rows.push(row);
|
|
170
|
+
o.onRow?.(row);
|
|
171
|
+
try {
|
|
172
|
+
rmSync(dir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
catch { /* a leftover temp folder is harmless */ }
|
|
175
|
+
});
|
|
176
|
+
let next = 0;
|
|
177
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(cfg.jobs ?? 1, work.length)) }, async () => { while (next < work.length)
|
|
178
|
+
await work[next++](); }));
|
|
179
|
+
rows.sort((a, b) => cases.findIndex((x) => x.name === a.case) - cases.findIndex((x) => x.name === b.case) || cfg.contestants.findIndex((x) => x.name === a.variant) - cfg.contestants.findIndex((x) => x.name === b.variant) || a.trial - b.trial);
|
|
180
|
+
return { rows, analysis: analyze(rows, { baseline: o.baseline, k: Math.min(3, trials), seed: o.seed }) };
|
|
181
|
+
}
|
|
182
|
+
/** What each contestant would run, with the prompt, without running anything. */
|
|
183
|
+
export function describePlan(cases, cfg) {
|
|
184
|
+
const out = [], c = cases[0];
|
|
185
|
+
if (!c)
|
|
186
|
+
return ["no tasks"];
|
|
187
|
+
const verify = c.verify ?? "(no check command)", template = cfg.promptTemplate ?? DEFAULT_PROMPT;
|
|
188
|
+
out.push(`${cases.length} task(s) × ${cfg.contestants.length} contestant(s) × ${cfg.trials ?? 1} trial(s) = ${cases.length * cfg.contestants.length * (cfg.trials ?? 1)} runs, up to ${cfg.timeoutSec ?? 900}s each, ${cfg.jobs ?? 1} at a time`);
|
|
189
|
+
out.push("", "prompt for the first task:", ...fill(template, { task: c.task, verify, cwd: "<dir>", model: "" }).split("\n").map((l) => " " + l), "");
|
|
190
|
+
for (const k of cfg.contestants) {
|
|
191
|
+
const { file, args } = commandLine(k, { task: c.task, cwd: "<dir>", model: k.model ?? "", verify });
|
|
192
|
+
out.push(`${k.name}: ${[file, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" ")}${k.stdin ? " (prompt on stdin)" : ""}${k.model ? ` model: ${k.model}` : " model: the tool's default"}`, ...(k.note ? [` ${k.note}`] : []));
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export const installed = (command) => { const r = spawnSync(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" }); return r.status === 0; };
|
|
197
|
+
export const configFileTemplate = (amnessCli) => ({
|
|
198
|
+
contestants: [PRESETS.amness({ amnessCli }), PRESETS["amness-gate"]({ amnessCli }), PRESETS.claude({}), PRESETS.codex({})],
|
|
199
|
+
promptTemplate: DEFAULT_PROMPT, gitInit: true, timeoutSec: 900, trials: 3, jobs: 2, logDir: ".amness/bench/logs",
|
|
200
|
+
});
|
|
201
|
+
void existsSync;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryStore } from "./memory.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A portable session: provider-neutral JSON you can move to another machine, repo checkout or model.
|
|
4
|
+
* It deliberately carries a handoff note + memory + the files the agent changed, not a raw transcript, because transcripts
|
|
5
|
+
* are provider-specific (thinking blocks, tool-call formats) while a handoff note works anywhere.
|
|
6
|
+
*/
|
|
7
|
+
export type Bundle = {
|
|
8
|
+
schema: "amness.session/1";
|
|
9
|
+
createdAt: string;
|
|
10
|
+
task: string;
|
|
11
|
+
status: {
|
|
12
|
+
finished: boolean;
|
|
13
|
+
summary: string;
|
|
14
|
+
};
|
|
15
|
+
handoff: string;
|
|
16
|
+
memory: MemoryEntry[];
|
|
17
|
+
files: Record<string, string>;
|
|
18
|
+
from: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export declare function collectFiles(cwd: string, paths: Iterable<string>): Record<string, string>;
|
|
25
|
+
export declare const writeBundle: (path: string, b: Bundle) => string;
|
|
26
|
+
export declare function readBundle(path: string): Bundle;
|
|
27
|
+
/** Restore a bundle into `cwd`. Never overwrites a differing file unless asked; never writes outside `cwd`. */
|
|
28
|
+
export declare function applyBundle(b: Bundle, o: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
memory?: MemoryStore;
|
|
31
|
+
overwrite?: boolean;
|
|
32
|
+
}): {
|
|
33
|
+
restored: string[];
|
|
34
|
+
skipped: string[];
|
|
35
|
+
memory: number;
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, normalize, resolve, sep } from "node:path";
|
|
3
|
+
const MAX_FILE = 200_000, MAX_TOTAL = 2_000_000;
|
|
4
|
+
export function collectFiles(cwd, paths) {
|
|
5
|
+
const files = {};
|
|
6
|
+
let total = 0;
|
|
7
|
+
for (const p of paths) {
|
|
8
|
+
const abs = resolve(cwd, p);
|
|
9
|
+
if (!existsSync(abs) || !statSync(abs).isFile() || statSync(abs).size > MAX_FILE || total + statSync(abs).size > MAX_TOTAL)
|
|
10
|
+
continue;
|
|
11
|
+
const text = readFileSync(abs, "utf8");
|
|
12
|
+
if (text.includes("\0"))
|
|
13
|
+
continue; // binary
|
|
14
|
+
files[normalize(p).replace(/\\/g, "/")] = text;
|
|
15
|
+
total += text.length;
|
|
16
|
+
}
|
|
17
|
+
return files;
|
|
18
|
+
}
|
|
19
|
+
export const writeBundle = (path, b) => { mkdirSync(dirname(resolve(path)), { recursive: true }); writeFileSync(path, JSON.stringify(b, null, 2)); return path; };
|
|
20
|
+
export function readBundle(path) {
|
|
21
|
+
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
if (b?.schema !== "amness.session/1" || typeof b.handoff !== "string" || typeof b.task !== "string")
|
|
23
|
+
throw new Error(`${path} is not an Amness session bundle`);
|
|
24
|
+
return b;
|
|
25
|
+
}
|
|
26
|
+
/** Restore a bundle into `cwd`. Never overwrites a differing file unless asked; never writes outside `cwd`. */
|
|
27
|
+
export function applyBundle(b, o) {
|
|
28
|
+
const root = resolve(o.cwd), restored = [], skipped = [];
|
|
29
|
+
for (const [rel, content] of Object.entries(b.files)) {
|
|
30
|
+
const abs = resolve(root, rel);
|
|
31
|
+
if (isAbsolute(rel) || (abs !== root && !abs.startsWith(root + sep))) {
|
|
32
|
+
skipped.push(`${rel} (outside workspace)`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (existsSync(abs) && readFileSync(abs, "utf8") !== content && !o.overwrite) {
|
|
36
|
+
skipped.push(`${rel} (differs; kept yours)`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
40
|
+
writeFileSync(abs, content);
|
|
41
|
+
restored.push(rel);
|
|
42
|
+
}
|
|
43
|
+
let memory = 0;
|
|
44
|
+
if (o.memory)
|
|
45
|
+
for (const e of b.memory) {
|
|
46
|
+
if (o.memory.list().some((x) => x.name === e.name && x.scope === e.scope))
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
o.memory.save(e);
|
|
50
|
+
memory++;
|
|
51
|
+
}
|
|
52
|
+
catch { /* skip invalid entries */ }
|
|
53
|
+
}
|
|
54
|
+
return { restored, skipped, memory };
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Model, Tool } from "./harness.ts";
|
|
2
|
+
export type ChaosKind = "tools" | "429" | "args" | "slow-tools";
|
|
3
|
+
export declare const CHAOS_KINDS: ChaosKind[];
|
|
4
|
+
/** Make some tool calls fail with an error the model has to read and work around (never the finish tool). */
|
|
5
|
+
export declare function chaosTools(tools: Tool[], o?: {
|
|
6
|
+
rate?: number;
|
|
7
|
+
seed?: number;
|
|
8
|
+
delayMs?: number;
|
|
9
|
+
}): Tool[];
|
|
10
|
+
/** Make some model calls fail with a recoverable 429 and Retry-After (the rate limiter should absorb it), or answer with broken tool arguments (the harness should ask for a fix). */
|
|
11
|
+
export declare function chaosModel(model: Model, o: {
|
|
12
|
+
kind: "429" | "args";
|
|
13
|
+
rate?: number;
|
|
14
|
+
seed?: number;
|
|
15
|
+
}): Model;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { rng } from "./stats.js";
|
|
2
|
+
export const CHAOS_KINDS = ["tools", "429", "args", "slow-tools"];
|
|
3
|
+
/** Make some tool calls fail with an error the model has to read and work around (never the finish tool). */
|
|
4
|
+
export function chaosTools(tools, o = {}) {
|
|
5
|
+
const r = rng(o.seed ?? 1), rate = o.rate ?? 0.25;
|
|
6
|
+
return tools.map((t) => ({ ...t, run: async (a, c) => {
|
|
7
|
+
if (o.delayMs)
|
|
8
|
+
await new Promise((res) => setTimeout(res, o.delayMs));
|
|
9
|
+
if (r() < rate)
|
|
10
|
+
return `ERROR: simulated failure in ${t.name} (chaos test): try again or use another approach`;
|
|
11
|
+
return t.run(a, c);
|
|
12
|
+
} }));
|
|
13
|
+
}
|
|
14
|
+
/** Make some model calls fail with a recoverable 429 and Retry-After (the rate limiter should absorb it), or answer with broken tool arguments (the harness should ask for a fix). */
|
|
15
|
+
export function chaosModel(model, o) {
|
|
16
|
+
const r = rng(o.seed ?? 1), rate = o.rate ?? 0.3;
|
|
17
|
+
const call = (async (messages, tools, opts) => {
|
|
18
|
+
if (o.kind === "429" && r() < rate)
|
|
19
|
+
throw Object.assign(new Error("HTTP 429: rate limit reached (chaos test)"), { status: 429, retryAfterMs: 50 });
|
|
20
|
+
const m = await model(messages, tools, opts);
|
|
21
|
+
if (o.kind === "args" && m.tool_calls?.length && r() < rate)
|
|
22
|
+
return { ...m, tool_calls: m.tool_calls.map((c) => ({ ...c, function: { ...c.function, arguments: (c.function.arguments || "{}").slice(0, -1) } })) }; // truncated JSON
|
|
23
|
+
return m;
|
|
24
|
+
});
|
|
25
|
+
return Object.assign(call, model);
|
|
26
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { type SandboxMode } from "./sandbox.ts";
|
|
2
|
+
import { type Bundle } from "./bundle.ts";
|
|
3
|
+
import { Registry, type Approve, type Budget, type Event, type Guard, type Message, type Model, type Tool } from "./harness.ts";
|
|
4
|
+
import { MemoryStore, type MemoryBackend } from "./memory.ts";
|
|
5
|
+
import type { Embedder } from "./embed.ts";
|
|
6
|
+
import { Guidance } from "./guidance.ts";
|
|
7
|
+
import { ProcessManager } from "./procs.ts";
|
|
8
|
+
import { Diagnostics, type LspServer } from "./lsp.ts";
|
|
9
|
+
import { CodeIndex } from "./codeindex.ts";
|
|
10
|
+
import { type AgentDef } from "./agents.ts";
|
|
11
|
+
import type { Roles } from "./route.ts";
|
|
12
|
+
import { type Skill } from "./skills.ts";
|
|
13
|
+
import type { HookRunner } from "./hooks.ts";
|
|
14
|
+
import { type Plugin } from "./plugins.ts";
|
|
15
|
+
import { type PermissionChecker } from "./permissions.ts";
|
|
16
|
+
export declare const riskOf: (cmd: string) => string | false;
|
|
17
|
+
/** How much the agent may do without asking. default: ask for edits and non-trivial shell; acceptEdits: ask only for shell; plan: read-only; yolo: never ask. */
|
|
18
|
+
export type PermMode = "default" | "acceptEdits" | "plan" | "yolo";
|
|
19
|
+
/** Inspection-only shell commands (no chaining, redirects or substitutions): always allowed, even in plan mode. */
|
|
20
|
+
export declare const READONLY_CMD: RegExp;
|
|
21
|
+
/** Explain "command not found" (exit 127): the name is not a program here, and say so when it is really one of the agent's own tools. */
|
|
22
|
+
export declare function withNotFoundHint(out: string, isTool?: (name: string) => boolean, forUser?: boolean): string;
|
|
23
|
+
export declare function runShell(cmd: string, cwd: string, ms?: number, signal?: AbortSignal, sandbox?: SandboxMode): Promise<string>;
|
|
24
|
+
/** File + shell tools confined to `cwd`. Enforces read-before-edit; flags dangerous shell commands as risky. */
|
|
25
|
+
export declare function codeTools(cwd: string, o?: {
|
|
26
|
+
beforeShell?: (cmd: string) => Promise<void>; /** OS sandbox for shell commands, read each time so it can be switched live */
|
|
27
|
+
sandbox?: () => SandboxMode; /** background processes (sh_start, sh_read, sh_stop, sh_list) */
|
|
28
|
+
procs?: ProcessManager; /** folder-level instruction files, appended to results the first time the agent works in that folder */
|
|
29
|
+
guidance?: Guidance;
|
|
30
|
+
onChange?: (path: string) => void;
|
|
31
|
+
mode?: () => PermMode; /** Run after each successful edit; returns text to append to the tool result (empty when all is well). */
|
|
32
|
+
check?: (paths: string[]) => Promise<string>;
|
|
33
|
+
}): Tool[];
|
|
34
|
+
export declare const CODE_SYSTEM = "You are an expert software engineer working autonomously in a repository. Complete the task, then call `finish`.\nWorkflow:\n1. EXPLORE (proportionate: a small fix needs a handful of reads, not a survey; stop when you know where to change): list_files / grep / read_file to understand the code before changing it. Reproduce the problem or run the existing tests first when possible.\n2. EDIT: smallest correct change, matching surrounding style. Prefer edit_file over rewriting files (several replacements in one file: its `edits` list); use apply_patch when one change spans several files. Do not touch unrelated code. Never edit tests to make them pass unless the task says so.\n3. VERIFY: run the tests/build/command that proves it works. Read failures carefully and fix the root cause. If an approach fails twice, rethink.\n4. FINISH: `finish(summary)` with what changed, how you verified, and assumptions. It is rejected if verification fails.\nMEMORY: notes from earlier sessions may appear below the task; treat them as hints to verify. When you learn something durable that is NOT obvious from the code (the real test command, a convention the user insists on, a correction), save it with memory_save. Never save secrets or task-progress chatter.\nNever invent file contents or command output. Don't ask the user questions; choose sensible defaults. Some commands need human approval; if denied, use another route.";
|
|
35
|
+
/** Conversational variant of the prompt: answers questions directly, plans, delegates exploration, works within permission modes. */
|
|
36
|
+
export declare const CHAT_SYSTEM = "You are an expert software engineer pair-programming with the user in their terminal, inside their repository. Be concise and direct; format with Markdown (short paragraphs, `inline code`, fenced code blocks).\nHow to work:\n- Questions: answer from the code. Explore first (list_files, grep, read_file; in a large repository start with repo_map, find_refs and impact when you have them) instead of guessing; cite files as path:line. Use the `task` tool for broad searches so your own context stays clean.\n- Effort: match effort to the question. A fact or one-file question: 3-10 tool calls, no subagent. Comparing or tracing a few things: 10-15 calls. Only a genuinely broad survey justifies `task`, and then give each subagent a distinct slice. When your budget notes say it is running low, stop exploring and give the best answer you have, saying what you did not cover.\n- Changes: read a file before editing it, make the smallest correct change, then verify it (run the tests/build). For anything with 3+ steps, keep a checklist with `todo_write` and update it as you go (exactly one item in_progress).\n- When you finish a coding task, call `finish(summary)` (what changed, how you verified). For plain questions just answer; no `finish` needed.\n- Permissions: the user may be asked to approve edits and shell commands. If a call is denied, do not retry it; use another approach or explain what you need. In plan mode you are read-only: investigate, then present a plan with `exit_plan_mode` and wait for approval.\n- If the request is ambiguous in a way that matters, ask ONE short clarifying question; otherwise choose sensible defaults and say what you assumed.\n- Notes from earlier sessions may appear below the user's message; treat them as hints to verify. Save durable, non-obvious lessons with memory_save (never secrets).\n- Servers and watchers: start them with `sh_start` (plain `sh` waits for the command to end), read their output with `sh_read`, and `sh_stop` them when you are done.\n- Never invent file contents or command output. The user can type while you work; their new messages arrive between your steps, so adjust course when they do.";
|
|
37
|
+
export type CodeAgentOptions = {
|
|
38
|
+
/** Conversational mode (the TUI): chat prompt, plus todo_write, task (read-only explorer subagent) and exit_plan_mode tools. */
|
|
39
|
+
chat?: boolean;
|
|
40
|
+
/** Live permission mode. When given, edits and shell commands ask for approval per the mode (see PermMode); omit for the autonomous behaviour. */
|
|
41
|
+
permissions?: {
|
|
42
|
+
mode: () => PermMode;
|
|
43
|
+
};
|
|
44
|
+
/** Polled every turn for messages the user typed while the agent works. */
|
|
45
|
+
inbox?: () => string[];
|
|
46
|
+
model: Model;
|
|
47
|
+
cwd: string;
|
|
48
|
+
/** Shell command that must exit 0 (e.g. "npm test") before `finish` is accepted. */
|
|
49
|
+
verify?: string;
|
|
50
|
+
/** Fast command (typecheck, lint) run after every edit. Only a failure is shown to the model, appended to the edit result, so mistakes are caught one step after they happen. */
|
|
51
|
+
check?: string;
|
|
52
|
+
/** Ask an installed language server (typescript-language-server, pyright, gopls, rust-analyzer) for errors in each file after it is edited, and append them to the edit result. Pass servers to override the defaults. Off by default. */
|
|
53
|
+
lsp?: boolean | {
|
|
54
|
+
servers?: LspServer[];
|
|
55
|
+
};
|
|
56
|
+
approve?: Approve;
|
|
57
|
+
budget?: Budget;
|
|
58
|
+
maxSteps?: number;
|
|
59
|
+
tools?: Tool[];
|
|
60
|
+
/** Let the agent define its own shell-backed tools (each creation needs approval). Saved to `.amness/tools.json` and reloaded next run. */
|
|
61
|
+
selfExtend?: boolean;
|
|
62
|
+
off?: Guard[];
|
|
63
|
+
onEvent?: (e: Event) => void;
|
|
64
|
+
/** Persist transcript here after every turn; run(task, {resume:true}) continues from it. */
|
|
65
|
+
checkpointFile?: string;
|
|
66
|
+
/** Durable memory across sessions (markdown files in <cwd>/.amness/memory + ~/.amness/memory). Default on. */
|
|
67
|
+
memory?: boolean | MemoryStore | MemoryBackend;
|
|
68
|
+
/** Load AGENTS.md (or CLAUDE.md / GEMINI.md) from the workspace root and from folders the agent works in. Default on. */
|
|
69
|
+
guidance?: boolean;
|
|
70
|
+
/** Called before a command that may change files runs: take a snapshot, note that it cannot be undone. */
|
|
71
|
+
beforeShell?: (cmd: string) => Promise<void>;
|
|
72
|
+
/** Mask secrets in tool output before the model sees it (default on). */
|
|
73
|
+
redact?: boolean;
|
|
74
|
+
/** Commands that contain text copied from untrusted tool results (web pages, MCP servers): "ask" (default), "deny" or "off". */
|
|
75
|
+
taint?: "ask" | "deny" | "off";
|
|
76
|
+
/** Record full messages in events so a trace can be replayed (`replay`). */
|
|
77
|
+
record?: boolean;
|
|
78
|
+
/** Structural code index tools (repo_map, find_refs, impact), offered when the repository has at least `minFiles` source files (default 25). */
|
|
79
|
+
index?: boolean | {
|
|
80
|
+
minFiles?: number;
|
|
81
|
+
};
|
|
82
|
+
/** Discover custom subagents (.amness/agents/*.md, .claude/agents/*.md; project and home) for the `task` tool. Default on. */
|
|
83
|
+
agents?: boolean;
|
|
84
|
+
/** Models that agents can name in their front matter (`model: fast`). */
|
|
85
|
+
agentModels?: Record<string, Model>;
|
|
86
|
+
/** Different models for different jobs: `explore` (the read-only explorer), `compact` (handoff notes and /compact), `review` (the independent reviewer). Unset roles use `model`. */
|
|
87
|
+
models?: Roles;
|
|
88
|
+
/** Discover SKILL.md folders (.amness/skills, .agents/skills, .claude/skills; project and home) and offer them through the `skill` tool. Default on. */
|
|
89
|
+
skills?: boolean;
|
|
90
|
+
/** Run shell commands under an OS sandbox: "workspace" (writes only in the project and temp/cache folders, `.git` read-only) or "strict" (also no network). A getter lets you switch it live. */
|
|
91
|
+
sandbox?: SandboxMode | (() => SandboxMode);
|
|
92
|
+
/** An independent, skeptical reviewer (a fresh context that cannot edit): it writes acceptance criteria before the work and checks the result against them before a finish is accepted. Costs extra model calls; off by default. `model` lets it be a different model. */
|
|
93
|
+
review?: boolean | {
|
|
94
|
+
model?: Model;
|
|
95
|
+
};
|
|
96
|
+
/** A second model looks at each risky call before the user is asked: it can approve (with a stated reason, logged as a policy event) or pass the call on. It never sees the safety floor or untrusted-text calls. Pass { model } to use a cheaper model. Costs one short model call per risky action. */
|
|
97
|
+
approvalReviewer?: boolean | {
|
|
98
|
+
model?: Model;
|
|
99
|
+
};
|
|
100
|
+
/** Turns without any measurable improvement before the loop reflects and then stops (default 8; 0 = off). */
|
|
101
|
+
stagnationTurns?: number;
|
|
102
|
+
/** Background process tools (sh_start / sh_read / sh_stop / sh_list). Default on. Call `agent.procs?.dispose()` when you are done. */
|
|
103
|
+
background?: boolean;
|
|
104
|
+
/** Lifecycle hooks, e.g. `Hooks.load(cwd, { trustProject: true })`. Nothing runs unless you pass this. */
|
|
105
|
+
hooks?: HookRunner;
|
|
106
|
+
/** Create a model from an id (same provider): needed for plugins that route requests with `route`. */
|
|
107
|
+
modelById?: (id: string) => Model;
|
|
108
|
+
/** Wrap or replace every tool the agent gets (chaos evals use it to make tools fail on purpose). */
|
|
109
|
+
wrapTools?: (tools: Tool[]) => Tool[];
|
|
110
|
+
/** Plugins (see plugins.ts): extra tools, in-process hooks on every loop event, system-prompt rewrites. */
|
|
111
|
+
plugins?: Plugin[];
|
|
112
|
+
/** Permission rules plus the built-in safety floor (see `Permissions`). Default: the floor alone. Pass `false` to switch even that off. */
|
|
113
|
+
rules?: PermissionChecker | false;
|
|
114
|
+
/** Turns on semantic memory retrieval (blended with keyword ranking). See `openaiEmbedder`. */
|
|
115
|
+
embedder?: Embedder;
|
|
116
|
+
/** Start a fresh context with a handoff note when the transcript gets big. Default: at 120k prompt tokens / 500k chars. */
|
|
117
|
+
rollover?: {
|
|
118
|
+
afterTokens?: number;
|
|
119
|
+
afterChars?: number;
|
|
120
|
+
} | false;
|
|
121
|
+
/** Interrupt a running agent (e.g. Ctrl-C). It stops at a safe point with the checkpoint written, so it can be resumed. */
|
|
122
|
+
signal?: AbortSignal | (() => AbortSignal | undefined);
|
|
123
|
+
/** Recorded in exported bundles so you can see where a session came from. */
|
|
124
|
+
meta?: {
|
|
125
|
+
provider?: string;
|
|
126
|
+
model?: string;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export type RunOptions = {
|
|
130
|
+
images?: {
|
|
131
|
+
mime: string;
|
|
132
|
+
data: string;
|
|
133
|
+
}[];
|
|
134
|
+
resume?: boolean;
|
|
135
|
+
bundle?: Bundle | string;
|
|
136
|
+
overwrite?: boolean;
|
|
137
|
+
};
|
|
138
|
+
export declare function createCodeAgent(o: CodeAgentOptions): {
|
|
139
|
+
/** The live toolbox: add/remove tools between or during runs. */
|
|
140
|
+
registry: Registry;
|
|
141
|
+
memory: MemoryStore | undefined;
|
|
142
|
+
guidance: Guidance | undefined;
|
|
143
|
+
procs: ProcessManager | undefined;
|
|
144
|
+
lsp: Diagnostics | undefined;
|
|
145
|
+
codeIndex: CodeIndex | undefined;
|
|
146
|
+
readonly agents: AgentDef[];
|
|
147
|
+
readonly agentWarnings: string[];
|
|
148
|
+
readonly skills: Skill[];
|
|
149
|
+
readonly skillWarnings: string[];
|
|
150
|
+
/** Keep the provider's prompt cache alive across an idle gap (see createHarness().warm). */
|
|
151
|
+
warm: (msgs: Message[], opts?: {
|
|
152
|
+
signal?: AbortSignal;
|
|
153
|
+
}) => Promise<{
|
|
154
|
+
prompt: number;
|
|
155
|
+
cached: number;
|
|
156
|
+
}>;
|
|
157
|
+
readonly todos: {
|
|
158
|
+
content: string;
|
|
159
|
+
status: "pending" | "in_progress" | "done";
|
|
160
|
+
}[];
|
|
161
|
+
/** One conversational turn: `history` is everything so far (empty to start); returns the updated transcript in `messages`. */
|
|
162
|
+
chat(history: Message[], userText: string, extra?: {
|
|
163
|
+
images?: {
|
|
164
|
+
mime: string;
|
|
165
|
+
data: string;
|
|
166
|
+
}[];
|
|
167
|
+
}): Promise<import("./harness.ts").RunResult>;
|
|
168
|
+
run(t: string, opts?: RunOptions): Promise<import("./harness.ts").RunResult>;
|
|
169
|
+
/** Keep talking to the agent after it finished (or stopped): same conversation, full context. */
|
|
170
|
+
followUp(prev: {
|
|
171
|
+
messages: Message[];
|
|
172
|
+
}, text: string): Promise<import("./harness.ts").RunResult>;
|
|
173
|
+
/** Replace a long conversation by a model-written handoff note (same mechanism as automatic rollover, on demand). */
|
|
174
|
+
compact(messages: Message[]): Promise<{
|
|
175
|
+
messages: Message[];
|
|
176
|
+
note: string;
|
|
177
|
+
}>;
|
|
178
|
+
/** Package the work so far for another machine / model / day. Asks the model for a handoff note unless one is supplied. */
|
|
179
|
+
exportBundle(result: {
|
|
180
|
+
messages: Message[];
|
|
181
|
+
finished: boolean;
|
|
182
|
+
text: string;
|
|
183
|
+
}, path?: string, handoff?: string): Promise<{
|
|
184
|
+
bundle: Bundle;
|
|
185
|
+
path?: string;
|
|
186
|
+
}>;
|
|
187
|
+
};
|