@vietor/agent-core 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +849 -0
- package/dist/create-session.d.ts +4 -0
- package/dist/create-session.js +51 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -0
- package/dist/llm/anthropic.d.ts +14 -0
- package/dist/llm/anthropic.js +200 -0
- package/dist/llm/base.d.ts +10 -0
- package/dist/llm/base.js +12 -0
- package/dist/llm/client.d.ts +4 -0
- package/dist/llm/client.js +61 -0
- package/dist/llm/completions.d.ts +8 -0
- package/dist/llm/completions.js +88 -0
- package/dist/llm/messages.d.ts +50 -0
- package/dist/llm/messages.js +28 -0
- package/dist/llm/responses.d.ts +14 -0
- package/dist/llm/responses.js +130 -0
- package/dist/llm/types.d.ts +40 -0
- package/dist/llm/types.js +1 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +53 -0
- package/dist/mcp/manager.d.ts +18 -0
- package/dist/mcp/manager.js +155 -0
- package/dist/mcp/types.d.ts +26 -0
- package/dist/mcp/types.js +1 -0
- package/dist/runtime/agent.d.ts +56 -0
- package/dist/runtime/agent.js +253 -0
- package/dist/runtime/events.d.ts +69 -0
- package/dist/runtime/events.js +1 -0
- package/dist/runtime/prompts.d.ts +5 -0
- package/dist/runtime/prompts.js +45 -0
- package/dist/runtime/session-messages.d.ts +43 -0
- package/dist/runtime/session-messages.js +174 -0
- package/dist/runtime/session.d.ts +102 -0
- package/dist/runtime/session.js +375 -0
- package/dist/runtime/sub-agent-runner.d.ts +18 -0
- package/dist/runtime/sub-agent-runner.js +26 -0
- package/dist/runtime/timeline.d.ts +21 -0
- package/dist/runtime/timeline.js +146 -0
- package/dist/runtime/todo-store.d.ts +8 -0
- package/dist/runtime/todo-store.js +15 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +43 -0
- package/dist/tools/ask-user.d.ts +3 -0
- package/dist/tools/ask-user.js +26 -0
- package/dist/tools/file-edit.d.ts +2 -0
- package/dist/tools/file-edit.js +43 -0
- package/dist/tools/file-read.d.ts +2 -0
- package/dist/tools/file-read.js +93 -0
- package/dist/tools/file-write.d.ts +2 -0
- package/dist/tools/file-write.js +25 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +31 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +67 -0
- package/dist/tools/registry.d.ts +30 -0
- package/dist/tools/registry.js +107 -0
- package/dist/tools/shell.d.ts +2 -0
- package/dist/tools/shell.js +57 -0
- package/dist/tools/skill.d.ts +3 -0
- package/dist/tools/skill.js +30 -0
- package/dist/tools/sub-agent.d.ts +7 -0
- package/dist/tools/sub-agent.js +81 -0
- package/dist/tools/todo-write.d.ts +3 -0
- package/dist/tools/todo-write.js +72 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/web-fetch.d.ts +2 -0
- package/dist/tools/web-fetch.js +104 -0
- package/dist/util/async.d.ts +19 -0
- package/dist/util/async.js +93 -0
- package/dist/util/constants.d.ts +25 -0
- package/dist/util/constants.js +27 -0
- package/dist/util/emitter.d.ts +5 -0
- package/dist/util/emitter.js +15 -0
- package/dist/util/file.d.ts +3 -0
- package/dist/util/file.js +19 -0
- package/dist/util/html.d.ts +1 -0
- package/dist/util/html.js +14 -0
- package/dist/util/index.d.ts +7 -0
- package/dist/util/index.js +7 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +31 -0
- package/dist/util/ripgrep.d.ts +10 -0
- package/dist/util/ripgrep.js +34 -0
- package/dist/util/subprocess.d.ts +15 -0
- package/dist/util/subprocess.js +113 -0
- package/dist/util/text.d.ts +15 -0
- package/dist/util/text.js +72 -0
- package/package.json +52 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Emitter } from "../util/emitter.js";
|
|
2
|
+
export class TodoStore {
|
|
3
|
+
listeners = new Emitter();
|
|
4
|
+
items = [];
|
|
5
|
+
get all() {
|
|
6
|
+
return this.items;
|
|
7
|
+
}
|
|
8
|
+
subscribe(listener) {
|
|
9
|
+
return this.listeners.subscribe(listener);
|
|
10
|
+
}
|
|
11
|
+
set(todos) {
|
|
12
|
+
this.items = todos;
|
|
13
|
+
this.listeners.notify();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tryReadFileText } from "../util/file.js";
|
|
4
|
+
function parseSkillFile(skillName, skillFile) {
|
|
5
|
+
const content = tryReadFileText(skillFile);
|
|
6
|
+
if (!content)
|
|
7
|
+
return undefined;
|
|
8
|
+
const frontMatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
9
|
+
const match = content.match(frontMatterRegex);
|
|
10
|
+
let name = null;
|
|
11
|
+
let description = "";
|
|
12
|
+
let prompt = content;
|
|
13
|
+
if (match) {
|
|
14
|
+
const yamlBody = match[1];
|
|
15
|
+
prompt = content.replace(match[0], "").trim();
|
|
16
|
+
const nameMatch = yamlBody.match(/^name:\s*(.+)$/m);
|
|
17
|
+
if (nameMatch)
|
|
18
|
+
name = nameMatch[1].trim();
|
|
19
|
+
const descMatch = yamlBody.match(/^description:\s*(.+)$/m);
|
|
20
|
+
if (descMatch)
|
|
21
|
+
description = descMatch[1].trim();
|
|
22
|
+
}
|
|
23
|
+
if (!name) {
|
|
24
|
+
name = skillName;
|
|
25
|
+
}
|
|
26
|
+
return { name, description, prompt };
|
|
27
|
+
}
|
|
28
|
+
export function tryLoadSkills(path) {
|
|
29
|
+
if (!existsSync(path))
|
|
30
|
+
return undefined;
|
|
31
|
+
const skills = [];
|
|
32
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
33
|
+
if (!entry.isDirectory())
|
|
34
|
+
continue;
|
|
35
|
+
const skillFile = join(path, entry.name, "SKILL.md");
|
|
36
|
+
if (!existsSync(skillFile))
|
|
37
|
+
continue;
|
|
38
|
+
const skill = parseSkillFile(entry.name, skillFile);
|
|
39
|
+
if (skill && skill.prompt)
|
|
40
|
+
skills.push(skill);
|
|
41
|
+
}
|
|
42
|
+
return skills.length > 0 ? skills : undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Tool } from "./types.js";
|
|
2
|
+
export declare const ASK_USER_GUIDANCE = "- When a decision belongs to the user, call AskUser and wait for the answer rather than listing options in prose. Ask when there are multiple reasonable approaches, an irreversible or consequential action, or the request is ambiguous. When you have enough information to proceed, act without asking.";
|
|
3
|
+
export declare function createAskUserTool(ask: (question: string, options: string[]) => Promise<string>): Tool;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { toolError } from "./types.js";
|
|
2
|
+
export const ASK_USER_GUIDANCE = "- When a decision belongs to the user, call AskUser and wait for the answer rather than listing options in prose. Ask when there are multiple reasonable approaches, an irreversible or consequential action, or the request is ambiguous. When you have enough information to proceed, act without asking.";
|
|
3
|
+
const DESCRIPTION = "Ask the user a question and wait for the answer. Provide at least one option. Returns the answer as text.";
|
|
4
|
+
export function createAskUserTool(ask) {
|
|
5
|
+
return {
|
|
6
|
+
name: "AskUser",
|
|
7
|
+
description: DESCRIPTION,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
question: { type: "string", description: "The question to ask the user." },
|
|
12
|
+
options: { type: "array", items: { type: "string" }, minItems: 1, description: "List of choices; at least one required." },
|
|
13
|
+
},
|
|
14
|
+
required: ["question", "options"],
|
|
15
|
+
},
|
|
16
|
+
async execute(args, _ctx) {
|
|
17
|
+
const question = args.question;
|
|
18
|
+
const options = Array.isArray(args.options) ? args.options : [];
|
|
19
|
+
if (!options.length) {
|
|
20
|
+
return toolError("options must contain at least one choice");
|
|
21
|
+
}
|
|
22
|
+
return { content: await ask(question, options) };
|
|
23
|
+
},
|
|
24
|
+
summaryKeys: ["question"],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { resolveRequiredPath } from "../util/file.js";
|
|
3
|
+
const DESCRIPTION = "Replace old_string with new_string in a file. Read the file first — old_string must match exactly including whitespace/indentation. Must be unique unless replace_all is set. For full rewrites prefer FileWrite.";
|
|
4
|
+
export const fileEditTool = {
|
|
5
|
+
name: "FileEdit",
|
|
6
|
+
description: DESCRIPTION,
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
path: { type: "string" },
|
|
11
|
+
old_string: { type: "string" },
|
|
12
|
+
new_string: { type: "string" },
|
|
13
|
+
replace_all: { type: "boolean", description: "replace all occurrences (default false)" },
|
|
14
|
+
},
|
|
15
|
+
required: ["path", "old_string", "new_string"],
|
|
16
|
+
},
|
|
17
|
+
async execute(args, ctx) {
|
|
18
|
+
const resolved = resolveRequiredPath(args, ctx.cwd);
|
|
19
|
+
const oldStr = args.old_string;
|
|
20
|
+
const newStr = args.new_string;
|
|
21
|
+
const all = args.replace_all === true;
|
|
22
|
+
if (!oldStr)
|
|
23
|
+
throw new Error("old_string is required");
|
|
24
|
+
const content = await readFile(resolved, "utf-8");
|
|
25
|
+
if (!content.includes(oldStr))
|
|
26
|
+
throw new Error(`old_string not found in ${args.path}; re-read the file with FileRead to get the exact current text (watch whitespace/indentation)`);
|
|
27
|
+
if (all) {
|
|
28
|
+
await writeFile(resolved, content.split(oldStr).join(newStr), "utf-8");
|
|
29
|
+
return { content: `Edited ${args.path} (replaced all)` };
|
|
30
|
+
}
|
|
31
|
+
const count = content.split(oldStr).length - 1;
|
|
32
|
+
if (count > 1)
|
|
33
|
+
throw new Error(`old_string appears ${count} times in ${args.path}, must be unique (or set replace_all)`);
|
|
34
|
+
await writeFile(resolved, content.replace(oldStr, newStr), "utf-8");
|
|
35
|
+
return { content: `Edited ${args.path}` };
|
|
36
|
+
},
|
|
37
|
+
summarizeResult(result) {
|
|
38
|
+
if (result.isError)
|
|
39
|
+
return "Edit failed";
|
|
40
|
+
return "Edit completed";
|
|
41
|
+
},
|
|
42
|
+
summaryKeys: ["path"],
|
|
43
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { resolveRequiredPath } from "../util/file.js";
|
|
3
|
+
import { DEFAULT_FILE_READ_LIMIT, MAX_FILE_READ_MB, mbToBytes } from "../util/constants.js";
|
|
4
|
+
import { formatCompactNumber, summaryBytes } from "../util/text.js";
|
|
5
|
+
const CHUNK = 64 * 1024;
|
|
6
|
+
const MAX_FILE_READ_BYTES = mbToBytes(MAX_FILE_READ_MB);
|
|
7
|
+
const DESCRIPTION = `Read a file as UTF-8 text, returned with line numbers (cat -n format). Reads up to ${DEFAULT_FILE_READ_LIMIT} lines; use offset and limit to page further. Files over ${MAX_FILE_READ_MB}MB are rejected. Binary files may return garbled output or fail.`;
|
|
8
|
+
async function readPage(handle, offset, limit) {
|
|
9
|
+
const startLine = offset - 1;
|
|
10
|
+
let newlines = 0;
|
|
11
|
+
let windowStart = startLine === 0 ? 0 : -1;
|
|
12
|
+
const pieces = [];
|
|
13
|
+
const buf = Buffer.allocUnsafe(CHUNK);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const { bytesRead } = await handle.read(buf, 0, CHUNK, null);
|
|
16
|
+
if (bytesRead === 0)
|
|
17
|
+
break;
|
|
18
|
+
for (let from = 0;;) {
|
|
19
|
+
const i = buf.indexOf(0x0a, from);
|
|
20
|
+
if (i === -1 || i >= bytesRead)
|
|
21
|
+
break;
|
|
22
|
+
if (newlines === startLine - 1)
|
|
23
|
+
windowStart = i + 1;
|
|
24
|
+
newlines++;
|
|
25
|
+
if (newlines === startLine + limit) {
|
|
26
|
+
if (windowStart >= 0 && windowStart < i)
|
|
27
|
+
pieces.push(Buffer.from(buf.subarray(windowStart, i)));
|
|
28
|
+
return { text: Buffer.concat(pieces).toString("utf-8"), totalLines: -1, eof: false };
|
|
29
|
+
}
|
|
30
|
+
from = i + 1;
|
|
31
|
+
}
|
|
32
|
+
if (windowStart >= 0) {
|
|
33
|
+
if (windowStart < bytesRead)
|
|
34
|
+
pieces.push(Buffer.from(buf.subarray(windowStart, bytesRead)));
|
|
35
|
+
windowStart = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const totalLines = newlines + 1;
|
|
39
|
+
if (startLine >= totalLines)
|
|
40
|
+
return { text: null, totalLines, eof: true };
|
|
41
|
+
return { text: Buffer.concat(pieces).toString("utf-8"), totalLines, eof: true };
|
|
42
|
+
}
|
|
43
|
+
export const fileReadTool = {
|
|
44
|
+
name: "FileRead",
|
|
45
|
+
readOnly: true,
|
|
46
|
+
description: DESCRIPTION,
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
path: { type: "string" },
|
|
51
|
+
offset: { type: "number", description: "line number to start reading from (1-indexed)" },
|
|
52
|
+
limit: { type: "number", description: `number of lines to read (default ${DEFAULT_FILE_READ_LIMIT})` },
|
|
53
|
+
},
|
|
54
|
+
required: ["path"],
|
|
55
|
+
},
|
|
56
|
+
async execute(args, ctx) {
|
|
57
|
+
const resolved = resolveRequiredPath(args, ctx.cwd);
|
|
58
|
+
const offset = args.offset === undefined ? 1 : args.offset;
|
|
59
|
+
const limit = args.limit === undefined ? DEFAULT_FILE_READ_LIMIT : args.limit;
|
|
60
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)
|
|
61
|
+
throw new Error("offset must be a positive integer");
|
|
62
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)
|
|
63
|
+
throw new Error("limit must be a positive integer");
|
|
64
|
+
const handle = await open(resolved, "r");
|
|
65
|
+
try {
|
|
66
|
+
const { size } = await handle.stat();
|
|
67
|
+
if (size > MAX_FILE_READ_BYTES) {
|
|
68
|
+
throw new Error(`file is ${formatCompactNumber(size)} — larger than the ${formatCompactNumber(MAX_FILE_READ_BYTES)} read limit`);
|
|
69
|
+
}
|
|
70
|
+
if (size === 0)
|
|
71
|
+
return { content: "(empty file)" };
|
|
72
|
+
const { text, totalLines, eof } = await readPage(handle, offset, limit);
|
|
73
|
+
if (text === null) {
|
|
74
|
+
return { content: `(offset ${offset} is past end of file; file has ${totalLines} lines)` };
|
|
75
|
+
}
|
|
76
|
+
const lines = text.split("\n");
|
|
77
|
+
let out = lines
|
|
78
|
+
.map((line, i) => `${String(offset + i).padStart(6, " ")}\t${line}`)
|
|
79
|
+
.join("\n");
|
|
80
|
+
if (!eof) {
|
|
81
|
+
out += `\n(more lines; use offset=${offset + limit} to continue)`;
|
|
82
|
+
}
|
|
83
|
+
return { content: out };
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await handle.close();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
summarizeResult(result) {
|
|
90
|
+
return summaryBytes("Read", result, "Read failed");
|
|
91
|
+
},
|
|
92
|
+
summaryKeys: ["path"],
|
|
93
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { resolveRequiredPath } from "../util/file.js";
|
|
4
|
+
const DESCRIPTION = "Write content to a file, overwriting if it exists and creating parent directories. For targeted changes prefer FileEdit.";
|
|
5
|
+
export const fileWriteTool = {
|
|
6
|
+
name: "FileWrite",
|
|
7
|
+
description: DESCRIPTION,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
11
|
+
required: ["path", "content"],
|
|
12
|
+
},
|
|
13
|
+
async execute(args, ctx) {
|
|
14
|
+
const resolved = resolveRequiredPath(args, ctx.cwd);
|
|
15
|
+
await mkdir(dirname(resolved), { recursive: true });
|
|
16
|
+
await writeFile(resolved, args.content, "utf-8");
|
|
17
|
+
return { content: `Wrote ${args.path}` };
|
|
18
|
+
},
|
|
19
|
+
summarizeResult(result) {
|
|
20
|
+
if (result.isError)
|
|
21
|
+
return "Write failed";
|
|
22
|
+
return "Write completed";
|
|
23
|
+
},
|
|
24
|
+
summaryKeys: ["path"],
|
|
25
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { formatRipgrepOutput, ripgrepResultSummary, runRipgrepLines } from "../util/ripgrep.js";
|
|
2
|
+
import { NO_MATCHES } from "../util/constants.js";
|
|
3
|
+
import { resolveOptionalPath } from "../util/file.js";
|
|
4
|
+
const DESCRIPTION = "List files under a directory, optionally filtered by a glob pattern (e.g. **/*.ts). Skips node_modules and .git.";
|
|
5
|
+
export const globTool = {
|
|
6
|
+
name: "Glob",
|
|
7
|
+
readOnly: true,
|
|
8
|
+
description: DESCRIPTION,
|
|
9
|
+
parameters: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
pattern: { type: "string", description: "glob pattern; omit to list all files" },
|
|
13
|
+
path: { type: "string", description: "root directory, defaults to cwd" },
|
|
14
|
+
},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
async execute(args, ctx) {
|
|
18
|
+
const cwd = resolveOptionalPath(args, ctx.cwd);
|
|
19
|
+
const rgArgs = ["--files"];
|
|
20
|
+
const pattern = args.pattern;
|
|
21
|
+
if (pattern)
|
|
22
|
+
rgArgs.push("-g", pattern);
|
|
23
|
+
rgArgs.push(".");
|
|
24
|
+
const { lines, truncated } = await runRipgrepLines(rgArgs, cwd, ctx.signal);
|
|
25
|
+
return { content: formatRipgrepOutput(lines, truncated, NO_MATCHES) };
|
|
26
|
+
},
|
|
27
|
+
summarizeResult(result) {
|
|
28
|
+
return ripgrepResultSummary("file", result, "Glob failed", "Found 0 files");
|
|
29
|
+
},
|
|
30
|
+
summaryKeys: ["pattern", "path"],
|
|
31
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { formatRipgrepOutput, ripgrepResultSummary, runRipgrepLines } from "../util/ripgrep.js";
|
|
2
|
+
import { DEFAULT_GREP_LIMIT, NO_MATCHES } from "../util/constants.js";
|
|
3
|
+
import { resolveOptionalPath } from "../util/file.js";
|
|
4
|
+
const DESCRIPTION = `Search file contents recursively for a regex pattern (RE2 syntax). Skips node_modules and .git. Returns path:line:content, capped at ${DEFAULT_GREP_LIMIT} lines. For large codebases, use output_mode=files_with_matches first, or narrow with glob/type, or raise head_limit.`;
|
|
5
|
+
export const grepTool = {
|
|
6
|
+
name: "Grep",
|
|
7
|
+
readOnly: true,
|
|
8
|
+
description: DESCRIPTION,
|
|
9
|
+
parameters: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
pattern: { type: "string" },
|
|
13
|
+
path: { type: "string", description: "root directory, defaults to cwd" },
|
|
14
|
+
glob: { type: "string", description: "filter files, e.g. *.ts" },
|
|
15
|
+
type: { type: "string", description: "file type, e.g. ts, js, py" },
|
|
16
|
+
output_mode: { type: "string", enum: ["content", "files_with_matches", "count"], description: "defaults to content" },
|
|
17
|
+
ignore_case: { type: "boolean", description: "case-insensitive" },
|
|
18
|
+
before: { type: "number", description: "lines before each match" },
|
|
19
|
+
after: { type: "number", description: "lines after each match" },
|
|
20
|
+
context: { type: "number", description: "lines before and after each match" },
|
|
21
|
+
only_matching: { type: "boolean", description: "only the matched parts" },
|
|
22
|
+
multiline: { type: "boolean", description: "patterns may span newlines" },
|
|
23
|
+
head_limit: { type: "number", description: `max output lines, default ${DEFAULT_GREP_LIMIT}` },
|
|
24
|
+
},
|
|
25
|
+
required: ["pattern"],
|
|
26
|
+
},
|
|
27
|
+
async execute(args, ctx) {
|
|
28
|
+
const cwd = resolveOptionalPath(args, ctx.cwd);
|
|
29
|
+
const rgArgs = ["--line-number", "--with-filename", "--no-heading"];
|
|
30
|
+
if (args.ignore_case)
|
|
31
|
+
rgArgs.push("-i");
|
|
32
|
+
if (args.only_matching)
|
|
33
|
+
rgArgs.push("-o");
|
|
34
|
+
if (args.multiline)
|
|
35
|
+
rgArgs.push("-U", "--multiline-dotall");
|
|
36
|
+
const context = args.context;
|
|
37
|
+
if (context)
|
|
38
|
+
rgArgs.push("-C", String(context));
|
|
39
|
+
else {
|
|
40
|
+
const before = args.before;
|
|
41
|
+
const after = args.after;
|
|
42
|
+
if (before)
|
|
43
|
+
rgArgs.push("-B", String(before));
|
|
44
|
+
if (after)
|
|
45
|
+
rgArgs.push("-A", String(after));
|
|
46
|
+
}
|
|
47
|
+
if (args.glob)
|
|
48
|
+
rgArgs.push("-g", args.glob);
|
|
49
|
+
if (args.type)
|
|
50
|
+
rgArgs.push("-t", args.type);
|
|
51
|
+
const output_mode = args.output_mode || "content";
|
|
52
|
+
const headLimit = args.head_limit || DEFAULT_GREP_LIMIT;
|
|
53
|
+
if (output_mode === "files_with_matches")
|
|
54
|
+
rgArgs.push("-l");
|
|
55
|
+
else if (output_mode === "count")
|
|
56
|
+
rgArgs.push("-c");
|
|
57
|
+
else
|
|
58
|
+
rgArgs.push("-m", String(headLimit));
|
|
59
|
+
rgArgs.push("--", args.pattern, ".");
|
|
60
|
+
const { lines, truncated } = await runRipgrepLines(rgArgs, cwd, ctx.signal, headLimit);
|
|
61
|
+
return { content: formatRipgrepOutput(lines, truncated, NO_MATCHES) };
|
|
62
|
+
},
|
|
63
|
+
summarizeResult(result) {
|
|
64
|
+
return ripgrepResultSummary("match", result, "Grep failed", "Found 0 matches");
|
|
65
|
+
},
|
|
66
|
+
summaryKeys: ["pattern", "path", "glob"],
|
|
67
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Tool, ToolContext, ToolSchema, Todo } from "./types.js";
|
|
2
|
+
import { type SubAgentToolDeps } from "./sub-agent.js";
|
|
3
|
+
import type { Skill } from "../skills/loader.js";
|
|
4
|
+
import type { TextResult } from "./types.js";
|
|
5
|
+
export declare class ToolRegistry {
|
|
6
|
+
private tools;
|
|
7
|
+
private schemasCache;
|
|
8
|
+
register(tool: Tool): this;
|
|
9
|
+
registerAll(tools: Tool[]): this;
|
|
10
|
+
get(name: string): Tool | undefined;
|
|
11
|
+
filter(predicate: (t: Tool) => boolean): Tool[];
|
|
12
|
+
schemas(): ToolSchema[];
|
|
13
|
+
unregister(name: string): void;
|
|
14
|
+
execute(name: string, args: Record<string, unknown>, ctx: ToolContext): Promise<TextResult>;
|
|
15
|
+
summarizeResult(name: string, result: TextResult, durationMs?: number): string;
|
|
16
|
+
summarizeArgs(name: string, args: Record<string, unknown>): string;
|
|
17
|
+
}
|
|
18
|
+
export interface BuiltinToolsOptions {
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
askUser?: boolean;
|
|
21
|
+
todoWrite?: boolean;
|
|
22
|
+
subAgent?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface BuiltinToolsDeps {
|
|
25
|
+
ask: (question: string, options: string[]) => Promise<string>;
|
|
26
|
+
setTodos: (todos: Todo[]) => void;
|
|
27
|
+
resolveSkill?: (name: string) => Skill | undefined;
|
|
28
|
+
subAgent: SubAgentToolDeps;
|
|
29
|
+
}
|
|
30
|
+
export declare function registerBuiltinTools(tools: ToolRegistry, opts: BuiltinToolsOptions | false | undefined, deps: BuiltinToolsDeps): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { toolError } from "./types.js";
|
|
2
|
+
import { shellTool } from "./shell.js";
|
|
3
|
+
import { fileReadTool } from "./file-read.js";
|
|
4
|
+
import { fileWriteTool } from "./file-write.js";
|
|
5
|
+
import { fileEditTool } from "./file-edit.js";
|
|
6
|
+
import { globTool } from "./glob.js";
|
|
7
|
+
import { grepTool } from "./grep.js";
|
|
8
|
+
import { webFetchTool } from "./web-fetch.js";
|
|
9
|
+
import { createAskUserTool } from "./ask-user.js";
|
|
10
|
+
import { createSkillTool } from "./skill.js";
|
|
11
|
+
import { createTodoWriteTool } from "./todo-write.js";
|
|
12
|
+
import { createSubAgentTool } from "./sub-agent.js";
|
|
13
|
+
import { MAX_ARGS_SUMMARY_LENGTH } from "../util/constants.js";
|
|
14
|
+
import { defaultResultSummary, formatDuration, summarizeText, toErrorMessage } from "../util/text.js";
|
|
15
|
+
export class ToolRegistry {
|
|
16
|
+
tools = new Map();
|
|
17
|
+
schemasCache = null;
|
|
18
|
+
register(tool) {
|
|
19
|
+
this.tools.set(tool.name, tool);
|
|
20
|
+
this.schemasCache = null;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
registerAll(tools) {
|
|
24
|
+
for (const t of tools)
|
|
25
|
+
this.tools.set(t.name, t);
|
|
26
|
+
this.schemasCache = null;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
get(name) {
|
|
30
|
+
return this.tools.get(name);
|
|
31
|
+
}
|
|
32
|
+
filter(predicate) {
|
|
33
|
+
return [...this.tools.values()].filter(predicate);
|
|
34
|
+
}
|
|
35
|
+
schemas() {
|
|
36
|
+
if (!this.schemasCache) {
|
|
37
|
+
this.schemasCache = [...this.tools.values()].map((t) => ({
|
|
38
|
+
type: "function",
|
|
39
|
+
function: {
|
|
40
|
+
name: t.name,
|
|
41
|
+
description: t.description,
|
|
42
|
+
parameters: t.parameters,
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return this.schemasCache;
|
|
47
|
+
}
|
|
48
|
+
unregister(name) {
|
|
49
|
+
if (this.tools.delete(name)) {
|
|
50
|
+
this.schemasCache = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async execute(name, args, ctx) {
|
|
54
|
+
const tool = this.tools.get(name);
|
|
55
|
+
if (!tool)
|
|
56
|
+
return toolError(`unknown tool ${name}`);
|
|
57
|
+
try {
|
|
58
|
+
return await tool.execute(args, ctx);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return toolError(toErrorMessage(e));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
summarizeResult(name, result, durationMs) {
|
|
65
|
+
const tool = this.tools.get(name);
|
|
66
|
+
const resultSummary = tool?.summarizeResult
|
|
67
|
+
? tool.summarizeResult(result)
|
|
68
|
+
: defaultResultSummary(result);
|
|
69
|
+
return durationMs !== undefined
|
|
70
|
+
? `[${formatDuration(durationMs / 1000)}] ${resultSummary}`
|
|
71
|
+
: resultSummary;
|
|
72
|
+
}
|
|
73
|
+
summarizeArgs(name, args) {
|
|
74
|
+
const tool = this.tools.get(name);
|
|
75
|
+
if (!tool)
|
|
76
|
+
return "";
|
|
77
|
+
if (tool.summarizeArgs)
|
|
78
|
+
return tool.summarizeArgs(args);
|
|
79
|
+
if (!tool.summaryKeys)
|
|
80
|
+
return "";
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (const k of tool.summaryKeys) {
|
|
83
|
+
const v = args[k];
|
|
84
|
+
if (typeof v === "string" && v) {
|
|
85
|
+
parts.push(v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return summarizeText(parts.join(" "), MAX_ARGS_SUMMARY_LENGTH, true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const BUILTIN_TOOLS = [fileReadTool, globTool, grepTool, webFetchTool, shellTool, fileWriteTool, fileEditTool];
|
|
92
|
+
export function registerBuiltinTools(tools, opts, deps) {
|
|
93
|
+
if (opts === false)
|
|
94
|
+
return;
|
|
95
|
+
const builtins = opts?.readOnly ? BUILTIN_TOOLS.filter((t) => t.readOnly) : BUILTIN_TOOLS;
|
|
96
|
+
for (const tool of builtins) {
|
|
97
|
+
tools.register(tool);
|
|
98
|
+
}
|
|
99
|
+
if (opts?.askUser)
|
|
100
|
+
tools.register(createAskUserTool(deps.ask));
|
|
101
|
+
if (opts?.todoWrite)
|
|
102
|
+
tools.register(createTodoWriteTool(deps.setTodos));
|
|
103
|
+
if (deps.resolveSkill)
|
|
104
|
+
tools.register(createSkillTool(deps.resolveSkill));
|
|
105
|
+
if (opts?.subAgent)
|
|
106
|
+
tools.register(createSubAgentTool(deps.subAgent));
|
|
107
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { runProcess } from "../util/subprocess.js";
|
|
2
|
+
import { CALL_TIMEOUT_MS, NO_OUTPUT } from "../util/constants.js";
|
|
3
|
+
import { toolError } from "./types.js";
|
|
4
|
+
import { summaryBytes } from "../util/text.js";
|
|
5
|
+
const isWindows = process.platform === "win32";
|
|
6
|
+
const shell = isWindows ? "powershell.exe" : (process.env.SHELL || "/bin/bash");
|
|
7
|
+
const shellArgs = isWindows ? ["-NoProfile", "-NonInteractive", "-Command"] : ["-c"];
|
|
8
|
+
const commandPrefix = isWindows
|
|
9
|
+
? "[Console]::OutputEncoding=[Text.Encoding]::UTF8; $OutputEncoding=[Text.Encoding]::UTF8; "
|
|
10
|
+
: "";
|
|
11
|
+
const PRIVILEGED_RE = /(^|[;&|()`\n])\s*(?:(?:env|command|xargs)\s+)?(sudo|su|doas|pkexec)\b/;
|
|
12
|
+
const DESCRIPTION_POWERSHELL = `
|
|
13
|
+
Windows PowerShell 5.1 (powershell.exe — NOT pwsh/bash).
|
|
14
|
+
|
|
15
|
+
WARNING — four common mistakes:
|
|
16
|
+
• QUOTE unquoted args starting with a single - that contain a dot — splits them at the last dot (or use --%).
|
|
17
|
+
Example: -DoutputFile=dep.txt → "-DoutputFile=dep" + ".txt" → fails; use "-DoutputFile=dep.txt"
|
|
18
|
+
Safe unquoted: --key=value, plain paths, dotless values.
|
|
19
|
+
• Use ; not &&/|| to chain commands
|
|
20
|
+
• Use \` (backtick) to escape, not \\
|
|
21
|
+
• Use $env:NAME, not $NAME
|
|
22
|
+
|
|
23
|
+
QUOTING: '...' literal. "..." expands $var, $env:NAME, $(...).
|
|
24
|
+
SYNTAX: Conditional: if ($?) { }. No heredocs (<<) or background (&).
|
|
25
|
+
LIMITATIONS: No stdin. Long-running killed at timeout.
|
|
26
|
+
`;
|
|
27
|
+
const DESCRIPTION_BASH = `
|
|
28
|
+
Execute a bash command.
|
|
29
|
+
|
|
30
|
+
QUOTING: Always double-quote: "$FILE" not $FILE.
|
|
31
|
+
LIMITATIONS: Blocked: direct sudo/su/doas/pkexec (best-effort; indirect invocation may bypass). No stdin. Long-running killed at timeout.
|
|
32
|
+
`;
|
|
33
|
+
export const shellTool = {
|
|
34
|
+
name: "Shell",
|
|
35
|
+
description: isWindows ? DESCRIPTION_POWERSHELL : DESCRIPTION_BASH,
|
|
36
|
+
parameters: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: { command: { type: "string" } },
|
|
39
|
+
required: ["command"],
|
|
40
|
+
},
|
|
41
|
+
async execute(args, ctx) {
|
|
42
|
+
const command = args.command;
|
|
43
|
+
if (!isWindows && PRIVILEGED_RE.test(command)) {
|
|
44
|
+
return toolError("privileged commands (sudo/su/doas/pkexec) are not allowed");
|
|
45
|
+
}
|
|
46
|
+
const r = await runProcess(shell, [...shellArgs, commandPrefix + command], { cwd: ctx.cwd, timeout: CALL_TIMEOUT_MS }, ctx.signal);
|
|
47
|
+
if (r.status === 0 && !r.error) {
|
|
48
|
+
return { content: r.stdout || NO_OUTPUT };
|
|
49
|
+
}
|
|
50
|
+
const parts = [r.stdout, r.stderr, r.error?.message].filter(Boolean);
|
|
51
|
+
return toolError(parts.join("\n") || NO_OUTPUT);
|
|
52
|
+
},
|
|
53
|
+
summarizeResult(result) {
|
|
54
|
+
return summaryBytes("Command executed", result, "Command failed");
|
|
55
|
+
},
|
|
56
|
+
summaryKeys: ["command"],
|
|
57
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SKILL_TOOL_NAME } from "../util/constants.js";
|
|
2
|
+
import { toolError } from "./types.js";
|
|
3
|
+
const DESCRIPTION = "Invoke a skill by name. Skills are packaged instructions that extend capabilities. Available skills and their descriptions are listed in the system prompt. When invoked, the skill's instructions are loaded into context — follow them.";
|
|
4
|
+
export function createSkillTool(resolve) {
|
|
5
|
+
return {
|
|
6
|
+
name: SKILL_TOOL_NAME,
|
|
7
|
+
description: DESCRIPTION,
|
|
8
|
+
parameters: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
name: { type: "string", description: "The name of the skill to invoke" },
|
|
12
|
+
},
|
|
13
|
+
required: ["name"],
|
|
14
|
+
},
|
|
15
|
+
summaryKeys: ["name"],
|
|
16
|
+
async execute(args, _ctx) {
|
|
17
|
+
const name = (args.name || "").trim();
|
|
18
|
+
if (!name) {
|
|
19
|
+
return toolError("skill name is required");
|
|
20
|
+
}
|
|
21
|
+
if (!resolve(name)) {
|
|
22
|
+
return toolError(`skill "${name}" not found`);
|
|
23
|
+
}
|
|
24
|
+
return { content: `Skill "${name}" loaded. Follow its instructions above.` };
|
|
25
|
+
},
|
|
26
|
+
summarizeResult(_result) {
|
|
27
|
+
return "Successfully loaded skill";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SubAgentRunResult } from "../runtime/sub-agent-runner.js";
|
|
2
|
+
import type { Tool } from "./types.js";
|
|
3
|
+
export declare const SUB_AGENT_GUIDANCE = "- Consider delegating to the SubAgent tool when the task matches an agent type, when you have independent work to run in parallel, or when answering would mean reading across several files \u2014 delegate and keep the conclusion, not the file dumps. type: \"explore\" \u2014 read-only search agent for broad fan-out searches (state the search breadth in the task); type: \"plan\" \u2014 software architect producing implementation plans. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you have delegated a search, do not also run it yourself \u2014 wait for the result. Issue at most 2 SubAgent calls per turn; multiple calls in the same turn run concurrently. Sub-agents are read-only and return only their final report, not intermediate steps \u2014 verify important results yourself. For large workloads with many independent items that would exceed the turn budget, split the items into chunks sized so each sub-agent can complete its chunk within its own loop budget, delegate one SubAgent per chunk, and run the remaining chunks in the following turns as results return. Instruct each sub-agent to report results per item in structured lines so you can consolidate.";
|
|
4
|
+
export interface SubAgentToolDeps {
|
|
5
|
+
runSubAgent: (systemPrompt: string, task: string, signal?: AbortSignal) => Promise<SubAgentRunResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createSubAgentTool(deps: SubAgentToolDeps): Tool;
|