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.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -2
  3. package/dist/cli/acpcli.d.ts +7 -0
  4. package/dist/cli/acpcli.js +14 -0
  5. package/dist/cli/amness.d.ts +2 -0
  6. package/dist/cli/amness.js +693 -0
  7. package/dist/cli/benchcli.d.ts +5 -0
  8. package/dist/cli/benchcli.js +160 -0
  9. package/dist/cli/config.d.ts +79 -0
  10. package/dist/cli/config.js +54 -0
  11. package/dist/cli/demo.d.ts +13 -0
  12. package/dist/cli/demo.js +44 -0
  13. package/dist/cli/detect.d.ts +2 -0
  14. package/dist/cli/detect.js +20 -0
  15. package/dist/cli/evalcli.d.ts +15 -0
  16. package/dist/cli/evalcli.js +198 -0
  17. package/dist/cli/format.d.ts +5 -0
  18. package/dist/cli/format.js +55 -0
  19. package/dist/cli/headless.d.ts +2 -0
  20. package/dist/cli/headless.js +169 -0
  21. package/dist/cli/model.d.ts +11 -0
  22. package/dist/cli/model.js +19 -0
  23. package/dist/cli/plugincli.d.ts +2 -0
  24. package/dist/cli/plugincli.js +176 -0
  25. package/dist/cli/schedulecli.d.ts +2 -0
  26. package/dist/cli/schedulecli.js +66 -0
  27. package/dist/cli/state.d.ts +11 -0
  28. package/dist/cli/state.js +19 -0
  29. package/dist/cli/tui/ansi.d.ts +38 -0
  30. package/dist/cli/tui/ansi.js +142 -0
  31. package/dist/cli/tui/app.d.ts +328 -0
  32. package/dist/cli/tui/app.js +1358 -0
  33. package/dist/cli/tui/blocks.d.ts +151 -0
  34. package/dist/cli/tui/blocks.js +128 -0
  35. package/dist/cli/tui/brand.d.ts +9 -0
  36. package/dist/cli/tui/brand.js +16 -0
  37. package/dist/cli/tui/canvas.d.ts +92 -0
  38. package/dist/cli/tui/canvas.js +199 -0
  39. package/dist/cli/tui/cards.d.ts +44 -0
  40. package/dist/cli/tui/cards.js +215 -0
  41. package/dist/cli/tui/clipboard.d.ts +5 -0
  42. package/dist/cli/tui/clipboard.js +29 -0
  43. package/dist/cli/tui/commands.d.ts +10 -0
  44. package/dist/cli/tui/commands.js +649 -0
  45. package/dist/cli/tui/complete.d.ts +39 -0
  46. package/dist/cli/tui/complete.js +113 -0
  47. package/dist/cli/tui/custom.d.ts +9 -0
  48. package/dist/cli/tui/custom.js +34 -0
  49. package/dist/cli/tui/diffview.d.ts +25 -0
  50. package/dist/cli/tui/diffview.js +130 -0
  51. package/dist/cli/tui/editor.d.ts +60 -0
  52. package/dist/cli/tui/editor.js +230 -0
  53. package/dist/cli/tui/full.d.ts +86 -0
  54. package/dist/cli/tui/full.js +630 -0
  55. package/dist/cli/tui/keys.d.ts +31 -0
  56. package/dist/cli/tui/keys.js +134 -0
  57. package/dist/cli/tui/markdown.d.ts +37 -0
  58. package/dist/cli/tui/markdown.js +155 -0
  59. package/dist/cli/tui/mentions.d.ts +15 -0
  60. package/dist/cli/tui/mentions.js +48 -0
  61. package/dist/cli/tui/modal.d.ts +26 -0
  62. package/dist/cli/tui/modal.js +107 -0
  63. package/dist/cli/tui/overlays.d.ts +98 -0
  64. package/dist/cli/tui/overlays.js +237 -0
  65. package/dist/cli/tui/pager.d.ts +15 -0
  66. package/dist/cli/tui/pager.js +66 -0
  67. package/dist/cli/tui/pricing.d.ts +9 -0
  68. package/dist/cli/tui/pricing.js +23 -0
  69. package/dist/cli/tui/rich.d.ts +16 -0
  70. package/dist/cli/tui/rich.js +72 -0
  71. package/dist/cli/tui/screen.d.ts +30 -0
  72. package/dist/cli/tui/screen.js +62 -0
  73. package/dist/cli/tui/session.d.ts +77 -0
  74. package/dist/cli/tui/session.js +103 -0
  75. package/dist/cli/tui/sidebar.d.ts +84 -0
  76. package/dist/cli/tui/sidebar.js +114 -0
  77. package/dist/cli/tui/text.d.ts +15 -0
  78. package/dist/cli/tui/text.js +94 -0
  79. package/dist/cli/tui/theme.d.ts +59 -0
  80. package/dist/cli/tui/theme.js +148 -0
  81. package/dist/cli/tui/themes.d.ts +56 -0
  82. package/dist/cli/tui/themes.js +80 -0
  83. package/dist/cli/tui/tokens.d.ts +39 -0
  84. package/dist/cli/tui/tokens.js +71 -0
  85. package/dist/cli/tui/view.d.ts +60 -0
  86. package/dist/cli/tui/view.js +178 -0
  87. package/dist/cli/ui.d.ts +34 -0
  88. package/dist/cli/ui.js +224 -0
  89. package/dist/src/acp.d.ts +28 -0
  90. package/dist/src/acp.js +111 -0
  91. package/dist/src/agents.d.ts +23 -0
  92. package/dist/src/agents.js +71 -0
  93. package/dist/src/anthropic.d.ts +30 -0
  94. package/dist/src/anthropic.js +91 -0
  95. package/dist/src/approver.d.ts +15 -0
  96. package/dist/src/approver.js +40 -0
  97. package/dist/src/bench.d.ts +67 -0
  98. package/dist/src/bench.js +201 -0
  99. package/dist/src/bundle.d.ts +36 -0
  100. package/dist/src/bundle.js +55 -0
  101. package/dist/src/chaos.d.ts +15 -0
  102. package/dist/src/chaos.js +26 -0
  103. package/dist/src/code.d.ts +187 -0
  104. package/dist/src/code.js +590 -0
  105. package/dist/src/codeindex.d.ts +65 -0
  106. package/dist/src/codeindex.js +300 -0
  107. package/dist/src/deep.d.ts +62 -0
  108. package/dist/src/deep.js +177 -0
  109. package/dist/src/diff.d.ts +8 -0
  110. package/dist/src/diff.js +74 -0
  111. package/dist/src/dynamic.d.ts +42 -0
  112. package/dist/src/dynamic.js +133 -0
  113. package/dist/src/embed.d.ts +8 -0
  114. package/dist/src/embed.js +25 -0
  115. package/dist/src/evalcases.d.ts +11 -0
  116. package/dist/src/evalcases.js +196 -0
  117. package/dist/src/evals.d.ts +197 -0
  118. package/dist/src/evals.js +340 -0
  119. package/dist/src/gitflow.d.ts +27 -0
  120. package/dist/src/gitflow.js +56 -0
  121. package/dist/src/grade.d.ts +42 -0
  122. package/dist/src/grade.js +96 -0
  123. package/dist/src/guidance.d.ts +15 -0
  124. package/dist/src/guidance.js +68 -0
  125. package/dist/src/hardcases.d.ts +2 -0
  126. package/dist/src/hardcases.js +94 -0
  127. package/dist/src/harness.d.ts +411 -0
  128. package/dist/src/harness.js +725 -0
  129. package/dist/src/hooks.d.ts +44 -0
  130. package/dist/src/hooks.js +116 -0
  131. package/dist/src/images.d.ts +13 -0
  132. package/dist/src/images.js +27 -0
  133. package/dist/src/index.d.ts +57 -0
  134. package/dist/src/index.js +57 -0
  135. package/dist/src/ledger.d.ts +58 -0
  136. package/dist/src/ledger.js +78 -0
  137. package/dist/src/live.d.ts +16 -0
  138. package/dist/src/live.js +80 -0
  139. package/dist/src/lsp.d.ts +31 -0
  140. package/dist/src/lsp.js +147 -0
  141. package/dist/src/memory.d.ts +128 -0
  142. package/dist/src/memory.js +390 -0
  143. package/dist/src/metrics.d.ts +3 -0
  144. package/dist/src/metrics.js +42 -0
  145. package/dist/src/mining.d.ts +49 -0
  146. package/dist/src/mining.js +126 -0
  147. package/dist/src/openai.d.ts +50 -0
  148. package/dist/src/openai.js +307 -0
  149. package/dist/src/parallel.d.ts +66 -0
  150. package/dist/src/parallel.js +135 -0
  151. package/dist/src/patch.d.ts +45 -0
  152. package/dist/src/patch.js +219 -0
  153. package/dist/src/permissions.d.ts +46 -0
  154. package/dist/src/permissions.js +245 -0
  155. package/dist/src/plugins.d.ts +148 -0
  156. package/dist/src/plugins.js +225 -0
  157. package/dist/src/pluginscan.d.ts +8 -0
  158. package/dist/src/pluginscan.js +20 -0
  159. package/dist/src/procs.d.ts +41 -0
  160. package/dist/src/procs.js +94 -0
  161. package/dist/src/profiles.d.ts +36 -0
  162. package/dist/src/profiles.js +61 -0
  163. package/dist/src/progress.d.ts +29 -0
  164. package/dist/src/progress.js +75 -0
  165. package/dist/src/prompts.d.ts +10 -0
  166. package/dist/src/prompts.js +28 -0
  167. package/dist/src/ratelimit.d.ts +34 -0
  168. package/dist/src/ratelimit.js +116 -0
  169. package/dist/src/replay.d.ts +25 -0
  170. package/dist/src/replay.js +99 -0
  171. package/dist/src/review.d.ts +20 -0
  172. package/dist/src/review.js +49 -0
  173. package/dist/src/route.d.ts +9 -0
  174. package/dist/src/route.js +30 -0
  175. package/dist/src/router.d.ts +61 -0
  176. package/dist/src/router.js +126 -0
  177. package/dist/src/sandbox.d.ts +13 -0
  178. package/dist/src/sandbox.js +68 -0
  179. package/dist/src/schedule.d.ts +20 -0
  180. package/dist/src/schedule.js +59 -0
  181. package/dist/src/schema.d.ts +13 -0
  182. package/dist/src/schema.js +36 -0
  183. package/dist/src/secrets.d.ts +10 -0
  184. package/dist/src/secrets.js +41 -0
  185. package/dist/src/shell.d.ts +34 -0
  186. package/dist/src/shell.js +95 -0
  187. package/dist/src/skills.d.ts +20 -0
  188. package/dist/src/skills.js +112 -0
  189. package/dist/src/snapshot.d.ts +20 -0
  190. package/dist/src/snapshot.js +93 -0
  191. package/dist/src/stats.d.ts +24 -0
  192. package/dist/src/stats.js +62 -0
  193. package/dist/src/strategies.d.ts +16 -0
  194. package/dist/src/strategies.js +35 -0
  195. package/dist/src/suites.d.ts +30 -0
  196. package/dist/src/suites.js +112 -0
  197. package/dist/src/trace.d.ts +63 -0
  198. package/dist/src/trace.js +153 -0
  199. package/dist/src/tune.d.ts +38 -0
  200. package/dist/src/tune.js +49 -0
  201. package/dist/src/web.d.ts +19 -0
  202. package/dist/src/web.js +56 -0
  203. package/dist/src/worktree.d.ts +24 -0
  204. package/dist/src/worktree.js +85 -0
  205. package/docs/agent-loop.md +222 -0
  206. package/docs/architecture.md +228 -0
  207. package/docs/bench.md +293 -0
  208. package/docs/budgets.md +185 -0
  209. package/docs/build-your-own-harness.md +424 -0
  210. package/docs/cookbook.md +375 -0
  211. package/docs/development.md +150 -0
  212. package/docs/evaluation.md +450 -0
  213. package/docs/features.md +1320 -0
  214. package/docs/getting-started.md +196 -0
  215. package/docs/headless.md +183 -0
  216. package/docs/images/approval.png +0 -0
  217. package/docs/images/chat.png +0 -0
  218. package/docs/images/daylight.png +0 -0
  219. package/docs/images/loop.png +0 -0
  220. package/docs/images/palette.png +0 -0
  221. package/docs/images/running.png +0 -0
  222. package/docs/images/subagent-done.png +0 -0
  223. package/docs/images/subagent-running.png +0 -0
  224. package/docs/images/welcome.png +0 -0
  225. package/docs/index.md +25 -0
  226. package/docs/library.md +276 -0
  227. package/docs/plugins.md +691 -0
  228. package/docs/providers.md +175 -0
  229. package/docs/safety.md +237 -0
  230. package/docs/usage.md +348 -0
  231. package/docs/why.md +155 -0
  232. package/docs/writing-guide.md +39 -0
  233. package/package.json +72 -4
@@ -0,0 +1,24 @@
1
+ /** Deterministic PRNG (mulberry32) so a report can be reproduced exactly. */
2
+ export declare function rng(seed?: number): () => number;
3
+ export declare const mean: (xs: number[]) => number;
4
+ export declare function quantile(sorted: number[], q: number): number;
5
+ export type Interval = {
6
+ lo: number;
7
+ hi: number;
8
+ };
9
+ /** Percentile bootstrap CI of `stat` over tasks (each element is one task's data). */
10
+ export declare function bootstrap<T>(items: T[], stat: (xs: T[]) => number, o?: {
11
+ B?: number;
12
+ seed?: number;
13
+ level?: number;
14
+ }): Interval;
15
+ /** Exact two-sided sign test: of the tasks where two variants differ, `wins` favour A and `losses` favour B (McNemar's exact test on discordant pairs). */
16
+ export declare function signTest(wins: number, losses: number): number;
17
+ /** Unbiased estimate of "all k of k independent attempts pass" from c passes in n trials (reliability, as in pass^k). */
18
+ export declare const passPowK: (c: number, n: number, k: number) => number;
19
+ /** Unbiased estimate of "at least one of k attempts passes" (pass@k). */
20
+ export declare const passAtK: (c: number, n: number, k: number) => number;
21
+ /** Holm-Bonferroni adjusted p-values (controls the family-wise error when comparing several variants at once). */
22
+ export declare function holm(ps: number[]): number[];
23
+ /** Smallest paired difference in pass rate that `n` tasks can reliably detect (rough, 80% power, alpha 0.05, worst-case variance). */
24
+ export declare const minDetectable: (nTasks: number) => number;
@@ -0,0 +1,62 @@
1
+ // Small, dependency-free statistics for comparing agent variants. Tasks are the unit of analysis: repeated trials of one task are
2
+ // correlated, so every interval here resamples tasks, never individual runs.
3
+ /** Deterministic PRNG (mulberry32) so a report can be reproduced exactly. */
4
+ export function rng(seed = 1) {
5
+ let a = seed >>> 0;
6
+ return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
7
+ }
8
+ export const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : NaN);
9
+ export function quantile(sorted, q) {
10
+ if (!sorted.length)
11
+ return NaN;
12
+ const i = (sorted.length - 1) * q, lo = Math.floor(i), hi = Math.ceil(i);
13
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (i - lo);
14
+ }
15
+ /** Percentile bootstrap CI of `stat` over tasks (each element is one task's data). */
16
+ export function bootstrap(items, stat, o = {}) {
17
+ if (items.length < 2) {
18
+ const v = items.length ? stat(items) : NaN;
19
+ return { lo: v, hi: v };
20
+ }
21
+ const B = o.B ?? 4000, r = rng(o.seed ?? 7), est = [];
22
+ for (let b = 0; b < B; b++) {
23
+ const s = [];
24
+ for (let i = 0; i < items.length; i++)
25
+ s.push(items[Math.floor(r() * items.length)]);
26
+ const v = stat(s);
27
+ if (!Number.isNaN(v))
28
+ est.push(v);
29
+ }
30
+ est.sort((x, y) => x - y);
31
+ const a = (1 - (o.level ?? 0.95)) / 2;
32
+ return { lo: quantile(est, a), hi: quantile(est, 1 - a) };
33
+ }
34
+ /** Exact two-sided sign test: of the tasks where two variants differ, `wins` favour A and `losses` favour B (McNemar's exact test on discordant pairs). */
35
+ export function signTest(wins, losses) {
36
+ const n = wins + losses;
37
+ if (n === 0)
38
+ return 1;
39
+ const k = Math.min(wins, losses);
40
+ let p = 0, c = 1; // c = C(n, i) built iteratively
41
+ for (let i = 0; i <= k; i++) {
42
+ p += c;
43
+ c = (c * (n - i)) / (i + 1);
44
+ }
45
+ return Math.min(1, (2 * p) / 2 ** n);
46
+ }
47
+ const choose = (n, k) => { if (k < 0 || k > n)
48
+ return 0; let r = 1; for (let i = 1; i <= k; i++)
49
+ r = (r * (n - k + i)) / i; return r; };
50
+ /** Unbiased estimate of "all k of k independent attempts pass" from c passes in n trials (reliability, as in pass^k). */
51
+ export const passPowK = (c, n, k) => (k > n ? NaN : choose(c, k) / choose(n, k));
52
+ /** Unbiased estimate of "at least one of k attempts passes" (pass@k). */
53
+ export const passAtK = (c, n, k) => (k > n ? NaN : 1 - choose(n - c, k) / choose(n, k));
54
+ /** Holm-Bonferroni adjusted p-values (controls the family-wise error when comparing several variants at once). */
55
+ export function holm(ps) {
56
+ const idx = ps.map((p, i) => [p, i]).sort((a, b) => a[0] - b[0]), out = new Array(ps.length);
57
+ let run = 0;
58
+ idx.forEach(([p, i], rank) => { run = Math.max(run, Math.min(1, p * (ps.length - rank))); out[i] = run; });
59
+ return out;
60
+ }
61
+ /** Smallest paired difference in pass rate that `n` tasks can reliably detect (rough, 80% power, alpha 0.05, worst-case variance). */
62
+ export const minDetectable = (nTasks) => (nTasks > 0 ? Math.min(1, (2.8 * Math.sqrt(0.5)) / Math.sqrt(nTasks)) : 1);
@@ -0,0 +1,16 @@
1
+ import type { Plugin } from "./plugins.ts";
2
+ /** Write a short plan before touching any tool, then work as usual. Costs one extra model call per request. */
3
+ export declare function planFirst(o?: {
4
+ note?: string;
5
+ }): Plugin;
6
+ /** Two phases: look around with read-only tools for a few steps, then act. Stops the habit of editing before understanding. */
7
+ export declare function exploreThenAct(o?: {
8
+ readSteps?: number;
9
+ }): Plugin;
10
+ /**
11
+ * Before finishing a task that points at a document ("as described in docs/SPEC.md", "follow the README"), make the model go through that document requirement by requirement.
12
+ * Models often pass the visible tests and miss the edge cases a spec lists. It asks once, so it adds one round trip, not a loop.
13
+ */
14
+ export declare function specCheck(): Plugin;
15
+ export declare const STRATEGIES: Record<string, () => Plugin>;
16
+ export declare const strategyNames: () => string[];
@@ -0,0 +1,35 @@
1
+ // Loop strategies, written as plugins on the PreModel event: they change *when* the model may use which tools, without replacing the loop (so the budget, permissions, ledger and
2
+ // tracing all keep working). They are also examples of what you can build yourself: copy one and change it. `amness eval --variants full,strategy:plan-first` measures one against the default.
3
+ import { existsSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ const READ_TOOLS = ["list_files", "read_file", "grep", "repo_map", "find_refs", "impact", "task", "memory_search", "web_search", "fetch_url", "todo_write", "tool_search", "skill", "finish"];
6
+ /** Write a short plan before touching any tool, then work as usual. Costs one extra model call per request. */
7
+ export function planFirst(o = {}) {
8
+ return { name: "strategy-plan-first", description: "plan in text first, then act", hooks: {
9
+ PreModel: ({ step }) => (step === 0 ? { context: o.note ?? "Before you use any tool, write a short numbered plan (3 to 7 steps): what you will look at, what you will change, and how you will check it. Do not call tools in this reply.", tools: [], then: "Good. Now carry out the plan with the tools. Adjust it if you find something unexpected." } : undefined),
10
+ } };
11
+ }
12
+ /** Two phases: look around with read-only tools for a few steps, then act. Stops the habit of editing before understanding. */
13
+ export function exploreThenAct(o = {}) {
14
+ const n = Math.max(1, o.readSteps ?? 6);
15
+ return { name: "strategy-explore-then-act", description: `read-only for the first ${n} steps, then edit`, hooks: {
16
+ PreModel: ({ step }) => (step === 0 ? { context: `Work in two phases. Phase 1 (the next ${n} steps): you may only read and search; edits and shell commands are refused. Phase 2: you may change things. Use phase 1 to find where the change belongs.`, tools: READ_TOOLS }
17
+ : step < n ? { tools: READ_TOOLS }
18
+ : step === n ? { context: "Phase 2 starts now: you may edit files and run commands. Make the smallest correct change and verify it." } : undefined),
19
+ } };
20
+ }
21
+ /**
22
+ * Before finishing a task that points at a document ("as described in docs/SPEC.md", "follow the README"), make the model go through that document requirement by requirement.
23
+ * Models often pass the visible tests and miss the edge cases a spec lists. It asks once, so it adds one round trip, not a loop.
24
+ */
25
+ export function specCheck() {
26
+ return { name: "strategy-spec-check", description: "check the work against the document the task points to", gates: [{ name: "spec-check", maxRejections: 1, check: ({ task, cwd }) => {
27
+ const named = [...new Set(task.match(/[\w./-]+\.(?:md|txt|rst|ya?ml|json|adoc)\b/gi) ?? [])].filter((f) => existsSync(resolve(cwd, f)));
28
+ if (!named.length && !/\b(spec|specification|requirements?|as described|according to)\b/i.test(task))
29
+ return true;
30
+ const where = named.length ? named.join(", ") : "the document or requirements the task refers to";
31
+ return `Before you finish, open ${where} again and write its requirements as a numbered list (including edge cases, error cases and anything that says "must" or "always"). For each one say how you checked it: a test, or running the code. Fix whatever is not covered, then finish again.`;
32
+ } }] };
33
+ }
34
+ export const STRATEGIES = { "plan-first": () => planFirst(), "explore-then-act": () => exploreThenAct(), "spec-check": () => specCheck() };
35
+ export const strategyNames = () => Object.keys(STRATEGIES);
@@ -0,0 +1,30 @@
1
+ import type { Case } from "./evals.ts";
2
+ export type SuiteInstance = {
3
+ id: string;
4
+ repo: string;
5
+ baseCommit: string;
6
+ problem: string;
7
+ testPatch?: string;
8
+ goldPatch?: string;
9
+ failToPass: string[];
10
+ passToPass: string[];
11
+ installCmd?: string;
12
+ };
13
+ /** Read a JSONL file in SWE-bench's field names (instance_id, repo, base_commit, problem_statement, test_patch, patch, FAIL_TO_PASS, PASS_TO_PASS) or in short names (id, ...). */
14
+ export declare function loadSuite(file: string): SuiteInstance[];
15
+ /** Turn instances into cases. `testCmd` is a template: `{tests}` becomes the instance's FAIL_TO_PASS (and PASS_TO_PASS when `regression`) test ids joined by spaces. */
16
+ export declare function suiteToCases(instances: SuiteInstance[], o: {
17
+ testCmd: string;
18
+ installCmd?: string;
19
+ regression?: boolean;
20
+ limit?: number;
21
+ }): Case[];
22
+ export type Leak = {
23
+ id: string;
24
+ problem: string;
25
+ };
26
+ /**
27
+ * Contamination checks that need no model. For every instance: (1) lines the gold patch ADDS must not appear in the problem statement, (2) they must not already exist in the
28
+ * base checkout (a fix that is already there makes the task trivial or broken), (3) the base commit must exist.
29
+ */
30
+ export declare function leakAudit(instances: SuiteInstance[]): Promise<Leak[]>;
@@ -0,0 +1,112 @@
1
+ // Public-benchmark style suites (SWE-bench format) as eval cases: clone a repository, check out the base commit, let the agent work, then apply the hidden tests and run them.
2
+ // Each instance prepares itself in setup(); one that cannot be prepared is EXCLUDED and listed in the report's ledger instead of silently counting as a failure.
3
+ import { execFile } from "node:child_process";
4
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { runShell } from "./code.js";
8
+ const sh = (cwd, file, args, timeout = 300_000) => new Promise((res, rej) => execFile(file, args, { cwd, timeout, maxBuffer: 64 << 20 }, (e, out, err) => (e ? rej(new Error(`${file} ${args.slice(0, 3).join(" ")}: ${(err || e.message).trim().split("\n")[0]}`)) : res(out))));
9
+ const list = (v) => { if (Array.isArray(v))
10
+ return v.map(String); if (typeof v === "string") {
11
+ try {
12
+ const j = JSON.parse(v);
13
+ if (Array.isArray(j))
14
+ return j.map(String);
15
+ }
16
+ catch { /* a plain string */ }
17
+ return v ? [v] : [];
18
+ } return []; };
19
+ /** Read a JSONL file in SWE-bench's field names (instance_id, repo, base_commit, problem_statement, test_patch, patch, FAIL_TO_PASS, PASS_TO_PASS) or in short names (id, ...). */
20
+ export function loadSuite(file) {
21
+ const out = [];
22
+ for (const [i, l] of readFileSync(file, "utf8").split("\n").entries()) {
23
+ if (!l.trim())
24
+ continue;
25
+ let j;
26
+ try {
27
+ j = JSON.parse(l);
28
+ }
29
+ catch {
30
+ throw new Error(`${file}:${i + 1} is not valid JSON`);
31
+ }
32
+ const id = String(j.instance_id ?? j.id ?? ""), repo = String(j.repo ?? ""), base = String(j.base_commit ?? j.baseCommit ?? ""), problem = String(j.problem_statement ?? j.problem ?? "");
33
+ if (!id || !repo || !base || !problem)
34
+ throw new Error(`${file}:${i + 1}: each instance needs instance_id, repo, base_commit and problem_statement`);
35
+ out.push({ id, repo: /^[\w.-]+\/[\w.-]+$/.test(repo) && !repo.startsWith(".") ? `https://github.com/${repo}.git` : repo, baseCommit: base, problem, testPatch: (j.test_patch ?? j.testPatch), goldPatch: (j.patch ?? j.gold_patch), failToPass: list(j.FAIL_TO_PASS ?? j.fail_to_pass), passToPass: list(j.PASS_TO_PASS ?? j.pass_to_pass), installCmd: j.install_cmd });
36
+ }
37
+ return out;
38
+ }
39
+ /** Turn instances into cases. `testCmd` is a template: `{tests}` becomes the instance's FAIL_TO_PASS (and PASS_TO_PASS when `regression`) test ids joined by spaces. */
40
+ export function suiteToCases(instances, o) {
41
+ return instances.slice(0, o.limit ?? instances.length).map((inst) => {
42
+ const tests = [...inst.failToPass, ...(o.regression ? inst.passToPass : [])].join(" "), cmd = o.testCmd.replace("{tests}", tests);
43
+ return {
44
+ name: inst.id, files: {}, task: `${inst.problem.trim()}\n\nFix the problem in this repository. Do not modify existing tests.`,
45
+ setup: async (dir) => {
46
+ await sh(dir, "git", ["clone", "--quiet", inst.repo, "."]);
47
+ await sh(dir, "git", ["checkout", "--quiet", inst.baseCommit]);
48
+ const install = inst.installCmd ?? o.installCmd;
49
+ if (install) {
50
+ const r = await runShell(install, dir, 600_000);
51
+ if (!r.startsWith("exit=0"))
52
+ throw new Error(`install failed: ${r.split("\n")[1] ?? r.slice(0, 100)}`);
53
+ }
54
+ },
55
+ // The hidden tests are applied only now, after the agent is done, from a file outside the workspace.
56
+ check: async (dir) => {
57
+ if (inst.testPatch) {
58
+ const tmp = mkdtempSync(join(tmpdir(), "amness-tp-")), f = join(tmp, "test.patch");
59
+ writeFileSync(f, inst.testPatch.endsWith("\n") ? inst.testPatch : inst.testPatch + "\n");
60
+ try {
61
+ await sh(dir, "git", ["apply", "--whitespace=nowarn", f]);
62
+ }
63
+ catch (e) {
64
+ return `could not apply the hidden tests (${e.message})`;
65
+ }
66
+ finally {
67
+ rmSync(tmp, { recursive: true, force: true });
68
+ }
69
+ }
70
+ return (await runShell(cmd, dir, 900_000)).startsWith("exit=0") || "the hidden tests still fail";
71
+ },
72
+ };
73
+ });
74
+ }
75
+ /**
76
+ * Contamination checks that need no model. For every instance: (1) lines the gold patch ADDS must not appear in the problem statement, (2) they must not already exist in the
77
+ * base checkout (a fix that is already there makes the task trivial or broken), (3) the base commit must exist.
78
+ */
79
+ export async function leakAudit(instances) {
80
+ const out = [];
81
+ for (const inst of instances) {
82
+ const dir = mkdtempSync(join(tmpdir(), "amness-audit-"));
83
+ try {
84
+ try {
85
+ await sh(dir, "git", ["clone", "--quiet", "--no-checkout", inst.repo, "."]);
86
+ await sh(dir, "git", ["checkout", "--quiet", inst.baseCommit]);
87
+ }
88
+ catch (e) {
89
+ out.push({ id: inst.id, problem: `cannot check out the base commit: ${e.message}` });
90
+ continue;
91
+ }
92
+ const added = [...new Set((inst.goldPatch ?? "").split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1).trim()).filter((l) => l.length >= 25))];
93
+ const inProblem = added.filter((l) => inst.problem.includes(l));
94
+ if (inProblem.length)
95
+ out.push({ id: inst.id, problem: `the problem statement contains ${inProblem.length} line(s) of the fix, e.g. "${inProblem[0].slice(0, 60)}"` });
96
+ let present = 0;
97
+ for (const l of added.slice(0, 40)) {
98
+ try {
99
+ if ((await sh(dir, "git", ["grep", "-F", "-q", "--", l]).then(() => true, () => false)))
100
+ present++;
101
+ }
102
+ catch { /* ignore */ }
103
+ }
104
+ if (added.length >= 3 && present / Math.min(added.length, 40) > 0.6)
105
+ out.push({ id: inst.id, problem: `${present} of the ${Math.min(added.length, 40)} lines the fix adds already exist in the base checkout (the fix may already be applied)` });
106
+ }
107
+ finally {
108
+ rmSync(dir, { recursive: true, force: true });
109
+ }
110
+ }
111
+ return out;
112
+ }
@@ -0,0 +1,63 @@
1
+ import type { Event } from "./harness.ts";
2
+ export type TraceLine = {
3
+ t: string;
4
+ ms: number;
5
+ kind: string;
6
+ [k: string]: unknown;
7
+ };
8
+ export type TracerOptions = {
9
+ sink?: (line: TraceLine) => void;
10
+ file?: string;
11
+ otlp?: string;
12
+ sessionId: string;
13
+ model?: string; /** extra resource attributes */
14
+ attrs?: Record<string, string>;
15
+ };
16
+ export declare class Tracer {
17
+ readonly file?: string;
18
+ private sink?;
19
+ private otlp?;
20
+ private id;
21
+ private model?;
22
+ private attrs;
23
+ private t0;
24
+ private traceId;
25
+ private rootId;
26
+ private spans;
27
+ private open;
28
+ private turnStart;
29
+ private textChars;
30
+ private pending;
31
+ /** Rolling data for the cache-waste figure. */
32
+ turns: {
33
+ prompt: number;
34
+ cached: number;
35
+ break?: boolean;
36
+ }[];
37
+ constructor(o: TracerOptions);
38
+ private agent?;
39
+ private write;
40
+ /** Record the messages the model produced (needed for replay). */
41
+ message(m: {
42
+ role: string;
43
+ content: string | null;
44
+ tool_calls?: unknown;
45
+ }): void;
46
+ event(e: Event & {
47
+ agent?: string;
48
+ }): void;
49
+ private add;
50
+ /** OTLP/HTTP JSON body for the spans collected so far. */
51
+ payload(): object;
52
+ flush(): void;
53
+ close(): Promise<void>;
54
+ }
55
+ /**
56
+ * Prompt tokens that were re-read at full price although the prefix should have been cached: for each turn, the uncached part beyond what was newly added since the
57
+ * previous turn. A fresh start (first turn) and the turns right after a deliberate rewrite are not waste, so only turns where the cache really should have hit count.
58
+ */
59
+ export declare function cacheWaste(turns: {
60
+ prompt: number;
61
+ cached: number;
62
+ break?: boolean;
63
+ }[]): number;
@@ -0,0 +1,153 @@
1
+ // Tracing: every loop event goes to a local JSONL file (always, cheap, greppable) and optionally to an OpenTelemetry collector (OTLP/HTTP JSON, plain fetch).
2
+ // The JSONL file is also what `replay` and failure mining read, so it keeps what is needed to re-run the harness logic: the model's messages and tool results.
3
+ import { appendFileSync, mkdirSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ import { randomBytes } from "node:crypto";
6
+ import { redactDeep } from "./secrets.js";
7
+ const clip = (s, n) => { const t = typeof s === "string" ? s : JSON.stringify(s) ?? ""; return t.length > n ? t.slice(0, n) + `…[${t.length - n} more]` : t; };
8
+ const hex = (n) => randomBytes(n).toString("hex");
9
+ const ns = (ms) => String(Math.round(ms * 1e6));
10
+ const attrVal = (v) => (typeof v === "string" ? { stringValue: v } : typeof v === "boolean" ? { boolValue: v } : Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v });
11
+ export class Tracer {
12
+ file;
13
+ sink;
14
+ otlp;
15
+ id;
16
+ model;
17
+ attrs;
18
+ t0 = Date.now();
19
+ traceId = hex(16);
20
+ rootId = hex(8);
21
+ spans = [];
22
+ open = new Map();
23
+ turnStart = 0;
24
+ textChars = 0;
25
+ pending = [];
26
+ /** Rolling data for the cache-waste figure. */
27
+ turns = [];
28
+ constructor(o) { this.file = o.file; this.sink = o.sink; this.otlp = o.otlp; this.id = o.sessionId; this.model = o.model; this.attrs = o.attrs ?? {}; if (o.file) {
29
+ try {
30
+ mkdirSync(dirname(o.file), { recursive: true });
31
+ }
32
+ catch { /* writes below will fail quietly */ }
33
+ } }
34
+ agent;
35
+ write(kind, rest) {
36
+ if (!this.file && !this.sink)
37
+ return;
38
+ const line = { t: new Date().toISOString(), ms: Date.now() - this.t0, kind, ...(this.agent && { agent: this.agent }), ...redactDeep(rest) }; // secrets never reach the trace file
39
+ this.sink?.(line);
40
+ if (!this.file)
41
+ return;
42
+ try {
43
+ appendFileSync(this.file, JSON.stringify(line) + "\n");
44
+ }
45
+ catch { /* a full disk must not stop the agent */ }
46
+ }
47
+ /** Record the messages the model produced (needed for replay). */
48
+ message(m) { this.write("message", { role: m.role, content: clip(m.content ?? "", 20_000), tool_calls: m.tool_calls }); }
49
+ event(e) {
50
+ const now = Date.now();
51
+ this.agent = e.agent && e.agent !== "lead" ? e.agent : undefined;
52
+ switch (e.kind) {
53
+ case "text":
54
+ case "thinking":
55
+ this.textChars += e.delta.length;
56
+ return; // too chatty; the finished message is recorded instead
57
+ case "run":
58
+ this.write("run", { messages: e.messages.map((m) => ({ ...m, content: clip(m.content ?? "", 60_000) })) });
59
+ return;
60
+ case "model":
61
+ this.write("model", { message: { ...e.message, content: clip(e.message.content ?? "", 60_000) } });
62
+ return;
63
+ case "assistant":
64
+ this.write("assistant", { chars: e.text.length, streamed: e.streamed });
65
+ this.textChars = 0;
66
+ return;
67
+ case "phase":
68
+ if (e.phase === "model")
69
+ this.turnStart = now;
70
+ this.write("phase", { phase: e.phase, step: e.step, n: e.n });
71
+ return;
72
+ case "turn": {
73
+ this.turns.push({ prompt: e.prompt, cached: e.cached });
74
+ this.write("turn", { step: e.step, prompt: e.prompt, completion: e.completion, cached: e.cached, reasoning: e.reasoning, ms: now - (this.turnStart || now) });
75
+ this.add({ name: `chat ${this.model ?? ""}`.trim(), start: this.turnStart || now, end: now, attrs: { "gen_ai.operation.name": "chat", ...(this.model && { "gen_ai.request.model": this.model }), "gen_ai.usage.input_tokens": e.prompt, "gen_ai.usage.output_tokens": e.completion, "gen_ai.usage.cache_read.input_tokens": e.cached, "amness.step": e.step } });
76
+ return;
77
+ }
78
+ case "toolstart": {
79
+ const s = { name: `execute_tool ${e.name}`, spanId: hex(8), start: now, end: now, attrs: { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": e.name } };
80
+ this.open.set(e.id, s);
81
+ this.write("toolstart", { id: e.id, name: e.name, args: clip(e.args, 20_000) });
82
+ return;
83
+ }
84
+ case "tool": {
85
+ const s = this.open.get(e.id ?? "");
86
+ this.open.delete(e.id ?? "");
87
+ const failed = /^(ERROR|DENIED|REJECTED|BLOCKED)/.test(e.result);
88
+ this.write("tool", { id: e.id, name: e.name, ms: s ? now - s.start : undefined, ok: !failed, result: clip(e.result, 100_000) });
89
+ if (s) {
90
+ s.end = now;
91
+ s.attrs["amness.tool.ok"] = !failed;
92
+ if (failed)
93
+ s.error = clip(e.result, 200);
94
+ this.add(s);
95
+ }
96
+ return;
97
+ }
98
+ case "cachebreak": {
99
+ const last = this.turns.at(-1);
100
+ if (last)
101
+ last.break = true;
102
+ this.write("cachebreak", { reason: e.reason, at: e.at });
103
+ return;
104
+ }
105
+ case "hook":
106
+ this.write("hook", { event: e.event, detail: clip(e.detail, 500), blocked: e.blocked });
107
+ this.add({ name: `hook ${e.event}`, start: now, end: now, attrs: { "amness.hook.event": e.event, "amness.hook.blocked": e.blocked } });
108
+ return;
109
+ case "edit":
110
+ this.write("edit", { path: e.path, created: e.created, added: e.added, removed: e.removed });
111
+ return;
112
+ default: {
113
+ const d = e;
114
+ this.write(e.kind, { detail: clip(d.detail ?? d.summary ?? d.text ?? (d.saved !== undefined ? `${d.saved} chars: ${d.what}` : ""), 1000) });
115
+ }
116
+ }
117
+ }
118
+ add(s) {
119
+ if (!this.otlp)
120
+ return;
121
+ this.spans.push({ ...s, spanId: s.spanId ?? hex(8), parent: this.rootId });
122
+ if (this.spans.length >= 50)
123
+ this.flush();
124
+ }
125
+ /** OTLP/HTTP JSON body for the spans collected so far. */
126
+ payload() {
127
+ const root = { name: "amness session", spanId: this.rootId, start: this.t0, end: Date.now(), attrs: { "amness.session.id": this.id } };
128
+ const all = [root, ...this.spans];
129
+ return { resourceSpans: [{ resource: { attributes: [{ key: "service.name", value: { stringValue: "amness" } }, ...Object.entries(this.attrs).map(([k, v]) => ({ key: k, value: { stringValue: v } }))] },
130
+ scopeSpans: [{ scope: { name: "amness" }, spans: all.map((s) => ({ traceId: this.traceId, spanId: s.spanId, ...(s.parent && { parentSpanId: s.parent }), name: s.name, kind: 1, startTimeUnixNano: ns(s.start), endTimeUnixNano: ns(Math.max(s.end, s.start)),
131
+ attributes: Object.entries(s.attrs).map(([key, v]) => ({ key, value: attrVal(v) })), ...(s.error && { status: { code: 2, message: s.error } }) })) }] }] };
132
+ }
133
+ flush() {
134
+ if (!this.otlp || !this.spans.length)
135
+ return;
136
+ const body = JSON.stringify(this.payload());
137
+ this.spans = [];
138
+ this.pending.push(fetch(this.otlp.replace(/\/$/, "").replace(/\/v1\/traces$/, "") + "/v1/traces", { method: "POST", headers: { "content-type": "application/json" }, body }).catch(() => { }));
139
+ }
140
+ async close() { this.write("end", {}); this.flush(); await Promise.allSettled(this.pending); }
141
+ }
142
+ /**
143
+ * Prompt tokens that were re-read at full price although the prefix should have been cached: for each turn, the uncached part beyond what was newly added since the
144
+ * previous turn. A fresh start (first turn) and the turns right after a deliberate rewrite are not waste, so only turns where the cache really should have hit count.
145
+ */
146
+ export function cacheWaste(turns) {
147
+ let waste = 0;
148
+ for (let i = 1; i < turns.length; i++) {
149
+ const grew = Math.max(0, turns[i].prompt - turns[i - 1].prompt), uncached = Math.max(0, turns[i].prompt - turns[i].cached);
150
+ waste += Math.max(0, uncached - grew);
151
+ }
152
+ return waste;
153
+ }
@@ -0,0 +1,38 @@
1
+ import { type Case, type Row, type Variant } from "./evals.ts";
2
+ import type { Profile } from "./profiles.ts";
3
+ import type { Guard } from "./harness.ts";
4
+ export type Step = {
5
+ change: string;
6
+ stage: "train" | "heldout";
7
+ accepted: boolean;
8
+ why: string;
9
+ };
10
+ export type TuneResult = {
11
+ profile: Profile;
12
+ accepted: string[];
13
+ steps: Step[];
14
+ train: string[];
15
+ heldOut: string[];
16
+ strength: string;
17
+ };
18
+ export type TuneOptions = {
19
+ cases: Case[];
20
+ /** Build a runnable variant for a profile. `amness tune` uses the real code agent; tests use a scripted one. */
21
+ makeVariant: (profile: Profile, name: string) => Variant;
22
+ base?: Profile;
23
+ trials?: number;
24
+ jobs?: number;
25
+ seed?: number;
26
+ heldOutFraction?: number;
27
+ guards?: Guard[];
28
+ stagnationTurns?: number[];
29
+ onStep?: (s: Step) => void;
30
+ onRow?: (r: Row) => void;
31
+ };
32
+ /** A deterministic split of tasks into a training set and a held-out set (at least one task in each when there are two or more). */
33
+ export declare function split(cases: Case[], fraction?: number, seed?: number): {
34
+ train: Case[];
35
+ heldOut: Case[];
36
+ };
37
+ export declare function tune(o: TuneOptions): Promise<TuneResult>;
38
+ export declare function writeProfile(file: string, model: string, p: Profile): void;
@@ -0,0 +1,49 @@
1
+ // `amness tune`: bounded self-improvement of the harness's own settings. The search space is small and fixed (switch one guard off, change a limit), never free-form edits.
2
+ // A change is only kept if it holds up twice: on the training tasks, and again on tasks the search never saw. Everything else is rolled back; the profile file is written only on success.
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+ import { analyze, evaluate } from "./evals.js";
6
+ import { rng } from "./stats.js";
7
+ const DEFAULT_GUARDS = ["nudge", "loop", "stuck", "wrapup", "condense", "stagnation"];
8
+ /** A deterministic split of tasks into a training set and a held-out set (at least one task in each when there are two or more). */
9
+ export function split(cases, fraction = 0.34, seed = 1) {
10
+ const r = rng(seed), order = cases.map((_, i) => [r(), i]).sort((a, b) => a[0] - b[0]).map(([, i]) => i);
11
+ const n = cases.length < 2 ? 0 : Math.max(1, Math.round(cases.length * fraction)), held = new Set(order.slice(0, n));
12
+ return { train: cases.filter((_, i) => !held.has(i)), heldOut: cases.filter((_, i) => held.has(i)) };
13
+ }
14
+ const withChange = (p, c) => ({ ...p, ...(c.guard && { off: [...new Set([...(p.off ?? []), c.guard])] }), ...(c.stagnation !== undefined && { stagnationTurns: c.stagnation }) });
15
+ /** Is `cand` at least as good as `cur` on these cases? Never worse on pass rate, and either better or cheaper per solved task. */
16
+ async function compare(cases, cur, cand, o) {
17
+ const { rows } = await evaluate(cases, [o.makeVariant(cur, "current"), o.makeVariant(cand, "candidate")], { trials: o.trials ?? 2, jobs: o.jobs, baseline: "current", seed: o.seed, onRow: o.onRow });
18
+ const a = analyze(rows, { baseline: "current", seed: o.seed }), c = a.comparisons.find((x) => x.variant === "candidate");
19
+ const cheaper = Number.isFinite(c.dTokensPerSolved) && c.dTokensPerSolved < 0;
20
+ const ok = c.verdict !== "worse" && c.dPass >= -0.02 && (c.verdict === "better" || c.dPass > 0 || cheaper);
21
+ return { ok, why: `pass ${c.dPass >= 0 ? "+" : ""}${(c.dPass * 100).toFixed(0)} pts (CI ${(c.ci.lo * 100).toFixed(0)} to ${(c.ci.hi * 100).toFixed(0)}), tokens per solved task ${Number.isFinite(c.dTokensPerSolved) ? Math.round(c.dTokensPerSolved).toLocaleString() : "n/a"}`, minDetectable: a.minDetectable, tasks: a.tasks };
22
+ }
23
+ export async function tune(o) {
24
+ const { train, heldOut } = split(o.cases, o.heldOutFraction, o.seed), steps = [], accepted = [];
25
+ const note = (s) => { steps.push(s); o.onStep?.(s); };
26
+ const startingPoint = { ...(o.base ?? {}) };
27
+ let cur = startingPoint, strength = "";
28
+ const candidates = [...(o.guards ?? DEFAULT_GUARDS).filter((g) => !(cur.off ?? []).includes(g)).map((g) => ({ label: `switch off ${g}`, c: { guard: g } })), ...(o.stagnationTurns ?? []).map((n) => ({ label: `stagnationTurns = ${n}`, c: { stagnation: n } }))];
29
+ for (const { label, c } of candidates) {
30
+ const cand = withChange(cur, c), t = await compare(train, cur, cand, o);
31
+ strength = `${t.tasks} training task(s) can only detect differences above ~${(t.minDetectable * 100).toFixed(0)} points`;
32
+ note({ change: label, stage: "train", accepted: t.ok, why: t.why });
33
+ if (t.ok) {
34
+ cur = cand;
35
+ accepted.push(label);
36
+ }
37
+ }
38
+ // The held-out gate: all accepted changes together must not hurt on tasks the search never looked at; otherwise everything is rolled back.
39
+ if (accepted.length) {
40
+ const h = heldOut.length ? await compare(heldOut, startingPoint, cur, o) : { ok: false, why: "no held-out tasks (need at least 2 tasks); nothing is kept without that check" };
41
+ note({ change: accepted.join(" + "), stage: "heldout", accepted: h.ok, why: h.why });
42
+ if (!h.ok) {
43
+ cur = startingPoint;
44
+ accepted.length = 0;
45
+ }
46
+ }
47
+ return { profile: cur, accepted, steps, train: train.map((c) => c.name), heldOut: heldOut.map((c) => c.name), strength };
48
+ }
49
+ export function writeProfile(file, model, p) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify({ models: { [model]: { ...p, note: "tuned with a held-out check (amness tune)" } } }, null, 2) + "\n"); }
@@ -0,0 +1,19 @@
1
+ export type SearchHit = {
2
+ title: string;
3
+ url: string;
4
+ snippet: string;
5
+ };
6
+ export type SearchProvider = (query: string, n: number) => Promise<SearchHit[]>;
7
+ /** Tavily (needs TAVILY_API_KEY): returns clean snippets built for agents. */
8
+ export declare const tavily: (key?: string) => SearchProvider;
9
+ /** Keyless fallback: scrapes DuckDuckGo's HTML endpoint. Fragile by nature; prefer a real provider in production. */
10
+ export declare const duckduckgo: SearchProvider;
11
+ /** Fetch a URL and reduce it to readable text. Refuses non-http(s) and local/private hosts. */
12
+ export declare function fetchText(url: string, max?: number, offset?: number): Promise<string>;
13
+ export declare const webTools: (search?: SearchProvider) => (import("./harness.ts").Tool<{
14
+ query: string;
15
+ n?: number;
16
+ }> | import("./harness.ts").Tool<{
17
+ url: string;
18
+ offset?: number;
19
+ }>)[];