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,65 @@
|
|
|
1
|
+
import { type Tool } from "./harness.ts";
|
|
2
|
+
export type Sym = {
|
|
3
|
+
name: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
line: number;
|
|
6
|
+
sig: string;
|
|
7
|
+
};
|
|
8
|
+
export type FileInfo = {
|
|
9
|
+
path: string;
|
|
10
|
+
lang: string;
|
|
11
|
+
lines: number;
|
|
12
|
+
mtime: number;
|
|
13
|
+
size: number;
|
|
14
|
+
symbols: Sym[];
|
|
15
|
+
imports: string[];
|
|
16
|
+
ids: Map<string, number>;
|
|
17
|
+
};
|
|
18
|
+
export declare function indexFile(path: string, text: string, mtime?: number): FileInfo;
|
|
19
|
+
export declare class CodeIndex {
|
|
20
|
+
files: Map<string, FileInfo>;
|
|
21
|
+
private root;
|
|
22
|
+
private built;
|
|
23
|
+
private edges;
|
|
24
|
+
private rev;
|
|
25
|
+
private symDefs;
|
|
26
|
+
private cap;
|
|
27
|
+
constructor(root: string, o?: {
|
|
28
|
+
maxFiles?: number;
|
|
29
|
+
});
|
|
30
|
+
/** Source files currently under the root (fast: names and sizes only). */
|
|
31
|
+
list(): string[];
|
|
32
|
+
/** Bring the index up to date: re-read only files whose modification time or size changed. */
|
|
33
|
+
refresh(): {
|
|
34
|
+
files: number;
|
|
35
|
+
changed: number;
|
|
36
|
+
};
|
|
37
|
+
private resolveImport;
|
|
38
|
+
private link;
|
|
39
|
+
importedBy(path: string): string[];
|
|
40
|
+
imports(path: string): string[];
|
|
41
|
+
/** Files that depend on `path`, directly or through up to `depth` hops. */
|
|
42
|
+
dependents(path: string, depth?: number): {
|
|
43
|
+
file: string;
|
|
44
|
+
hops: number;
|
|
45
|
+
}[];
|
|
46
|
+
definitions(name: string): {
|
|
47
|
+
file: string;
|
|
48
|
+
sym: Sym;
|
|
49
|
+
}[];
|
|
50
|
+
/** Files that mention `name` as an identifier (excluding the ones that define it), with counts. */
|
|
51
|
+
users(name: string): {
|
|
52
|
+
file: string;
|
|
53
|
+
count: number;
|
|
54
|
+
}[];
|
|
55
|
+
/** Rank files for `query` (identifier overlap with paths and symbols, weighted up for definitions) with a small boost for files many others import. Without a query: the most central files. */
|
|
56
|
+
rank(query?: string, limit?: number): {
|
|
57
|
+
file: FileInfo;
|
|
58
|
+
score: number;
|
|
59
|
+
why: string[];
|
|
60
|
+
}[];
|
|
61
|
+
/** A compact map: ranked files with their main symbols, within a character budget. */
|
|
62
|
+
map(query?: string, maxChars?: number, limit?: number): string;
|
|
63
|
+
}
|
|
64
|
+
/** Tools: `repo_map`, `find_refs`, `impact`. Read-only. */
|
|
65
|
+
export declare function indexTools(index: CodeIndex): Tool[];
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// A structural index of the repository, built from plain files with no dependencies: symbols (functions, classes, types...), the import graph between files, and
|
|
2
|
+
// where each symbol is used. It answers "where does this live and what depends on it" in one call instead of a dozen greps (the idea behind Aider's repo map and
|
|
3
|
+
// the structural indexes in recent coding-agent work), and it is rebuilt incrementally from file modification times.
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { defineTool } from "./harness.js";
|
|
7
|
+
const IGNORE = new Set(["node_modules", ".git", "dist", "build", "out", "target", ".amness", "__pycache__", ".venv", "venv", ".next", ".cache", "coverage", "vendor", ".gradle", ".idea", ".vscode"]);
|
|
8
|
+
const LANG = { ".ts": "ts", ".tsx": "ts", ".mts": "ts", ".cts": "ts", ".js": "js", ".jsx": "js", ".mjs": "js", ".cjs": "js", ".py": "py", ".go": "go", ".rs": "rs", ".java": "java", ".kt": "kt", ".cs": "cs", ".rb": "rb", ".php": "php", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp", ".swift": "swift", ".scala": "scala" };
|
|
9
|
+
const STOP = new Set("if else for while return new this self true false null undefined none nil let const var def func fn function class struct enum type interface public private protected static final async await import from export default package use mod pub void int string bool boolean float double char long short byte then end do case switch break continue try catch finally throw throws raise with as in is not and or the".split(" "));
|
|
10
|
+
const RULES = {
|
|
11
|
+
ts: [[/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)/, "function"], [/^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(\w+)/, "class"], [/^\s*(?:export\s+)?interface\s+(\w+)/, "interface"], [/^\s*(?:export\s+)?type\s+(\w+)\s*[=<]/, "type"], [/^\s*(?:export\s+)?(?:const\s+)?enum\s+(\w+)/, "enum"],
|
|
12
|
+
[/^\s*(?:export\s+)?const\s+(\w+)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|\w+)\s*(?::[^=]+)?=>/, "function"], [/^\s{2,}(?:public\s+|private\s+|protected\s+|static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\([^)]*\)\s*(?::\s*[^{;]+)?\s*\{/, "method"]],
|
|
13
|
+
py: [[/^\s*(?:async\s+)?def\s+(\w+)/, "function"], [/^\s*class\s+(\w+)/, "class"]],
|
|
14
|
+
go: [[/^func\s+(?:\([^)]*\)\s*)?(\w+)/, "func"], [/^type\s+(\w+)\s+(?:struct|interface)/, "type"]],
|
|
15
|
+
rs: [[/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+(\w+)/, "fn"], [/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait)\s+(\w+)/, "type"], [/^\s*impl(?:<[^>]*>)?\s+(?:\w+\s+for\s+)?(\w+)/, "impl"]],
|
|
16
|
+
java: [[/^\s*(?:public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+)*(?:class|interface|enum|record)\s+(\w+)/, "class"], [/^\s{2,}(?:public\s+|private\s+|protected\s+|static\s+|final\s+|synchronized\s+)+[\w<>\[\],.? ]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [\w, .]+)?\s*\{/, "method"]],
|
|
17
|
+
rb: [[/^\s*def\s+(?:self\.)?(\w+[?!]?)/, "def"], [/^\s*(?:class|module)\s+(\w+)/, "class"]],
|
|
18
|
+
php: [[/^\s*(?:public\s+|private\s+|protected\s+|static\s+)*function\s+(\w+)/, "function"], [/^\s*(?:abstract\s+|final\s+)?(?:class|interface|trait)\s+(\w+)/, "class"]],
|
|
19
|
+
c: [[/^[A-Za-z_][\w \t\*]+?\b(\w+)\s*\([^;{]*\)\s*\{/, "function"], [/^\s*(?:typedef\s+)?(?:struct|enum|union)\s+(\w+)/, "type"]],
|
|
20
|
+
};
|
|
21
|
+
RULES.js = RULES.ts;
|
|
22
|
+
RULES.kt = [[/^\s*(?:private\s+|public\s+|internal\s+|override\s+|suspend\s+)*fun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?(\w+)/, "fun"], [/^\s*(?:data\s+|sealed\s+|abstract\s+|open\s+)*(?:class|interface|object)\s+(\w+)/, "class"]];
|
|
23
|
+
RULES.cs = RULES.java;
|
|
24
|
+
RULES.cpp = RULES.c;
|
|
25
|
+
RULES.swift = [[/^\s*(?:public\s+|private\s+|internal\s+|static\s+|override\s+)*func\s+(\w+)/, "func"], [/^\s*(?:public\s+|final\s+)*(?:class|struct|enum|protocol)\s+(\w+)/, "type"]];
|
|
26
|
+
RULES.scala = [[/^\s*def\s+(\w+)/, "def"], [/^\s*(?:case\s+)?(?:class|object|trait)\s+(\w+)/, "class"]];
|
|
27
|
+
const IMPORTS = {
|
|
28
|
+
ts: [/\bfrom\s+["']([^"']+)["']/g, /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g, /^\s*import\s+["']([^"']+)["']/gm, /\bexport\s+[^'"\n]*\bfrom\s+["']([^"']+)["']/g],
|
|
29
|
+
py: [/^\s*from\s+([.\w]+)\s+import\b/gm, /^\s*import\s+([\w.]+)/gm],
|
|
30
|
+
go: [/^\s*(?:import\s+)?(?:\w+\s+)?"([\w./-]+)"\s*$/gm], rs: [/^\s*(?:pub\s+)?(?:use|mod)\s+([\w:]+)/gm], java: [/^\s*import\s+(?:static\s+)?([\w.]+)\s*;/gm],
|
|
31
|
+
rb: [/^\s*require(?:_relative)?\s+["']([^"']+)["']/gm], php: [/^\s*(?:require|include)(?:_once)?\s*\(?\s*["']([^"']+)["']/gm], c: [/^\s*#\s*include\s+"([^"]+)"/gm],
|
|
32
|
+
};
|
|
33
|
+
IMPORTS.js = IMPORTS.ts;
|
|
34
|
+
IMPORTS.kt = IMPORTS.java;
|
|
35
|
+
IMPORTS.cs = [/^\s*using\s+([\w.]+)\s*;/gm];
|
|
36
|
+
IMPORTS.cpp = IMPORTS.c;
|
|
37
|
+
IMPORTS.swift = [/^\s*import\s+(\w+)/gm];
|
|
38
|
+
IMPORTS.scala = IMPORTS.java;
|
|
39
|
+
const words = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 1 && !STOP.has(w));
|
|
40
|
+
export function indexFile(path, text, mtime = 0) {
|
|
41
|
+
const lang = LANG[extname(path).toLowerCase()] ?? "", lines = text.split("\n"), symbols = [], rules = RULES[lang] ?? [];
|
|
42
|
+
const seenSym = new Set();
|
|
43
|
+
lines.forEach((l, i) => {
|
|
44
|
+
if (l.length > 300 || /^\s*(\/\/|#|\*|\/\*)/.test(l) && lang !== "py" && lang !== "rb" && !/^\s*(#\s*include)/.test(l))
|
|
45
|
+
return;
|
|
46
|
+
if ((lang === "py" || lang === "rb") && /^\s*#/.test(l))
|
|
47
|
+
return;
|
|
48
|
+
for (const [re, kind] of rules) {
|
|
49
|
+
const m = re.exec(l);
|
|
50
|
+
if (m && m[1] && !STOP.has(m[1]) && !seenSym.has(m[1] + ":" + kind)) {
|
|
51
|
+
seenSym.add(m[1] + ":" + kind);
|
|
52
|
+
symbols.push({ name: m[1], kind, line: i + 1, sig: l.trim().replace(/\s*[{:]\s*$/, "").slice(0, 110) });
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const imports = [];
|
|
58
|
+
for (const re of IMPORTS[lang] ?? [])
|
|
59
|
+
for (const m of text.matchAll(new RegExp(re.source, re.flags)))
|
|
60
|
+
imports.push(m[1]);
|
|
61
|
+
const ids = new Map();
|
|
62
|
+
for (const m of text.matchAll(/[A-Za-z_]\w{2,}/g)) {
|
|
63
|
+
const w = m[0];
|
|
64
|
+
if (!STOP.has(w))
|
|
65
|
+
ids.set(w, (ids.get(w) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
return { path, lang, lines: lines.length, mtime, size: text.length, symbols, imports: [...new Set(imports)], ids };
|
|
68
|
+
}
|
|
69
|
+
export class CodeIndex {
|
|
70
|
+
files = new Map();
|
|
71
|
+
root;
|
|
72
|
+
built = 0;
|
|
73
|
+
edges = new Map();
|
|
74
|
+
rev = new Map();
|
|
75
|
+
symDefs = new Map();
|
|
76
|
+
cap;
|
|
77
|
+
constructor(root, o = {}) { this.root = resolve(root); this.cap = o.maxFiles ?? 5000; }
|
|
78
|
+
/** Source files currently under the root (fast: names and sizes only). */
|
|
79
|
+
list() {
|
|
80
|
+
const out = [], walk = (d, depth) => {
|
|
81
|
+
let es = [];
|
|
82
|
+
try {
|
|
83
|
+
es = readdirSync(d).sort();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const e of es) {
|
|
89
|
+
if (out.length >= this.cap)
|
|
90
|
+
return;
|
|
91
|
+
if (IGNORE.has(e) || e.startsWith(".") && e !== ".github")
|
|
92
|
+
continue;
|
|
93
|
+
const p = join(d, e);
|
|
94
|
+
let st;
|
|
95
|
+
try {
|
|
96
|
+
st = statSync(p);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (st.isDirectory()) {
|
|
102
|
+
if (depth < 12)
|
|
103
|
+
walk(p, depth + 1);
|
|
104
|
+
}
|
|
105
|
+
else if (LANG[extname(e).toLowerCase()] && st.size <= 400_000)
|
|
106
|
+
out.push(relative(this.root, p).split(sep).join("/"));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(this.root, 0);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/** Bring the index up to date: re-read only files whose modification time or size changed. */
|
|
113
|
+
refresh() {
|
|
114
|
+
const now = this.list(), live = new Set(now);
|
|
115
|
+
let changed = 0;
|
|
116
|
+
for (const p of [...this.files.keys()])
|
|
117
|
+
if (!live.has(p)) {
|
|
118
|
+
this.files.delete(p);
|
|
119
|
+
changed++;
|
|
120
|
+
}
|
|
121
|
+
for (const p of now) {
|
|
122
|
+
let st;
|
|
123
|
+
try {
|
|
124
|
+
st = statSync(join(this.root, p));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const old = this.files.get(p);
|
|
130
|
+
if (old && old.mtime === st.mtimeMs && old.size === st.size)
|
|
131
|
+
continue;
|
|
132
|
+
try {
|
|
133
|
+
this.files.set(p, indexFile(p, readFileSync(join(this.root, p), "utf8"), st.mtimeMs));
|
|
134
|
+
changed++;
|
|
135
|
+
}
|
|
136
|
+
catch { /* unreadable */ }
|
|
137
|
+
}
|
|
138
|
+
if (changed || !this.built)
|
|
139
|
+
this.link();
|
|
140
|
+
this.built = Date.now();
|
|
141
|
+
return { files: this.files.size, changed };
|
|
142
|
+
}
|
|
143
|
+
resolveImport(from, spec) {
|
|
144
|
+
const known = (p) => (this.files.has(p) ? p : undefined), dir = dirname(from.path);
|
|
145
|
+
const tryPaths = (base) => known(base) ?? [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".rb", ".php", ".h", ".c", ".go", ".rs"].map((e) => known(base + e)).find(Boolean) ?? [".ts", ".js", ".py"].map((e) => known(join(base, "index" + e).split(sep).join("/")) ?? known(join(base, "__init__" + e).split(sep).join("/"))).find(Boolean);
|
|
146
|
+
const norm = (p) => p.split(sep).join("/").replace(/^\.\//, "");
|
|
147
|
+
if (from.lang === "ts" || from.lang === "js" || from.lang === "rb" || from.lang === "php" || from.lang === "c" || from.lang === "cpp") {
|
|
148
|
+
if (!spec.startsWith(".") && from.lang !== "c" && from.lang !== "cpp")
|
|
149
|
+
return undefined; // a package, not one of our files
|
|
150
|
+
const base = norm(join(dir, spec));
|
|
151
|
+
return tryPaths(base) ?? tryPaths(base.replace(/\.(m|c)?js$/, "")); // "./x.js" in TypeScript means x.ts
|
|
152
|
+
}
|
|
153
|
+
if (from.lang === "py") {
|
|
154
|
+
if (spec.startsWith(".")) {
|
|
155
|
+
const dots = /^\.+/.exec(spec)[0].length;
|
|
156
|
+
let d = dir;
|
|
157
|
+
for (let i = 1; i < dots; i++)
|
|
158
|
+
d = dirname(d);
|
|
159
|
+
return tryPaths(norm(join(d, spec.slice(dots).replace(/\./g, "/"))));
|
|
160
|
+
}
|
|
161
|
+
const rel = norm(spec.replace(/\./g, "/"));
|
|
162
|
+
const direct = tryPaths(rel) ?? tryPaths(norm(join(dir, rel)));
|
|
163
|
+
if (direct)
|
|
164
|
+
return direct;
|
|
165
|
+
const hits = [...this.files.keys()].filter((p) => p.endsWith("/" + rel + ".py") || p.endsWith("/" + rel + "/__init__.py")); // the package root is a subfolder (src/, py/...): match by path suffix when unique
|
|
166
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
167
|
+
}
|
|
168
|
+
if (from.lang === "go" || from.lang === "java" || from.lang === "kt" || from.lang === "scala" || from.lang === "cs" || from.lang === "rs") {
|
|
169
|
+
const last = spec.split(/[./:]+/).filter(Boolean).pop();
|
|
170
|
+
if (!last)
|
|
171
|
+
return undefined;
|
|
172
|
+
const hits = [...this.files.keys()].filter((p) => p !== from.path && (p.endsWith("/" + last + extname(p)) || p === last + extname(p)));
|
|
173
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
link() {
|
|
178
|
+
this.edges.clear();
|
|
179
|
+
this.rev.clear();
|
|
180
|
+
this.symDefs.clear();
|
|
181
|
+
for (const f of this.files.values()) {
|
|
182
|
+
for (const s of f.symbols) {
|
|
183
|
+
const l = this.symDefs.get(s.name) ?? [];
|
|
184
|
+
l.push({ file: f.path, sym: s });
|
|
185
|
+
this.symDefs.set(s.name, l);
|
|
186
|
+
}
|
|
187
|
+
const out = new Set();
|
|
188
|
+
for (const spec of f.imports) {
|
|
189
|
+
const t = this.resolveImport(f, spec);
|
|
190
|
+
if (t && t !== f.path)
|
|
191
|
+
out.add(t);
|
|
192
|
+
}
|
|
193
|
+
this.edges.set(f.path, out);
|
|
194
|
+
for (const t of out) {
|
|
195
|
+
const r = this.rev.get(t) ?? new Set();
|
|
196
|
+
r.add(f.path);
|
|
197
|
+
this.rev.set(t, r);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
importedBy(path) { return [...(this.rev.get(path) ?? [])].sort(); }
|
|
202
|
+
imports(path) { return [...(this.edges.get(path) ?? [])].sort(); }
|
|
203
|
+
/** Files that depend on `path`, directly or through up to `depth` hops. */
|
|
204
|
+
dependents(path, depth = 2) {
|
|
205
|
+
const seen = new Map([[path, 0]]);
|
|
206
|
+
let frontier = [path];
|
|
207
|
+
for (let h = 1; h <= depth; h++) {
|
|
208
|
+
const next = [];
|
|
209
|
+
for (const p of frontier)
|
|
210
|
+
for (const d of this.rev.get(p) ?? [])
|
|
211
|
+
if (!seen.has(d)) {
|
|
212
|
+
seen.set(d, h);
|
|
213
|
+
next.push(d);
|
|
214
|
+
}
|
|
215
|
+
frontier = next;
|
|
216
|
+
}
|
|
217
|
+
seen.delete(path);
|
|
218
|
+
return [...seen].map(([file, hops]) => ({ file, hops })).sort((a, b) => a.hops - b.hops || (a.file < b.file ? -1 : 1));
|
|
219
|
+
}
|
|
220
|
+
definitions(name) { return this.symDefs.get(name) ?? []; }
|
|
221
|
+
/** Files that mention `name` as an identifier (excluding the ones that define it), with counts. */
|
|
222
|
+
users(name) { const defs = new Set(this.definitions(name).map((d) => d.file)); return [...this.files.values()].filter((f) => f.ids.has(name) && !defs.has(f.path)).map((f) => ({ file: f.path, count: f.ids.get(name) })).sort((a, b) => b.count - a.count || (a.file < b.file ? -1 : 1)); }
|
|
223
|
+
/** Rank files for `query` (identifier overlap with paths and symbols, weighted up for definitions) with a small boost for files many others import. Without a query: the most central files. */
|
|
224
|
+
rank(query, limit = 12) {
|
|
225
|
+
const q = [...new Set(words(query ?? ""))], maxIn = Math.max(1, ...[...this.rev.values()].map((s) => s.size));
|
|
226
|
+
const rows = [...this.files.values()].map((f) => {
|
|
227
|
+
const pw = new Set(words(f.path)), why = [];
|
|
228
|
+
let score = 0;
|
|
229
|
+
for (const w of q) {
|
|
230
|
+
if (pw.has(w)) {
|
|
231
|
+
score += 3;
|
|
232
|
+
why.push(`path:${w}`);
|
|
233
|
+
}
|
|
234
|
+
for (const s of f.symbols)
|
|
235
|
+
if (words(s.name).includes(w)) {
|
|
236
|
+
score += s.kind === "class" || s.kind === "type" || s.kind === "interface" ? 4 : 3;
|
|
237
|
+
if (why.length < 4)
|
|
238
|
+
why.push(`${s.kind} ${s.name}`);
|
|
239
|
+
}
|
|
240
|
+
const direct = f.ids.get(w) ?? [...f.ids.keys()].filter((k) => words(k).includes(w)).reduce((n, k) => n + Math.min(3, f.ids.get(k)), 0);
|
|
241
|
+
if (direct)
|
|
242
|
+
score += Math.min(2, Math.log2(1 + direct) * 0.5);
|
|
243
|
+
}
|
|
244
|
+
const central = (this.rev.get(f.path)?.size ?? 0) / maxIn;
|
|
245
|
+
if (q.length && score === 0)
|
|
246
|
+
return { file: f, score: 0, why }; // a file that matches no query word is not an answer, however central it is
|
|
247
|
+
return { file: f, score: (q.length ? score : 0) + central * (q.length ? 1.2 : 5) + Math.min(f.symbols.length, 20) * (q.length ? 0 : 0.05), why };
|
|
248
|
+
});
|
|
249
|
+
return rows.filter((r) => r.score > 0).sort((a, b) => b.score - a.score || (a.file.path < b.file.path ? -1 : 1)).slice(0, limit);
|
|
250
|
+
}
|
|
251
|
+
/** A compact map: ranked files with their main symbols, within a character budget. */
|
|
252
|
+
map(query, maxChars = 3500, limit = 14) {
|
|
253
|
+
const out = [];
|
|
254
|
+
let used = 0;
|
|
255
|
+
for (const r of this.rank(query, limit)) {
|
|
256
|
+
const syms = r.file.symbols.filter((s) => s.kind !== "method" || query).slice(0, 8).map((s) => ` ${s.kind} ${s.name} :${s.line}`), head = `${r.file.path} (${r.file.lines} lines${this.rev.get(r.file.path)?.size ? `, imported by ${this.rev.get(r.file.path).size}` : ""})`;
|
|
257
|
+
const block = [head, ...syms].join("\n");
|
|
258
|
+
if (used + block.length > maxChars && out.length) {
|
|
259
|
+
out.push(`… more files match; narrow the query`);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
out.push(block);
|
|
263
|
+
used += block.length + 1;
|
|
264
|
+
}
|
|
265
|
+
return out.join("\n") || "(nothing indexed matches; try another word, or use grep)";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Tools: `repo_map`, `find_refs`, `impact`. Read-only. */
|
|
269
|
+
export function indexTools(index) {
|
|
270
|
+
const ready = () => { index.refresh(); };
|
|
271
|
+
return [
|
|
272
|
+
defineTool({ name: "repo_map", readOnly: true,
|
|
273
|
+
description: "Where things live in this repository: the most relevant files for a query (words from a feature, symbol or concept), each with its main functions, classes and types and how many files import it. Use it first in an unfamiliar or large repository, before grep.",
|
|
274
|
+
schema: { type: "object", properties: { query: { type: "string", description: "what you are looking for, e.g. 'invoice tax calculation'; omit for the overall structure" }, max_chars: { type: "integer" } } },
|
|
275
|
+
run: ({ query, max_chars }) => { ready(); return index.map(query, Math.min(8000, Math.max(500, max_chars ?? 3500))); } }),
|
|
276
|
+
defineTool({ name: "find_refs", readOnly: true,
|
|
277
|
+
description: "Where a function, class or type is defined, and which files use it (by identifier, with counts). Faster than grep for 'who calls this'.",
|
|
278
|
+
schema: { type: "object", required: ["symbol"], properties: { symbol: { type: "string" } } },
|
|
279
|
+
run: ({ symbol }) => {
|
|
280
|
+
ready();
|
|
281
|
+
const defs = index.definitions(symbol), users = index.users(symbol);
|
|
282
|
+
if (!defs.length && !users.length)
|
|
283
|
+
return `no definition or use of "${symbol}" found in the index (it indexes functions, classes and types in ${[...new Set([...index.files.values()].map((f) => f.lang))].join(", ")}; try grep for other text)`;
|
|
284
|
+
return [`definitions:`, ...(defs.length ? defs.slice(0, 10).map((d) => ` ${d.file}:${d.sym.line} ${d.sym.sig}`) : [" (none found; it may come from a dependency)"]), `used in ${users.length} file(s):`, ...users.slice(0, 25).map((u) => ` ${u.file} (${u.count}×)`), ...(users.length > 25 ? [` … and ${users.length - 25} more`] : [])].join("\n");
|
|
285
|
+
} }),
|
|
286
|
+
defineTool({ name: "impact", readOnly: true,
|
|
287
|
+
description: "What depends on a file: who imports it (directly and one step further) and which of its symbols are used elsewhere. Use before changing a shared file's behaviour or signature.",
|
|
288
|
+
schema: { type: "object", required: ["path"], properties: { path: { type: "string" } } },
|
|
289
|
+
run: ({ path }) => {
|
|
290
|
+
ready();
|
|
291
|
+
const p = path.replace(/^\.\//, ""), f = index.files.get(p);
|
|
292
|
+
if (!f)
|
|
293
|
+
return `${p} is not in the index (indexed: ${[...index.files.keys()].slice(0, 5).join(", ")}…)`;
|
|
294
|
+
const deps = index.dependents(p, 2), used = f.symbols.map((s) => ({ s, u: index.users(s.name).filter((x) => x.file !== p) })).filter((x) => x.u.length);
|
|
295
|
+
return [`${p}: imports ${index.imports(p).length} file(s); ${index.importedBy(p).length} import it directly, ${deps.filter((d) => d.hops === 2).length} more through them.`,
|
|
296
|
+
...(deps.length ? ["dependents:", ...deps.slice(0, 20).map((d) => ` ${d.file}${d.hops > 1 ? " (via another import)" : ""}`)] : ["nothing in the index imports this file"]),
|
|
297
|
+
...(used.length ? ["symbols used elsewhere:", ...used.slice(0, 12).map(({ s, u }) => ` ${s.kind} ${s.name} → ${u.slice(0, 4).map((x) => x.file).join(", ")}${u.length > 4 ? ` +${u.length - 4}` : ""}`)] : [])].join("\n");
|
|
298
|
+
} }),
|
|
299
|
+
];
|
|
300
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Approve, type Budget, type Event, type Message, type Model, type Tool } from "./harness.ts";
|
|
2
|
+
import { type SearchProvider } from "./web.ts";
|
|
3
|
+
/** In-memory virtual filesystem, optionally mirrored to `dir` so work and memory survive across runs. */
|
|
4
|
+
export declare class VFS {
|
|
5
|
+
files: Map<string, string>;
|
|
6
|
+
dir?: string;
|
|
7
|
+
constructor(dir?: string);
|
|
8
|
+
norm(p: string): string;
|
|
9
|
+
read(p: string): string;
|
|
10
|
+
write(p: string, c: string): void;
|
|
11
|
+
ls(prefix?: string): string;
|
|
12
|
+
}
|
|
13
|
+
export type Todo = {
|
|
14
|
+
task: string;
|
|
15
|
+
status: "pending" | "in_progress" | "done";
|
|
16
|
+
};
|
|
17
|
+
export type SubAgent = {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
system: string;
|
|
21
|
+
tools?: Tool[];
|
|
22
|
+
};
|
|
23
|
+
export type DeepOptions = {
|
|
24
|
+
model: Model;
|
|
25
|
+
/** Extra tools for the lead agent and all subagents (the web tools are included by default). */
|
|
26
|
+
tools?: Tool[];
|
|
27
|
+
search?: SearchProvider;
|
|
28
|
+
web?: boolean;
|
|
29
|
+
/** Subagent types the lead can delegate to via `task`. A general-purpose one always exists. */
|
|
30
|
+
subagents?: SubAgent[];
|
|
31
|
+
/** Persist the virtual filesystem here (e.g. ./.amness) so later runs see earlier files. */
|
|
32
|
+
dir?: string;
|
|
33
|
+
maxSteps?: number;
|
|
34
|
+
subMaxSteps?: number;
|
|
35
|
+
/** One cap shared by the lead and every subagent. */
|
|
36
|
+
budget?: Budget;
|
|
37
|
+
/** Asked before any `risky` tool (yours, or anything you add). Default: deny. */
|
|
38
|
+
approve?: Approve;
|
|
39
|
+
/** Reject a report that cites URLs the agents never actually saw (search results or fetched pages), or cites none. Default true. */
|
|
40
|
+
citations?: boolean;
|
|
41
|
+
/** Tool results longer than this are saved to /offload/* and replaced by a preview + path. */
|
|
42
|
+
offloadAt?: number;
|
|
43
|
+
onEvent?: (e: Event & {
|
|
44
|
+
agent?: string;
|
|
45
|
+
}) => void;
|
|
46
|
+
};
|
|
47
|
+
export declare function createDeepAgent(o: DeepOptions): {
|
|
48
|
+
fs: VFS;
|
|
49
|
+
readonly todos: Todo[];
|
|
50
|
+
/** Run to completion; returns the report text (from /report.md) plus the lead's summary. */
|
|
51
|
+
run(goal: string, opts?: {
|
|
52
|
+
resume?: boolean;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
report: string | undefined;
|
|
55
|
+
text: string;
|
|
56
|
+
finished: boolean;
|
|
57
|
+
steps: number;
|
|
58
|
+
messages: Message[];
|
|
59
|
+
stopped?: "budget" | "maxSteps" | "aborted" | "stuck" | "blocked";
|
|
60
|
+
tokens: number;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
package/dist/src/deep.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { createHarness, defineTool } from "./harness.js";
|
|
4
|
+
import { webTools } from "./web.js";
|
|
5
|
+
/** In-memory virtual filesystem, optionally mirrored to `dir` so work and memory survive across runs. */
|
|
6
|
+
export class VFS {
|
|
7
|
+
files = new Map();
|
|
8
|
+
dir;
|
|
9
|
+
constructor(dir) {
|
|
10
|
+
this.dir = dir;
|
|
11
|
+
if (dir && existsSync(dir))
|
|
12
|
+
(function walk(d, base, f) {
|
|
13
|
+
for (const e of readdirSync(d)) {
|
|
14
|
+
if (e.startsWith("."))
|
|
15
|
+
continue;
|
|
16
|
+
const p = join(d, e);
|
|
17
|
+
statSync(p).isDirectory() ? walk(p, base + "/" + e, f) : f.set(base + "/" + e, readFileSync(p, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
})(dir, "", this.files);
|
|
20
|
+
}
|
|
21
|
+
norm(p) { return "/" + p.split("/").filter((x) => x && x !== "." && x !== "..").join("/"); }
|
|
22
|
+
read(p) { const f = this.files.get(this.norm(p)); if (f === undefined)
|
|
23
|
+
throw new Error(`no such file: ${p}. Use ls.`); return f; }
|
|
24
|
+
write(p, c) {
|
|
25
|
+
p = this.norm(p);
|
|
26
|
+
this.files.set(p, c);
|
|
27
|
+
if (this.dir) {
|
|
28
|
+
const abs = resolve(this.dir, "." + p);
|
|
29
|
+
if (abs.startsWith(resolve(this.dir) + sep)) {
|
|
30
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
31
|
+
writeFileSync(abs, c);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
ls(prefix = "/") { const x = this.norm(prefix); return [...this.files].filter(([k]) => k.startsWith(x)).map(([k, v]) => `${k} (${v.length} chars)`).join("\n") || "(empty)"; }
|
|
36
|
+
}
|
|
37
|
+
const LEAD = `You are a deep agent: you tackle open-ended, multi-step tasks by planning, delegating, and writing things down.
|
|
38
|
+
|
|
39
|
+
Method:
|
|
40
|
+
1. PLAN FIRST. Call \`write_todos\` with a short ordered list. Keep exactly one item in_progress. Update it as you go; the plan is your memory.
|
|
41
|
+
2. DELEGATE. Use \`task\` for any self-contained research or analysis chunk. Subagents have fresh context and return only a summary, so give each a complete, specific brief (they see nothing else). Issue independent \`task\` calls in the same turn so they run in parallel.
|
|
42
|
+
3. OFFLOAD. Long material lives in files (\`write_file\`/\`read_file\`), not in your head. Tool output that is too long is saved to /offload/ for you; read only the parts you need.
|
|
43
|
+
4. REFLECT. After each batch of findings call \`think\`: what is established, what is missing or contradictory, what to do next.
|
|
44
|
+
5. VERIFY. Cross-check important claims against at least two independent sources. Say so when you can't.
|
|
45
|
+
6. DELIVER. Write the final deliverable to /report.md with sources as URLs, then call \`finish\` with a short summary. \`finish\` is rejected while todos are open or /report.md is missing.
|
|
46
|
+
|
|
47
|
+
Never invent facts, URLs or quotes. Prefer primary sources. Be concise.`;
|
|
48
|
+
const SUB = `You are a focused research/analysis subagent working for a lead agent. You cannot see its conversation; your brief is all you have.
|
|
49
|
+
Do the assigned job thoroughly using your tools (search with several varied queries, read primary sources, cross-check). Save bulky raw material to files if useful.
|
|
50
|
+
Finish with \`finish\`: a dense summary of findings (facts, numbers, source URLs, confidence, open questions). The lead sees only that summary, so make it self-contained.
|
|
51
|
+
Never invent facts or URLs.`;
|
|
52
|
+
export function createDeepAgent(o) {
|
|
53
|
+
const fs = new VFS(o.dir);
|
|
54
|
+
let todos = [];
|
|
55
|
+
let offloads = 0;
|
|
56
|
+
const emit = (agent) => (e) => o.onEvent?.({ ...e, agent });
|
|
57
|
+
const offloadAt = o.offloadAt ?? 5000;
|
|
58
|
+
const seenUrls = new Set();
|
|
59
|
+
const urlKey = (u) => u.replace(/[).,;:'"\]>]+$/, "").replace(/#.*$/, "").replace(/\/$/, "").toLowerCase();
|
|
60
|
+
const urlsIn = (t) => (t.match(/https?:\/\/[^\s<>"'`\])]+/g) ?? []).map(urlKey);
|
|
61
|
+
/** Record every URL a web tool returned or fetched, so the report's citations can be checked against reality. */
|
|
62
|
+
const track = (t) => !["web_search", "fetch_url"].includes(t.name) ? t : ({
|
|
63
|
+
...t, run: async (a, ctx) => {
|
|
64
|
+
const r = String(await t.run(a, ctx));
|
|
65
|
+
if (t.name === "fetch_url")
|
|
66
|
+
seenUrls.add(urlKey(a.url));
|
|
67
|
+
else
|
|
68
|
+
urlsIn(r).forEach((u) => seenUrls.add(u));
|
|
69
|
+
return r;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
/** Large results go to a file (reversible) instead of being compressed (lossy). */
|
|
73
|
+
const offload = (t) => ({
|
|
74
|
+
...t,
|
|
75
|
+
run: async (a, ctx) => {
|
|
76
|
+
const r = await t.run(a, ctx);
|
|
77
|
+
const s = typeof r === "string" ? r : JSON.stringify(r) ?? "ok";
|
|
78
|
+
if (s.length <= offloadAt)
|
|
79
|
+
return s;
|
|
80
|
+
const path = `/offload/${++offloads}-${t.name}.txt`;
|
|
81
|
+
fs.write(path, s);
|
|
82
|
+
return `${s.slice(0, 1200)}\n...\n[${s.length} chars total, saved to ${path}. read_file it with start/end to see more]`;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const fileTools = [
|
|
86
|
+
defineTool({ name: "ls", description: "List files in the shared virtual filesystem.",
|
|
87
|
+
schema: { type: "object", properties: { path: { type: "string" } } }, run: ({ path }) => fs.ls(path) }),
|
|
88
|
+
defineTool({ name: "read_file", description: "Read a file (optionally lines start..end, 1-based).",
|
|
89
|
+
schema: { type: "object", required: ["path"], properties: { path: { type: "string" }, start: { type: "integer" }, end: { type: "integer" } } },
|
|
90
|
+
run: ({ path, start, end }) => fs.read(path).split("\n").slice((start ?? 1) - 1, end).map((l, i) => `${(start ?? 1) + i}\t${l}`).join("\n") }),
|
|
91
|
+
defineTool({ name: "write_file", description: "Create or overwrite a file.",
|
|
92
|
+
schema: { type: "object", required: ["path", "content"], properties: { path: { type: "string" }, content: { type: "string" } } },
|
|
93
|
+
run: ({ path, content }) => (fs.write(path, content), `wrote ${content.length} chars to ${fs.norm(path)}`) }),
|
|
94
|
+
defineTool({ name: "edit_file", description: "Replace one exact, unique occurrence of `old` with `new`.",
|
|
95
|
+
schema: { type: "object", required: ["path", "old", "new"], properties: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" } } },
|
|
96
|
+
run: ({ path, old, new: nw }) => {
|
|
97
|
+
const s = fs.read(path), n = s.split(old).length - 1;
|
|
98
|
+
if (n !== 1)
|
|
99
|
+
return `ERROR: \`old\` matched ${n} times; it must match exactly once.`;
|
|
100
|
+
fs.write(path, s.replace(old, () => nw));
|
|
101
|
+
return "edited " + fs.norm(path);
|
|
102
|
+
} }),
|
|
103
|
+
];
|
|
104
|
+
const think = defineTool({
|
|
105
|
+
name: "think", description: "Reflect: distill what you've learned, what's missing, and the next step. Appends to /notes.md.",
|
|
106
|
+
schema: { type: "object", required: ["note"], properties: { note: { type: "string" } } },
|
|
107
|
+
run: ({ note }) => (fs.write("/notes.md", (fs.files.get("/notes.md") ?? "") + `- ${note}\n`), "noted. Continue."),
|
|
108
|
+
});
|
|
109
|
+
const shared = [...(o.web === false ? [] : webTools(o.search).map(track)), ...(o.tools ?? []), ...fileTools, think].map(offload);
|
|
110
|
+
const subs = new Map([["general", { name: "general", description: "General-purpose researcher with web, file and reflection tools.", system: SUB }]]);
|
|
111
|
+
for (const s of o.subagents ?? [])
|
|
112
|
+
subs.set(s.name, { ...s, system: s.system + "\n\n" + SUB });
|
|
113
|
+
const task = defineTool({
|
|
114
|
+
name: "task", parallel: true,
|
|
115
|
+
description: `Delegate a self-contained job to a subagent with fresh context; returns only its summary. Agents: ${[...subs.values()].map((s) => `${s.name} (${s.description})`).join("; ")}.`,
|
|
116
|
+
schema: { type: "object", required: ["description"], properties: { agent: { type: "string" }, description: { type: "string", description: "Complete brief: goal, scope, what to return. The subagent sees nothing else." } } },
|
|
117
|
+
run: async ({ agent, description }) => {
|
|
118
|
+
const s = subs.get(agent ?? "general");
|
|
119
|
+
if (!s)
|
|
120
|
+
return `ERROR: unknown agent "${agent}". Available: ${[...subs.keys()].join(", ")}`;
|
|
121
|
+
const r = await createHarness({
|
|
122
|
+
model: o.model, system: s.system, tools: [...shared, ...(s.tools ?? []).map(offload)], maxSteps: o.subMaxSteps ?? 20,
|
|
123
|
+
budget: o.budget, approve: o.approve, onEvent: emit(s.name),
|
|
124
|
+
}).run(description);
|
|
125
|
+
return r.finished ? r.text : `(subagent did not finish cleanly) ${r.text}`;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const todoTool = defineTool({
|
|
129
|
+
name: "write_todos", description: "Set the full plan (replaces the old one). Statuses: pending | in_progress | done.",
|
|
130
|
+
schema: { type: "object", required: ["todos"], properties: { todos: { type: "array", items: { type: "object", required: ["task", "status"],
|
|
131
|
+
properties: { task: { type: "string" }, status: { type: "string", enum: ["pending", "in_progress", "done"] } } } } } },
|
|
132
|
+
run: ({ todos: t }) => { todos = t; return "plan:\n" + t.map((x, i) => `${i + 1}. [${x.status}] ${x.task}`).join("\n"); },
|
|
133
|
+
});
|
|
134
|
+
const lead = createHarness({
|
|
135
|
+
model: o.model, system: LEAD, tools: [todoTool, task, ...shared], maxSteps: o.maxSteps ?? 60,
|
|
136
|
+
onEvent: emit("lead"), budget: o.budget, approve: o.approve,
|
|
137
|
+
checkpoint: o.dir ? (m) => {
|
|
138
|
+
mkdirSync(o.dir, { recursive: true });
|
|
139
|
+
writeFileSync(join(o.dir, ".checkpoint.json"), JSON.stringify({ messages: m, todos, urls: [...seenUrls] }));
|
|
140
|
+
} : undefined,
|
|
141
|
+
verify: () => {
|
|
142
|
+
if (!todos.length)
|
|
143
|
+
return "You have no plan. Call write_todos, do the work, then finish.";
|
|
144
|
+
const open = todos.filter((t) => t.status !== "done");
|
|
145
|
+
if (open.length)
|
|
146
|
+
return `Open todos: ${open.map((t) => t.task).join("; ")}. Complete them (or remove them via write_todos if obsolete).`;
|
|
147
|
+
const report = fs.files.get("/report.md");
|
|
148
|
+
if (report === undefined)
|
|
149
|
+
return "Write the deliverable to /report.md first.";
|
|
150
|
+
if (o.citations !== false && o.web !== false) {
|
|
151
|
+
const cited = [...new Set(urlsIn(report))];
|
|
152
|
+
if (!cited.length)
|
|
153
|
+
return "/report.md cites no sources. Add the URLs that support each claim.";
|
|
154
|
+
const bogus = cited.filter((u) => !seenUrls.has(u));
|
|
155
|
+
if (bogus.length)
|
|
156
|
+
return `/report.md cites URLs no agent ever saw (possible hallucination): ${bogus.slice(0, 8).join(", ")}. Remove them, or fetch/search to confirm them, then fix the report.`;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
fs,
|
|
162
|
+
get todos() { return todos; },
|
|
163
|
+
/** Run to completion; returns the report text (from /report.md) plus the lead's summary. */
|
|
164
|
+
async run(goal, opts = {}) {
|
|
165
|
+
let resume;
|
|
166
|
+
const cp = o.dir && join(o.dir, ".checkpoint.json");
|
|
167
|
+
if (opts.resume && cp && existsSync(cp)) {
|
|
168
|
+
const j = JSON.parse(readFileSync(cp, "utf8"));
|
|
169
|
+
resume = j.messages;
|
|
170
|
+
todos = j.todos ?? [];
|
|
171
|
+
(j.urls ?? []).forEach((u) => seenUrls.add(u));
|
|
172
|
+
}
|
|
173
|
+
const r = await lead.run(goal, { resume });
|
|
174
|
+
return { ...r, report: fs.files.get("/report.md") };
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|