min-agent 0.2.0 → 0.3.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/README.md +146 -18
- package/dist/agent.js +293 -408
- package/dist/assistant-stream.js +11 -7
- package/dist/cli.js +403 -140
- package/dist/clipboard.js +59 -23
- package/dist/code-mode.js +3 -3
- package/dist/compaction.js +182 -81
- package/dist/config.js +186 -35
- package/dist/confirm.js +55 -6
- package/dist/context-window.js +67 -54
- package/dist/doom-loop.js +19 -12
- package/dist/http.js +119 -0
- package/dist/instructions.js +51 -33
- package/dist/logger.js +66 -0
- package/dist/markdown.js +3 -44
- package/dist/mcp.js +547 -100
- package/dist/memory.js +48 -6
- package/dist/output.js +36 -27
- package/dist/paste-handler.js +3 -3
- package/dist/plugins.js +33 -6
- package/dist/pricing.js +119 -0
- package/dist/provider.js +17 -15
- package/dist/serve.js +658 -369
- package/dist/sessions.js +151 -13
- package/dist/skills.js +466 -76
- package/dist/synthetic.js +7 -0
- package/dist/title-gen.js +2 -1
- package/dist/tool-display.js +173 -0
- package/dist/tool-output.js +54 -45
- package/dist/tools/apply_patch.js +191 -0
- package/dist/tools/backend.js +61 -0
- package/dist/tools/bash.js +147 -70
- package/dist/tools/code_search.js +6 -5
- package/dist/tools/edit.js +23 -7
- package/dist/tools/explore.js +80 -12
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +146 -14
- package/dist/tools/index.js +7 -7
- package/dist/tools/question.js +4 -22
- package/dist/tools/read.js +71 -11
- package/dist/tools/task.js +33 -20
- package/dist/tools/todo.js +83 -73
- package/dist/tools/web_fetch.js +150 -46
- package/dist/tools/web_search.js +706 -28
- package/dist/tools/write.js +13 -7
- package/dist/tui/App.js +40 -6
- package/dist/tui/ConfirmBar.js +24 -3
- package/dist/tui/InputBar.js +390 -45
- package/dist/tui/MessageList.js +533 -20
- package/dist/tui/ModelPicker.js +108 -0
- package/dist/tui/QuestionBar.js +104 -0
- package/dist/tui/StatusBar.js +19 -11
- package/dist/tui/agent-runner.js +103 -0
- package/dist/tui/caret-pos.js +134 -0
- package/dist/tui/caret.js +69 -0
- package/dist/tui/diff-view.js +61 -0
- package/dist/tui/drag-state.js +44 -0
- package/dist/tui/index.js +153 -24
- package/dist/tui/input-history.js +44 -0
- package/dist/tui/layout.js +17 -0
- package/dist/tui/mouse.js +46 -0
- package/dist/tui/selection.js +134 -0
- package/dist/tui/slash-commands.js +90 -0
- package/dist/tui/slash-handler.js +370 -0
- package/dist/tui/text-width.js +91 -0
- package/dist/tui/theme.js +12 -0
- package/dist/tui/undo-stack.js +14 -0
- package/dist/tui/use-sgr-mouse.js +27 -0
- package/dist/tui-chat.js +111 -331
- package/dist/updater.js +57 -0
- package/docs/API.md +160 -14
- package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +1510 -0
- package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +2105 -0
- package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +1595 -0
- package/docs/superpowers/plans/2026-08-16-input-caret.md +782 -0
- package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +183 -0
- package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +220 -0
- package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +196 -0
- package/docs/superpowers/specs/2026-08-16-input-caret-design.md +63 -0
- package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +116 -0
- package/package.json +7 -8
package/dist/tools/grep.js
CHANGED
|
@@ -1,8 +1,123 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import path from "path";
|
|
3
5
|
import { truncateToolOutput } from "../tool-output.js";
|
|
6
|
+
const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".out", "coverage", ".min-agent"]);
|
|
7
|
+
const MAX_RESULTS = 500;
|
|
8
|
+
const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
const MAX_RG_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const WALK_CONCURRENCY = 8;
|
|
11
|
+
/** Convert a simple glob like "*.ts" into an anchored regex for file names. */
|
|
12
|
+
function globToRegex(glob) {
|
|
13
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
14
|
+
return new RegExp(`^${escaped}$`);
|
|
15
|
+
}
|
|
16
|
+
/** Fast path: shell out to ripgrep when available. Resolves null if rg is missing or fails. */
|
|
17
|
+
function rgSearch(pattern, root, includeRegex, ignore) {
|
|
18
|
+
const args = [
|
|
19
|
+
"--no-heading",
|
|
20
|
+
"--line-number",
|
|
21
|
+
"-i",
|
|
22
|
+
"-e", pattern,
|
|
23
|
+
"--max-count", "1",
|
|
24
|
+
"-g", "!*.lock",
|
|
25
|
+
"--max-filesize", String(MAX_FILE_BYTES),
|
|
26
|
+
];
|
|
27
|
+
for (const dir of ignore)
|
|
28
|
+
args.push("-g", `!${dir}/**`);
|
|
29
|
+
if (includeRegex) {
|
|
30
|
+
const glob = includeRegex.source
|
|
31
|
+
.replace(/^\^/, "")
|
|
32
|
+
.replace(/\$$/, "")
|
|
33
|
+
.replace(/\\\./g, ".")
|
|
34
|
+
.replace(/\.\*/g, "*");
|
|
35
|
+
args.push("-g", glob);
|
|
36
|
+
}
|
|
37
|
+
args.push(root);
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let total = 0;
|
|
42
|
+
let settled = false;
|
|
43
|
+
const finish = (value) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(value);
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
child.kill("SIGKILL");
|
|
52
|
+
finish(null);
|
|
53
|
+
}, 15000);
|
|
54
|
+
child.stdout.on("data", (chunk) => {
|
|
55
|
+
total += chunk.byteLength;
|
|
56
|
+
if (total > MAX_RG_OUTPUT_BYTES) {
|
|
57
|
+
child.kill("SIGKILL");
|
|
58
|
+
finish(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
});
|
|
63
|
+
child.on("error", () => finish(null));
|
|
64
|
+
child.on("close", (code) => {
|
|
65
|
+
if (code === 1)
|
|
66
|
+
finish([]);
|
|
67
|
+
else if (code === 0)
|
|
68
|
+
finish(Buffer.concat(chunks).toString("utf-8").split("\n").filter(Boolean).slice(0, MAX_RESULTS));
|
|
69
|
+
else
|
|
70
|
+
finish(null);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function walk(dir, regex, includeRegex, results) {
|
|
75
|
+
const dirQueue = [dir];
|
|
76
|
+
const worker = async () => {
|
|
77
|
+
while (dirQueue.length > 0 && results.length < MAX_RESULTS) {
|
|
78
|
+
const current = dirQueue.pop();
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
await Promise.all(entries.map(async (entry) => {
|
|
87
|
+
const full = path.join(current, entry.name);
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
if (!IGNORED_DIRS.has(entry.name) && results.length < MAX_RESULTS)
|
|
90
|
+
dirQueue.push(full);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!entry.isFile())
|
|
94
|
+
return;
|
|
95
|
+
if (includeRegex && !includeRegex.test(entry.name))
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
const info = await stat(full);
|
|
99
|
+
if (info.size > MAX_FILE_BYTES)
|
|
100
|
+
return;
|
|
101
|
+
const content = await readFile(full, "utf-8");
|
|
102
|
+
if (content.includes("\0"))
|
|
103
|
+
return;
|
|
104
|
+
const lines = content.split("\n");
|
|
105
|
+
for (let i = 0; i < lines.length && results.length < MAX_RESULTS; i++) {
|
|
106
|
+
if (regex.test(lines[i])) {
|
|
107
|
+
results.push(`${path.relative(process.cwd(), full)}:${i + 1}:${lines[i]}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// unreadable file — skip
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
await Promise.all(Array.from({ length: WALK_CONCURRENCY }, worker));
|
|
118
|
+
}
|
|
4
119
|
export const grepTool = tool({
|
|
5
|
-
description: "Search for a pattern in files using
|
|
120
|
+
description: "Search for a pattern in files using regex. Returns matching lines with file paths and line numbers. Use this to find code references, usages, and definitions.",
|
|
6
121
|
inputSchema: jsonSchema({
|
|
7
122
|
type: "object",
|
|
8
123
|
properties: {
|
|
@@ -13,22 +128,39 @@ export const grepTool = tool({
|
|
|
13
128
|
required: ["pattern"],
|
|
14
129
|
}),
|
|
15
130
|
execute: async ({ pattern, path: searchPath, include }) => {
|
|
16
|
-
const target = searchPath ?? ".";
|
|
17
|
-
const includeFlag = include ? `--include='${include}'` : "";
|
|
18
|
-
const cmd = `grep -rn ${includeFlag} --color=never -E '${pattern.replace(/'/g, "'\\''")}' '${target}' 2>/dev/null | head -50`;
|
|
19
131
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
132
|
+
const regex = new RegExp(pattern);
|
|
133
|
+
const includeRegex = include ? globToRegex(include) : null;
|
|
134
|
+
const root = path.resolve(process.cwd(), searchPath ?? ".");
|
|
135
|
+
const results = [];
|
|
136
|
+
const info = await stat(root).catch(() => null);
|
|
137
|
+
if (!info)
|
|
138
|
+
return `Error: path not found: ${searchPath ?? "."}`;
|
|
139
|
+
if (info.isFile()) {
|
|
140
|
+
try {
|
|
141
|
+
const content = await readFile(root, "utf-8");
|
|
142
|
+
if (!content.includes("\0")) {
|
|
143
|
+
content.split("\n").forEach((line, i) => {
|
|
144
|
+
if (regex.test(line))
|
|
145
|
+
results.push(`${root}:${i + 1}:${line}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const rgResults = await rgSearch(pattern, root, includeRegex, [...IGNORED_DIRS]);
|
|
153
|
+
if (rgResults !== null) {
|
|
154
|
+
results.push(...rgResults);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await walk(root, regex, includeRegex, results);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const text = results.length > 0 ? results.join("\n") : "No matches found";
|
|
27
161
|
return truncateToolOutput(text, { direction: "head" }).content;
|
|
28
162
|
}
|
|
29
163
|
catch (err) {
|
|
30
|
-
if (err.status === 1)
|
|
31
|
-
return "No matches found";
|
|
32
164
|
return `Error: ${err.message}`;
|
|
33
165
|
}
|
|
34
166
|
},
|
package/dist/tools/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { bashTool } from "./bash.js";
|
|
|
2
2
|
import { readTool } from "./read.js";
|
|
3
3
|
import { writeTool } from "./write.js";
|
|
4
4
|
import { editTool } from "./edit.js";
|
|
5
|
+
import { applyPatchTool } from "./apply_patch.js";
|
|
5
6
|
import { globTool } from "./glob.js";
|
|
6
7
|
import { grepTool } from "./grep.js";
|
|
7
8
|
import { webSearchTool } from "./web_search.js";
|
|
@@ -16,24 +17,27 @@ export function createChatTools() {
|
|
|
16
17
|
read: readTool,
|
|
17
18
|
write: writeTool,
|
|
18
19
|
edit: editTool,
|
|
20
|
+
apply_patch: applyPatchTool,
|
|
19
21
|
glob: globTool,
|
|
20
22
|
grep: grepTool,
|
|
21
|
-
|
|
23
|
+
search_web: webSearchTool,
|
|
22
24
|
web_fetch: webFetchTool,
|
|
25
|
+
todo: todoTool,
|
|
23
26
|
question: questionTool,
|
|
24
27
|
};
|
|
25
28
|
return tools;
|
|
26
29
|
}
|
|
27
|
-
/** Code mode: full tool set including task tracking and code search */
|
|
30
|
+
/** Code mode: full tool set including task tracking and code search. Todo replaced per-run by the caller. */
|
|
28
31
|
export function createCodeTools() {
|
|
29
32
|
const tools = {
|
|
30
33
|
bash: bashTool,
|
|
31
34
|
read: readTool,
|
|
32
35
|
write: writeTool,
|
|
33
36
|
edit: editTool,
|
|
37
|
+
apply_patch: applyPatchTool,
|
|
34
38
|
glob: globTool,
|
|
35
39
|
grep: grepTool,
|
|
36
|
-
|
|
40
|
+
search_web: webSearchTool,
|
|
37
41
|
web_fetch: webFetchTool,
|
|
38
42
|
todo: todoTool,
|
|
39
43
|
question: questionTool,
|
|
@@ -43,7 +47,3 @@ export function createCodeTools() {
|
|
|
43
47
|
}
|
|
44
48
|
return tools;
|
|
45
49
|
}
|
|
46
|
-
/** @deprecated Use createChatTools or createCodeTools */
|
|
47
|
-
export function createTools() {
|
|
48
|
-
return createChatTools();
|
|
49
|
-
}
|
package/dist/tools/question.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import
|
|
2
|
+
import { askQuestion } from "../confirm.js";
|
|
3
3
|
export const questionTool = tool({
|
|
4
4
|
description: `Ask the user a question to get clarification before proceeding. Use this when:
|
|
5
5
|
- The task is ambiguous and you need more information
|
|
@@ -21,15 +21,9 @@ Provide clear, specific questions. Optionally include numbered options for the u
|
|
|
21
21
|
required: ["question"],
|
|
22
22
|
}),
|
|
23
23
|
execute: async ({ question, options }) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
for (let i = 0; i < options.length; i++) {
|
|
28
|
-
console.log(`\x1b[90m ${i + 1}. ${options[i]}\x1b[0m`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const answer = await askUser();
|
|
32
|
-
// If user picked a number and options exist, resolve it
|
|
24
|
+
const answer = await askQuestion(question, options);
|
|
25
|
+
if (answer === null)
|
|
26
|
+
return "User cancelled the question";
|
|
33
27
|
if (options && options.length > 0) {
|
|
34
28
|
const idx = parseInt(answer) - 1;
|
|
35
29
|
if (idx >= 0 && idx < options.length) {
|
|
@@ -39,15 +33,3 @@ Provide clear, specific questions. Optionally include numbered options for the u
|
|
|
39
33
|
return `User answered: ${answer}`;
|
|
40
34
|
},
|
|
41
35
|
});
|
|
42
|
-
function askUser() {
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
const rl = readline.createInterface({
|
|
45
|
-
input: process.stdin,
|
|
46
|
-
output: process.stdout,
|
|
47
|
-
});
|
|
48
|
-
rl.question("\x1b[36m → \x1b[0m", (answer) => {
|
|
49
|
-
rl.close();
|
|
50
|
-
resolve(answer.trim() || "(no answer)");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}
|
package/dist/tools/read.js
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from "fs";
|
|
3
|
+
import { stat, readFile } from "fs/promises";
|
|
4
|
+
import { createInterface } from "readline";
|
|
3
5
|
import path from "path";
|
|
4
|
-
import { truncateToolOutput } from "../tool-output.js";
|
|
6
|
+
import { TOOL_OUTPUT_MAX_BYTES, truncateToolOutput } from "../tool-output.js";
|
|
5
7
|
import { InstructionTracker } from "../instructions.js";
|
|
6
8
|
// Shared tracker instance for context-aware instruction discovery
|
|
7
9
|
const instructionTracker = new InstructionTracker();
|
|
10
|
+
/** Files above this size are streamed instead of loaded into memory. */
|
|
11
|
+
const STREAM_THRESHOLD_BYTES = 20 * 1024 * 1024;
|
|
12
|
+
/** Stream lines [start, end] (1-indexed, inclusive) without loading the whole file. */
|
|
13
|
+
function readLineRange(filePath, start, end) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const lines = [];
|
|
16
|
+
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
17
|
+
let i = 0;
|
|
18
|
+
rl.on("line", (line) => {
|
|
19
|
+
i++;
|
|
20
|
+
if (i >= start && i <= end)
|
|
21
|
+
lines.push(line);
|
|
22
|
+
if (i > end)
|
|
23
|
+
rl.close();
|
|
24
|
+
});
|
|
25
|
+
rl.on("close", () => resolve(lines.join("\n")));
|
|
26
|
+
rl.on("error", reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Stream the first maxBytes of a file, cut on a UTF-8 safe boundary. */
|
|
30
|
+
function readStreamHead(filePath, maxBytes) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let total = 0;
|
|
34
|
+
let truncated = false;
|
|
35
|
+
let totalLines = 0;
|
|
36
|
+
const stream = createReadStream(filePath);
|
|
37
|
+
stream.on("data", (chunk) => {
|
|
38
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
39
|
+
total += buf.byteLength;
|
|
40
|
+
if (total > maxBytes) {
|
|
41
|
+
const keep = buf.byteLength - (total - maxBytes);
|
|
42
|
+
chunks.push(buf.subarray(0, keep));
|
|
43
|
+
truncated = true;
|
|
44
|
+
stream.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
chunks.push(buf);
|
|
48
|
+
});
|
|
49
|
+
stream.on("close", () => {
|
|
50
|
+
totalLines = Buffer.concat(chunks).toString("utf-8").split("\n").length;
|
|
51
|
+
resolve({ text: Buffer.concat(chunks).toString("utf-8"), truncated, totalLines });
|
|
52
|
+
});
|
|
53
|
+
stream.on("error", reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
8
56
|
export const readTool = tool({
|
|
9
57
|
description: "Read the contents of a file. Returns the file content as text. Use this to understand code, check configurations, etc.",
|
|
10
58
|
inputSchema: jsonSchema({
|
|
@@ -19,19 +67,31 @@ export const readTool = tool({
|
|
|
19
67
|
execute: async ({ filePath, startLine, endLine }) => {
|
|
20
68
|
const resolved = path.resolve(process.cwd(), filePath);
|
|
21
69
|
try {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
70
|
+
const info = await stat(resolved);
|
|
71
|
+
if (info.isDirectory())
|
|
24
72
|
return `Error: ${filePath} is a directory, not a file`;
|
|
25
|
-
|
|
73
|
+
if (info.size > STREAM_THRESHOLD_BYTES && !startLine && !endLine) {
|
|
74
|
+
const { text, truncated, totalLines } = await readStreamHead(resolved, TOOL_OUTPUT_MAX_BYTES);
|
|
75
|
+
let result = text;
|
|
76
|
+
if (truncated) {
|
|
77
|
+
result += `\n\n...(file too large: ${info.size} bytes, ${totalLines}+ lines — use startLine/endLine to read specific ranges)...`;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
26
81
|
let result;
|
|
27
|
-
if (startLine || endLine) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
82
|
+
if (startLine != null || endLine != null) {
|
|
83
|
+
const start = startLine ?? 1;
|
|
84
|
+
const end = endLine ?? Number.MAX_SAFE_INTEGER;
|
|
85
|
+
if (start < 1)
|
|
86
|
+
return `Error: startLine must be >= 1`;
|
|
87
|
+
if (endLine != null && end < start)
|
|
88
|
+
return `Error: endLine (${end}) must be >= startLine (${start})`;
|
|
89
|
+
result = await readLineRange(resolved, start, end);
|
|
90
|
+
if (!result)
|
|
91
|
+
return `Error: no lines found in range ${start}-${end}`;
|
|
32
92
|
}
|
|
33
93
|
else {
|
|
34
|
-
result =
|
|
94
|
+
result = await readFile(resolved, "utf-8");
|
|
35
95
|
}
|
|
36
96
|
// Context-aware: discover nearby instruction files
|
|
37
97
|
const nearbyInstructions = instructionTracker.resolveForFile(resolved);
|
package/dist/tools/task.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
2
|
import { resolveModel } from "../provider.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createCodeTools } from "./index.js";
|
|
4
4
|
import { getMcpTools } from "../mcp.js";
|
|
5
|
-
import {
|
|
5
|
+
import { attachSkills } from "../skills.js";
|
|
6
6
|
import { loadPluginTools } from "../plugins.js";
|
|
7
7
|
import { DoomLoopDetector } from "../doom-loop.js";
|
|
8
8
|
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
@@ -19,7 +19,7 @@ Rules:
|
|
|
19
19
|
Working directory: ${process.cwd()}
|
|
20
20
|
Platform: ${process.platform}
|
|
21
21
|
Date: ${new Date().toDateString()}`;
|
|
22
|
-
export function createTaskTool(modelId) {
|
|
22
|
+
export function createTaskTool(modelId, abortSignal, onUsage) {
|
|
23
23
|
return tool({
|
|
24
24
|
description: `Launch a sub-agent to execute a task independently. The sub-agent has its own context and tools. Use this for:
|
|
25
25
|
- Parallel execution: call multiple tasks at once for independent work
|
|
@@ -37,42 +37,48 @@ Call multiple tasks in parallel when the work is independent.`,
|
|
|
37
37
|
required: ["description", "prompt"],
|
|
38
38
|
}),
|
|
39
39
|
execute: async ({ description, prompt }) => {
|
|
40
|
-
console.
|
|
40
|
+
console.error(`\x1b[90m ┌─ Sub-agent: ${description}\x1b[0m`);
|
|
41
41
|
try {
|
|
42
|
-
const result = await runSubAgent(prompt, modelId);
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const result = await runSubAgent(prompt, modelId, abortSignal);
|
|
43
|
+
if (result.usage)
|
|
44
|
+
onUsage?.(result.usage);
|
|
45
|
+
console.error(`\x1b[90m └─ ✓ Done\x1b[0m`);
|
|
46
|
+
return truncateToolOutput(result.text, { direction: "head" }).content;
|
|
45
47
|
}
|
|
46
48
|
catch (err) {
|
|
47
|
-
|
|
49
|
+
if (abortSignal?.aborted)
|
|
50
|
+
return "Sub-agent cancelled by user.";
|
|
51
|
+
console.error(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
|
|
48
52
|
return `Sub-agent error: ${err.message}`;
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
});
|
|
52
56
|
}
|
|
53
|
-
async function runSubAgent(prompt, modelId) {
|
|
57
|
+
async function runSubAgent(prompt, modelId, abortSignal) {
|
|
54
58
|
const model = resolveModel(modelId);
|
|
55
|
-
// Build tools for sub-agent (
|
|
56
|
-
const builtinTools =
|
|
59
|
+
// Build tools for sub-agent (code-mode tool set, minus recursion-prone tools)
|
|
60
|
+
const builtinTools = createCodeTools();
|
|
57
61
|
const mcpTools = getMcpTools();
|
|
58
62
|
const pluginTools = await loadPluginTools();
|
|
59
|
-
const skills = getSkills();
|
|
60
63
|
const allTools = { ...builtinTools, ...pluginTools };
|
|
61
64
|
for (const [id, t] of Object.entries(mcpTools))
|
|
62
65
|
allTools[id] = t;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Remove task tool from sub-agent to prevent infinite recursion
|
|
66
|
+
const skillsPrompt = attachSkills(allTools);
|
|
67
|
+
// Remove task/explore tools from sub-agent to prevent infinite recursion
|
|
66
68
|
delete allTools["task"];
|
|
69
|
+
delete allTools["explore"];
|
|
67
70
|
const messages = [{ role: "user", content: prompt }];
|
|
68
71
|
const doomLoop = new DoomLoopDetector();
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const signal = abortSignal ? AbortSignal.any([abortSignal, controller.signal]) : controller.signal;
|
|
69
74
|
const result = streamText({
|
|
70
75
|
model,
|
|
71
|
-
system: SUB_AGENT_SYSTEM,
|
|
76
|
+
system: skillsPrompt ? `${SUB_AGENT_SYSTEM}\n\n${skillsPrompt}` : SUB_AGENT_SYSTEM,
|
|
72
77
|
messages,
|
|
73
78
|
tools: allTools,
|
|
74
79
|
stopWhen: stepCountIs(SUB_AGENT_MAX_STEPS),
|
|
75
80
|
maxRetries: 2,
|
|
81
|
+
abortSignal: signal,
|
|
76
82
|
onError() { },
|
|
77
83
|
});
|
|
78
84
|
let assistantText = "";
|
|
@@ -83,16 +89,23 @@ async function runSubAgent(prompt, modelId) {
|
|
|
83
89
|
break;
|
|
84
90
|
case "tool-call":
|
|
85
91
|
if (doomLoop.record(event.toolName, event.input)) {
|
|
86
|
-
|
|
92
|
+
controller.abort();
|
|
93
|
+
return { text: assistantText + "\n\n[Sub-agent stopped: doom loop detected]" };
|
|
87
94
|
}
|
|
88
|
-
console.
|
|
95
|
+
console.error(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
89
96
|
break;
|
|
90
97
|
case "tool-result":
|
|
91
98
|
break;
|
|
92
99
|
case "error":
|
|
93
|
-
|
|
100
|
+
controller.abort();
|
|
101
|
+
return { text: assistantText + `\n\n[Sub-agent error: ${event.error}]` };
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
97
|
-
|
|
105
|
+
let usage;
|
|
106
|
+
try {
|
|
107
|
+
usage = await result.usage;
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
return { text: cleaned || "(sub-agent produced no output)", usage };
|
|
98
111
|
}
|
package/dist/tools/todo.js
CHANGED
|
@@ -1,43 +1,40 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
export const TODO_STATUSES = ["pending", "in_progress", "done", "cancelled"];
|
|
3
|
+
/** Create an isolated todo tool for one agent run. */
|
|
4
|
+
export function createTodoTool(opts) {
|
|
5
|
+
const silent = opts?.silent ?? false;
|
|
6
|
+
let todos = [];
|
|
7
|
+
let nextId = 1;
|
|
8
|
+
const formatTodos = () => {
|
|
9
|
+
if (todos.length === 0)
|
|
10
|
+
return "No tasks.";
|
|
11
|
+
const icons = {
|
|
12
|
+
pending: "○",
|
|
13
|
+
in_progress: "◐",
|
|
14
|
+
done: "●",
|
|
15
|
+
cancelled: "✕",
|
|
16
|
+
};
|
|
17
|
+
return todos
|
|
18
|
+
.map((t) => ` ${icons[t.status]} #${t.id} ${t.text}`)
|
|
19
|
+
.join("\n");
|
|
19
20
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
|
|
38
|
-
}
|
|
39
|
-
export const todoTool = tool({
|
|
40
|
-
description: `Create or update tasks to track progress. Use this FREQUENTLY to:
|
|
21
|
+
const printTodos = () => {
|
|
22
|
+
if (silent || todos.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
console.log(`\x1b[90m ┌─ Tasks${"─".repeat(36)}\x1b[0m`);
|
|
25
|
+
for (const t of todos) {
|
|
26
|
+
const icon = t.status === "done" ? "\x1b[32m●\x1b[0m"
|
|
27
|
+
: t.status === "in_progress" ? "\x1b[33m◐\x1b[0m"
|
|
28
|
+
: t.status === "cancelled" ? "\x1b[90m✕\x1b[0m"
|
|
29
|
+
: "\x1b[90m○\x1b[0m";
|
|
30
|
+
const dim = t.status === "done" || t.status === "cancelled" ? "\x1b[90m" : "";
|
|
31
|
+
const reset = dim ? "\x1b[0m" : "";
|
|
32
|
+
console.log(`\x1b[90m │\x1b[0m ${icon} ${dim}#${t.id} ${t.text}${reset}`);
|
|
33
|
+
}
|
|
34
|
+
console.log(`\x1b[90m └${"─".repeat(44)}\x1b[0m`);
|
|
35
|
+
};
|
|
36
|
+
return tool({
|
|
37
|
+
description: `Create or update tasks to track progress. Use this FREQUENTLY to:
|
|
41
38
|
- Plan multi-step work by creating tasks upfront
|
|
42
39
|
- Mark tasks as in_progress when starting them
|
|
43
40
|
- Mark tasks as done when completed
|
|
@@ -46,43 +43,56 @@ export const todoTool = tool({
|
|
|
46
43
|
To create new tasks: provide items with "text" and optionally "status" (defaults to "pending").
|
|
47
44
|
To update existing tasks: provide items with "id" and "status".
|
|
48
45
|
You can mix creates and updates in one call.`,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
inputSchema: jsonSchema({
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
todos: {
|
|
50
|
+
type: "array",
|
|
51
|
+
description: "List of tasks to create or update",
|
|
52
|
+
items: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
text: { type: "string", description: "Task description (for new tasks)" },
|
|
56
|
+
status: { type: "string", description: "Status: pending, in_progress, done, cancelled" },
|
|
57
|
+
id: { type: "number", description: "Task ID (for updating existing tasks)" },
|
|
58
|
+
},
|
|
61
59
|
},
|
|
62
60
|
},
|
|
63
61
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
required: ["todos"],
|
|
63
|
+
}),
|
|
64
|
+
execute: async ({ todos: items }) => {
|
|
65
|
+
const errors = [];
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
if (item.id != null) {
|
|
68
|
+
const existing = todos.find((t) => t.id === item.id);
|
|
69
|
+
if (!existing) {
|
|
70
|
+
errors.push(`task #${item.id} not found`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (item.status) {
|
|
74
|
+
if (!TODO_STATUSES.includes(item.status)) {
|
|
75
|
+
errors.push(`invalid status "${item.status}" for task #${item.id}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
existing.status = item.status;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (item.text) {
|
|
82
|
+
todos.push({
|
|
83
|
+
id: nextId++,
|
|
84
|
+
text: item.text,
|
|
85
|
+
status: TODO_STATUSES.includes(item.status) ? item.status : "pending",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
errors.push("item needs either an id (update) or text (create)");
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
printTodos();
|
|
86
|
-
return formatTodos();
|
|
87
|
-
},
|
|
88
|
-
});
|
|
92
|
+
printTodos();
|
|
93
|
+
return errors.length > 0 ? `${formatTodos()}\n\nWarnings: ${errors.join("; ")}` : formatTodos();
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Shared placeholder — callers in code mode replace it with a per-run instance. */
|
|
98
|
+
export const todoTool = createTodoTool();
|