reasonix 0.4.24 → 0.4.26
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/dist/cli/{chunk-K6MR4SWS.js → chunk-2BYEKJHX.js} +119 -10
- package/dist/cli/chunk-2BYEKJHX.js.map +1 -0
- package/dist/cli/index.js +477 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-VDN5U3YE.js → prompt-6DMLWG2H.js} +2 -2
- package/dist/index.d.ts +146 -2
- package/dist/index.js +523 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-K6MR4SWS.js.map +0 -1
- /package/dist/cli/{prompt-VDN5U3YE.js.map → prompt-6DMLWG2H.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1814,6 +1814,10 @@ var CacheFirstLoop = class {
|
|
|
1814
1814
|
usage = resp.usage;
|
|
1815
1815
|
}
|
|
1816
1816
|
} catch (err) {
|
|
1817
|
+
if (signal.aborted) {
|
|
1818
|
+
yield { turn: this._turn, role: "done", content: "" };
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1817
1821
|
yield {
|
|
1818
1822
|
turn: this._turn,
|
|
1819
1823
|
role: "error",
|
|
@@ -2215,9 +2219,11 @@ function isValidSkillName(name) {
|
|
|
2215
2219
|
var SkillStore = class {
|
|
2216
2220
|
homeDir;
|
|
2217
2221
|
projectRoot;
|
|
2222
|
+
disableBuiltins;
|
|
2218
2223
|
constructor(opts = {}) {
|
|
2219
2224
|
this.homeDir = opts.homeDir ?? homedir3();
|
|
2220
2225
|
this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : void 0;
|
|
2226
|
+
this.disableBuiltins = opts.disableBuiltins === true;
|
|
2221
2227
|
}
|
|
2222
2228
|
/** True iff this store was configured with a project root. */
|
|
2223
2229
|
hasProjectScope() {
|
|
@@ -2241,8 +2247,8 @@ var SkillStore = class {
|
|
|
2241
2247
|
}
|
|
2242
2248
|
/**
|
|
2243
2249
|
* List every skill visible to this store. On name collisions the
|
|
2244
|
-
* higher-priority root (project over global) wins.
|
|
2245
|
-
* for stable prefix hashing.
|
|
2250
|
+
* higher-priority root (project over global over builtin) wins.
|
|
2251
|
+
* Sorted by name for stable prefix hashing.
|
|
2246
2252
|
*/
|
|
2247
2253
|
list() {
|
|
2248
2254
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2260,6 +2266,11 @@ var SkillStore = class {
|
|
|
2260
2266
|
if (!byName.has(skill.name)) byName.set(skill.name, skill);
|
|
2261
2267
|
}
|
|
2262
2268
|
}
|
|
2269
|
+
if (!this.disableBuiltins) {
|
|
2270
|
+
for (const skill of BUILTIN_SKILLS) {
|
|
2271
|
+
if (!byName.has(skill.name)) byName.set(skill.name, skill);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2263
2274
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2264
2275
|
}
|
|
2265
2276
|
/** Resolve one skill by name. Returns `null` if not found or malformed. */
|
|
@@ -2276,6 +2287,11 @@ var SkillStore = class {
|
|
|
2276
2287
|
return this.parse(flatCandidate, name, scope);
|
|
2277
2288
|
}
|
|
2278
2289
|
}
|
|
2290
|
+
if (!this.disableBuiltins) {
|
|
2291
|
+
for (const skill of BUILTIN_SKILLS) {
|
|
2292
|
+
if (skill.name === name) return skill;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2279
2295
|
return null;
|
|
2280
2296
|
}
|
|
2281
2297
|
readEntry(dir, scope, entry) {
|
|
@@ -2307,15 +2323,21 @@ var SkillStore = class {
|
|
|
2307
2323
|
body: body.trim(),
|
|
2308
2324
|
scope,
|
|
2309
2325
|
path,
|
|
2310
|
-
allowedTools: data["allowed-tools"]
|
|
2326
|
+
allowedTools: data["allowed-tools"],
|
|
2327
|
+
runAs: parseRunAs(data.runAs),
|
|
2328
|
+
model: data.model?.startsWith("deepseek-") ? data.model : void 0
|
|
2311
2329
|
};
|
|
2312
2330
|
}
|
|
2313
2331
|
};
|
|
2332
|
+
function parseRunAs(raw) {
|
|
2333
|
+
return raw?.trim() === "subagent" ? "subagent" : "inline";
|
|
2334
|
+
}
|
|
2314
2335
|
function skillIndexLine(s) {
|
|
2315
2336
|
const safeDesc = s.description.replace(/\n/g, " ").trim();
|
|
2316
|
-
const
|
|
2337
|
+
const marker = s.runAs === "subagent" ? "\u{1F9EC} " : "";
|
|
2338
|
+
const max = 130 - s.name.length - marker.length;
|
|
2317
2339
|
const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
|
|
2318
|
-
return clipped ? `- ${s.name} \u2014 ${clipped}` : `- ${s.name}`;
|
|
2340
|
+
return clipped ? `- ${marker}${s.name} \u2014 ${clipped}` : `- ${marker}${s.name}`;
|
|
2319
2341
|
}
|
|
2320
2342
|
function applySkillsIndex(basePrompt, opts = {}) {
|
|
2321
2343
|
const store = new SkillStore(opts);
|
|
@@ -2328,15 +2350,78 @@ function applySkillsIndex(basePrompt, opts = {}) {
|
|
|
2328
2350
|
return [
|
|
2329
2351
|
basePrompt,
|
|
2330
2352
|
"",
|
|
2331
|
-
"# Skills \u2014
|
|
2353
|
+
"# Skills \u2014 playbooks you can invoke",
|
|
2332
2354
|
"",
|
|
2333
|
-
'One-liner index. Each
|
|
2355
|
+
'One-liner index. Each entry is either a built-in or a user-authored playbook. Call `run_skill({ name: "<skill-name>", arguments: "<task>" })` to invoke one. Skills marked with \u{1F9EC} spawn an **isolated subagent** \u2014 its tool calls and reasoning never enter your context, only its final answer does. Use \u{1F9EC} skills for tasks that would otherwise flood your context (deep exploration, multi-step research, anything where you only need the conclusion). Plain skills are inlined: their body becomes a tool result you read and act on directly. The user can also invoke a skill via `/skill <name>`.',
|
|
2334
2356
|
"",
|
|
2335
2357
|
"```",
|
|
2336
2358
|
truncated,
|
|
2337
2359
|
"```"
|
|
2338
2360
|
].join("\n");
|
|
2339
2361
|
}
|
|
2362
|
+
var BUILTIN_EXPLORE_BODY = `You are running as an exploration subagent. Your job is to investigate the codebase the parent agent pointed you at, then return one focused, distilled answer.
|
|
2363
|
+
|
|
2364
|
+
How to operate:
|
|
2365
|
+
- Use read_file, search_files, search_content, directory_tree, list_directory, get_file_info as your primary tools. Stay read-only.
|
|
2366
|
+
- For "find all places that call / reference / use X" questions, use \`search_content\` (content grep) \u2014 NOT \`search_files\` (which only matches file names). This is the most common subagent mistake; using the wrong tool gives empty results and you waste your iter budget chasing a phantom.
|
|
2367
|
+
- Cast a wide net first (search_content for symbol references, directory_tree for structure) to map the territory; then read the 3-10 most relevant files in full.
|
|
2368
|
+
- Don't read every file \u2014 be selective. Aim for breadth on the first pass, depth only where the question demands it.
|
|
2369
|
+
- Stop exploring as soon as you can answer the question. The parent doesn't see your tool calls, so over-exploration is pure waste.
|
|
2370
|
+
|
|
2371
|
+
Your final answer:
|
|
2372
|
+
- One paragraph (or a few short bullets). Lead with the conclusion.
|
|
2373
|
+
- Cite specific file paths + line ranges when they support the answer.
|
|
2374
|
+
- If the question can't be answered from what you found, say so plainly and suggest where to look next.
|
|
2375
|
+
- No follow-up offers, no "let me know if you need more." The parent will ask again if they need more.
|
|
2376
|
+
|
|
2377
|
+
Formatting (rendered in a TUI):
|
|
2378
|
+
- Tabular data \u2192 GitHub-Flavored Markdown tables with ASCII pipes (\`| col | col |\` + \`| --- | --- |\`). Never use Unicode box-drawing characters (\u2502 \u2500 \u253C) \u2014 they break word-wrap.
|
|
2379
|
+
- Keep table cells short; if a cell needs a paragraph, use bullets below the table instead.
|
|
2380
|
+
- Code, file paths with line ranges, and shell commands \u2192 fenced code blocks (\`\`\`).
|
|
2381
|
+
- NEVER draw decorative frames around code or text with \`\u250C\u2500\u2500\u2510 \u2502 \u2514\u2500\u2500\u2518\` box-drawing characters. Use plain code blocks; the renderer adds its own border.
|
|
2382
|
+
- For flow charts: use a bullet list with \`\u2192\` or \`\u2193\` between steps, not ASCII boxes-and-arrows.
|
|
2383
|
+
|
|
2384
|
+
The 'task' the parent gave you is the question you must answer. Treat any other reading of it as scope creep.`;
|
|
2385
|
+
var BUILTIN_RESEARCH_BODY = `You are running as a research subagent. Your job is to gather information from code AND the web, synthesize it, and return one focused conclusion.
|
|
2386
|
+
|
|
2387
|
+
How to operate:
|
|
2388
|
+
- Combine code reading (read_file, search_files) with web tools (web_search, web_fetch) as appropriate to the question.
|
|
2389
|
+
- For "how does X work" / "is Y supported" questions: web first to find the canonical reference, then verify against the local code.
|
|
2390
|
+
- For "what's our policy on Z" / "where do we use Q": local code first, web only if you need to compare against external standards.
|
|
2391
|
+
- Cap yourself at ~10 tool calls. If you can't converge in 10, return what you have plus a note about what's missing.
|
|
2392
|
+
|
|
2393
|
+
Your final answer:
|
|
2394
|
+
- One paragraph (or short bullets). Lead with the conclusion.
|
|
2395
|
+
- Cite both code (file:line) AND web sources (URL) when they back the answer.
|
|
2396
|
+
- Distinguish "I verified this in code" from "I read this on a docs page" \u2014 the parent will trust the former more.
|
|
2397
|
+
- If the answer is uncertain, say so. Don't invent confidence.
|
|
2398
|
+
|
|
2399
|
+
Formatting (rendered in a TUI):
|
|
2400
|
+
- Tabular data \u2192 GitHub-Flavored Markdown tables with ASCII pipes (\`| col | col |\` + \`| --- | --- |\`). Never use Unicode box-drawing characters (\u2502 \u2500 \u253C) \u2014 they break word-wrap.
|
|
2401
|
+
- Keep table cells short; if a cell needs a paragraph, use bullets below the table instead.
|
|
2402
|
+
- Code, file paths with line ranges, and shell commands \u2192 fenced code blocks (\`\`\`).
|
|
2403
|
+
- NEVER draw decorative frames around code or text with \`\u250C\u2500\u2500\u2510 \u2502 \u2514\u2500\u2500\u2518\` box-drawing characters. Use plain code blocks; the renderer adds its own border.
|
|
2404
|
+
- For flow charts: use a bullet list with \`\u2192\` or \`\u2193\` between steps, not ASCII boxes-and-arrows.
|
|
2405
|
+
|
|
2406
|
+
The 'task' the parent gave you is the research question. Stay on it.`;
|
|
2407
|
+
var BUILTIN_SKILLS = Object.freeze([
|
|
2408
|
+
Object.freeze({
|
|
2409
|
+
name: "explore",
|
|
2410
|
+
description: "Explore the codebase in an isolated subagent \u2014 wide-net read-only investigation that returns one distilled answer. Best for: 'find all places that...', 'how does X work across the project', 'survey the code for Y'.",
|
|
2411
|
+
body: BUILTIN_EXPLORE_BODY,
|
|
2412
|
+
scope: "builtin",
|
|
2413
|
+
path: "(builtin)",
|
|
2414
|
+
runAs: "subagent"
|
|
2415
|
+
}),
|
|
2416
|
+
Object.freeze({
|
|
2417
|
+
name: "research",
|
|
2418
|
+
description: "Research a question by combining web search + code reading in an isolated subagent. Best for: 'is X feature supported by lib Y', 'what's the canonical way to do Z', 'compare our impl against the spec'.",
|
|
2419
|
+
body: BUILTIN_RESEARCH_BODY,
|
|
2420
|
+
scope: "builtin",
|
|
2421
|
+
path: "(builtin)",
|
|
2422
|
+
runAs: "subagent"
|
|
2423
|
+
})
|
|
2424
|
+
]);
|
|
2340
2425
|
|
|
2341
2426
|
// src/user-memory.ts
|
|
2342
2427
|
var USER_MEMORY_DIR = "memory";
|
|
@@ -2618,6 +2703,74 @@ import { promises as fs } from "fs";
|
|
|
2618
2703
|
import * as pathMod from "path";
|
|
2619
2704
|
var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
|
|
2620
2705
|
var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
2706
|
+
var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
2707
|
+
"node_modules",
|
|
2708
|
+
".git",
|
|
2709
|
+
".hg",
|
|
2710
|
+
".svn",
|
|
2711
|
+
"dist",
|
|
2712
|
+
"build",
|
|
2713
|
+
"out",
|
|
2714
|
+
".next",
|
|
2715
|
+
".nuxt",
|
|
2716
|
+
"target",
|
|
2717
|
+
// Rust / Java
|
|
2718
|
+
".venv",
|
|
2719
|
+
"venv",
|
|
2720
|
+
"__pycache__",
|
|
2721
|
+
".pytest_cache",
|
|
2722
|
+
".mypy_cache",
|
|
2723
|
+
".cache",
|
|
2724
|
+
"coverage"
|
|
2725
|
+
]);
|
|
2726
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2727
|
+
".png",
|
|
2728
|
+
".jpg",
|
|
2729
|
+
".jpeg",
|
|
2730
|
+
".gif",
|
|
2731
|
+
".bmp",
|
|
2732
|
+
".ico",
|
|
2733
|
+
".webp",
|
|
2734
|
+
".tiff",
|
|
2735
|
+
".pdf",
|
|
2736
|
+
".zip",
|
|
2737
|
+
".tar",
|
|
2738
|
+
".gz",
|
|
2739
|
+
".bz2",
|
|
2740
|
+
".xz",
|
|
2741
|
+
".7z",
|
|
2742
|
+
".rar",
|
|
2743
|
+
".exe",
|
|
2744
|
+
".dll",
|
|
2745
|
+
".so",
|
|
2746
|
+
".dylib",
|
|
2747
|
+
".bin",
|
|
2748
|
+
".class",
|
|
2749
|
+
".jar",
|
|
2750
|
+
".war",
|
|
2751
|
+
".o",
|
|
2752
|
+
".obj",
|
|
2753
|
+
".lib",
|
|
2754
|
+
".a",
|
|
2755
|
+
".woff",
|
|
2756
|
+
".woff2",
|
|
2757
|
+
".ttf",
|
|
2758
|
+
".otf",
|
|
2759
|
+
".eot",
|
|
2760
|
+
".mp3",
|
|
2761
|
+
".mp4",
|
|
2762
|
+
".mov",
|
|
2763
|
+
".avi",
|
|
2764
|
+
".webm",
|
|
2765
|
+
".wasm",
|
|
2766
|
+
".pyc",
|
|
2767
|
+
".pyo"
|
|
2768
|
+
]);
|
|
2769
|
+
function isLikelyBinaryByName(name) {
|
|
2770
|
+
const dot = name.lastIndexOf(".");
|
|
2771
|
+
if (dot < 0) return false;
|
|
2772
|
+
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
2773
|
+
}
|
|
2621
2774
|
function registerFilesystemTools(registry, opts) {
|
|
2622
2775
|
const rootDir = pathMod.resolve(opts.rootDir);
|
|
2623
2776
|
const allowWriting = opts.allowWriting !== false;
|
|
@@ -2627,7 +2780,12 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2627
2780
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
2628
2781
|
throw new Error("path must be a non-empty string");
|
|
2629
2782
|
}
|
|
2630
|
-
|
|
2783
|
+
let normalized = raw;
|
|
2784
|
+
while (normalized.startsWith("/") || normalized.startsWith("\\")) {
|
|
2785
|
+
normalized = normalized.slice(1);
|
|
2786
|
+
}
|
|
2787
|
+
if (normalized.length === 0) normalized = ".";
|
|
2788
|
+
const resolved = pathMod.resolve(rootDir, normalized);
|
|
2631
2789
|
const normRoot = pathMod.resolve(rootDir);
|
|
2632
2790
|
const rel = pathMod.relative(normRoot, resolved);
|
|
2633
2791
|
if (rel.startsWith("..") || pathMod.isAbsolute(rel)) {
|
|
@@ -2793,6 +2951,114 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2793
2951
|
return matches.length === 0 ? "(no matches)" : matches.join("\n");
|
|
2794
2952
|
}
|
|
2795
2953
|
});
|
|
2954
|
+
registry.register({
|
|
2955
|
+
name: "search_content",
|
|
2956
|
+
description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
|
|
2957
|
+
readOnly: true,
|
|
2958
|
+
parameters: {
|
|
2959
|
+
type: "object",
|
|
2960
|
+
properties: {
|
|
2961
|
+
pattern: {
|
|
2962
|
+
type: "string",
|
|
2963
|
+
description: "Substring (or regex) to search file contents for."
|
|
2964
|
+
},
|
|
2965
|
+
path: {
|
|
2966
|
+
type: "string",
|
|
2967
|
+
description: "Directory to start the search at (default: sandbox root)."
|
|
2968
|
+
},
|
|
2969
|
+
glob: {
|
|
2970
|
+
type: "string",
|
|
2971
|
+
description: "Optional file-name suffix or substring filter. Examples: '.ts' (only TypeScript), 'test' (any file with 'test' in the name). Reduces noise when you know the file shape."
|
|
2972
|
+
},
|
|
2973
|
+
case_sensitive: {
|
|
2974
|
+
type: "boolean",
|
|
2975
|
+
description: "When true, match case exactly. Default false (case-insensitive)."
|
|
2976
|
+
},
|
|
2977
|
+
include_deps: {
|
|
2978
|
+
type: "boolean",
|
|
2979
|
+
description: "When true, also search inside node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
|
|
2980
|
+
}
|
|
2981
|
+
},
|
|
2982
|
+
required: ["pattern"]
|
|
2983
|
+
},
|
|
2984
|
+
fn: async (args) => {
|
|
2985
|
+
const startAbs = safePath(args.path ?? ".");
|
|
2986
|
+
const caseSensitive = args.case_sensitive === true;
|
|
2987
|
+
const includeDeps = args.include_deps === true;
|
|
2988
|
+
const nameFilter = typeof args.glob === "string" ? args.glob.toLowerCase() : null;
|
|
2989
|
+
let re = null;
|
|
2990
|
+
try {
|
|
2991
|
+
re = new RegExp(args.pattern, caseSensitive ? "" : "i");
|
|
2992
|
+
} catch {
|
|
2993
|
+
re = null;
|
|
2994
|
+
}
|
|
2995
|
+
const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
|
|
2996
|
+
const matches = [];
|
|
2997
|
+
let totalBytes = 0;
|
|
2998
|
+
let scanned = 0;
|
|
2999
|
+
let truncated = false;
|
|
3000
|
+
const walk2 = async (dir) => {
|
|
3001
|
+
if (truncated) return;
|
|
3002
|
+
let entries;
|
|
3003
|
+
try {
|
|
3004
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
3005
|
+
} catch {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
for (const e of entries) {
|
|
3009
|
+
if (truncated) return;
|
|
3010
|
+
if (e.isDirectory()) {
|
|
3011
|
+
if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
|
|
3012
|
+
await walk2(pathMod.join(dir, e.name));
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
if (!e.isFile()) continue;
|
|
3016
|
+
if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
|
|
3017
|
+
if (isLikelyBinaryByName(e.name)) continue;
|
|
3018
|
+
const full = pathMod.join(dir, e.name);
|
|
3019
|
+
let stat;
|
|
3020
|
+
try {
|
|
3021
|
+
stat = await fs.stat(full);
|
|
3022
|
+
} catch {
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
if (stat.size > 2 * 1024 * 1024) continue;
|
|
3026
|
+
let raw;
|
|
3027
|
+
try {
|
|
3028
|
+
raw = await fs.readFile(full);
|
|
3029
|
+
} catch {
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
const firstNul = raw.indexOf(0);
|
|
3033
|
+
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
3034
|
+
const text = raw.toString("utf8");
|
|
3035
|
+
const rel = pathMod.relative(rootDir, full);
|
|
3036
|
+
const lines = text.split(/\r?\n/);
|
|
3037
|
+
for (let li = 0; li < lines.length; li++) {
|
|
3038
|
+
const line = lines[li];
|
|
3039
|
+
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
3040
|
+
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
3041
|
+
if (!hit) continue;
|
|
3042
|
+
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
3043
|
+
const out = `${rel}:${li + 1}: ${display}`;
|
|
3044
|
+
if (totalBytes + out.length + 1 > maxListBytes) {
|
|
3045
|
+
matches.push(`[\u2026 truncated at ${maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
3046
|
+
truncated = true;
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
matches.push(out);
|
|
3050
|
+
totalBytes += out.length + 1;
|
|
3051
|
+
}
|
|
3052
|
+
scanned++;
|
|
3053
|
+
}
|
|
3054
|
+
};
|
|
3055
|
+
await walk2(startAbs);
|
|
3056
|
+
if (matches.length === 0) {
|
|
3057
|
+
return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
|
|
3058
|
+
}
|
|
3059
|
+
return matches.join("\n");
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
2796
3062
|
registry.register({
|
|
2797
3063
|
name: "get_file_info",
|
|
2798
3064
|
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
@@ -3119,6 +3385,195 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
3119
3385
|
return registry;
|
|
3120
3386
|
}
|
|
3121
3387
|
|
|
3388
|
+
// src/tools/subagent.ts
|
|
3389
|
+
var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
|
|
3390
|
+
|
|
3391
|
+
Rules:
|
|
3392
|
+
- Stay on the task you were given. Do not expand scope.
|
|
3393
|
+
- Use tools as needed. You share the parent's sandbox + safety rules.
|
|
3394
|
+
- When you're done, your final assistant message is the only thing the parent will see \u2014 make it complete and self-contained. No follow-up offers, no questions, no "let me know if you need more."
|
|
3395
|
+
- Prefer one clear, distilled answer over a long log of what you tried.
|
|
3396
|
+
|
|
3397
|
+
Formatting rules (the parent renders your reply in a TUI with a real markdown renderer):
|
|
3398
|
+
- For tabular data use GitHub-Flavored Markdown tables with ASCII pipes: \`| col | col |\` headers, \`| --- | --- |\` separator. NEVER draw tables with Unicode box-drawing characters (\u2502 \u2500 \u253C \u250C \u2510 \u2514 \u2518 \u251C \u2524). They look intentional but break terminal word-wrap and produce garbled output.
|
|
3399
|
+
- Keep table cells short \u2014 one short phrase per cell, not multi-line paragraphs. If a description doesn't fit in ~40 chars, use bullets below the table instead.
|
|
3400
|
+
- Use fenced code blocks (\`\`\`) for any code, file paths with line ranges, or shell commands.
|
|
3401
|
+
- NEVER draw decorative frames around content with \`\u250C\u2500\u2500\u2510 \u2502 \u2514\u2500\u2500\u2518\` box-drawing characters. The renderer handles code blocks and headings on its own \u2014 extra ASCII art adds noise without value and breaks at narrow terminal widths.
|
|
3402
|
+
- For flow charts and diagrams: use a markdown bullet list with \`\u2192\` or \`\u2193\` between steps. Don't try to draw boxes-and-arrows in ASCII; it never survives word-wrap.`;
|
|
3403
|
+
var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
|
|
3404
|
+
var DEFAULT_MAX_ITERS = 16;
|
|
3405
|
+
var DEFAULT_SUBAGENT_MODEL = "deepseek-chat";
|
|
3406
|
+
var SUBAGENT_TOOL_NAME = "spawn_subagent";
|
|
3407
|
+
var NEVER_INHERITED_TOOLS = /* @__PURE__ */ new Set([SUBAGENT_TOOL_NAME, "submit_plan"]);
|
|
3408
|
+
async function spawnSubagent(opts) {
|
|
3409
|
+
const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
|
|
3410
|
+
const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
|
|
3411
|
+
const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
|
|
3412
|
+
const sink = opts.sink;
|
|
3413
|
+
const startedAt = Date.now();
|
|
3414
|
+
const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
|
|
3415
|
+
sink?.current?.({
|
|
3416
|
+
kind: "start",
|
|
3417
|
+
task: taskPreview,
|
|
3418
|
+
iter: 0,
|
|
3419
|
+
elapsedMs: 0
|
|
3420
|
+
});
|
|
3421
|
+
const childTools = forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
|
|
3422
|
+
const childPrefix = new ImmutablePrefix({
|
|
3423
|
+
system: opts.system,
|
|
3424
|
+
toolSpecs: childTools.specs()
|
|
3425
|
+
});
|
|
3426
|
+
const childLoop = new CacheFirstLoop({
|
|
3427
|
+
client: opts.client,
|
|
3428
|
+
prefix: childPrefix,
|
|
3429
|
+
tools: childTools,
|
|
3430
|
+
model,
|
|
3431
|
+
maxToolIters,
|
|
3432
|
+
hooks: [],
|
|
3433
|
+
stream: false
|
|
3434
|
+
});
|
|
3435
|
+
const onParentAbort = () => childLoop.abort();
|
|
3436
|
+
opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
3437
|
+
let final = "";
|
|
3438
|
+
let errorMessage;
|
|
3439
|
+
let toolIter = 0;
|
|
3440
|
+
try {
|
|
3441
|
+
for await (const ev of childLoop.step(opts.task)) {
|
|
3442
|
+
if (ev.role === "tool") {
|
|
3443
|
+
toolIter++;
|
|
3444
|
+
sink?.current?.({
|
|
3445
|
+
kind: "progress",
|
|
3446
|
+
task: taskPreview,
|
|
3447
|
+
iter: toolIter,
|
|
3448
|
+
elapsedMs: Date.now() - startedAt
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
if (ev.role === "assistant_final") {
|
|
3452
|
+
final = ev.content ?? "";
|
|
3453
|
+
}
|
|
3454
|
+
if (ev.role === "error") {
|
|
3455
|
+
errorMessage = ev.error ?? "subagent error";
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
} catch (err) {
|
|
3459
|
+
errorMessage = err.message;
|
|
3460
|
+
} finally {
|
|
3461
|
+
opts.parentSignal?.removeEventListener("abort", onParentAbort);
|
|
3462
|
+
}
|
|
3463
|
+
if (!errorMessage && !final) {
|
|
3464
|
+
errorMessage = opts.parentSignal?.aborted ? "subagent aborted before producing an answer" : "subagent ended without producing an answer";
|
|
3465
|
+
}
|
|
3466
|
+
const elapsedMs = Date.now() - startedAt;
|
|
3467
|
+
const turns = childLoop.stats.turns.length;
|
|
3468
|
+
const costUsd2 = childLoop.stats.totalCost;
|
|
3469
|
+
const truncated = final.length > maxResultChars ? `${final.slice(0, maxResultChars)}
|
|
3470
|
+
|
|
3471
|
+
[\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
|
|
3472
|
+
sink?.current?.({
|
|
3473
|
+
kind: "end",
|
|
3474
|
+
task: taskPreview,
|
|
3475
|
+
iter: toolIter,
|
|
3476
|
+
elapsedMs,
|
|
3477
|
+
summary: errorMessage ? void 0 : truncated.slice(0, 120),
|
|
3478
|
+
error: errorMessage,
|
|
3479
|
+
turns
|
|
3480
|
+
});
|
|
3481
|
+
return {
|
|
3482
|
+
success: !errorMessage,
|
|
3483
|
+
output: errorMessage ? "" : truncated,
|
|
3484
|
+
error: errorMessage,
|
|
3485
|
+
turns,
|
|
3486
|
+
toolIters: toolIter,
|
|
3487
|
+
elapsedMs,
|
|
3488
|
+
costUsd: costUsd2
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
function formatSubagentResult(r) {
|
|
3492
|
+
if (!r.success) {
|
|
3493
|
+
return JSON.stringify({
|
|
3494
|
+
success: false,
|
|
3495
|
+
error: r.error ?? "unknown subagent error",
|
|
3496
|
+
turns: r.turns,
|
|
3497
|
+
tool_iters: r.toolIters,
|
|
3498
|
+
elapsed_ms: r.elapsedMs
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
return JSON.stringify({
|
|
3502
|
+
success: true,
|
|
3503
|
+
output: r.output,
|
|
3504
|
+
turns: r.turns,
|
|
3505
|
+
tool_iters: r.toolIters,
|
|
3506
|
+
elapsed_ms: r.elapsedMs,
|
|
3507
|
+
cost_usd: r.costUsd
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
function registerSubagentTool(parentRegistry, opts) {
|
|
3511
|
+
const baseSystem = opts.defaultSystem ?? DEFAULT_SUBAGENT_SYSTEM;
|
|
3512
|
+
const defaultSystem = opts.projectRoot ? applyProjectMemory(baseSystem, opts.projectRoot) : baseSystem;
|
|
3513
|
+
const defaultModel = opts.defaultModel ?? DEFAULT_SUBAGENT_MODEL;
|
|
3514
|
+
const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
|
|
3515
|
+
const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
|
|
3516
|
+
const sink = opts.sink;
|
|
3517
|
+
parentRegistry.register({
|
|
3518
|
+
name: SUBAGENT_TOOL_NAME,
|
|
3519
|
+
description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. Use for: deep codebase exploration that would flood the main context, multi-step research where you only need the conclusion, or any focused subtask whose intermediate reasoning the user does not need to see. The subagent inherits all your tools (filesystem, shell, web, MCP, etc.) but runs in its own isolated message log \u2014 its tool calls and reasoning never enter your context. Only the final assistant message comes back as this tool's result. Keep tasks focused; the subagent has a stricter iter budget than you do.",
|
|
3520
|
+
parameters: {
|
|
3521
|
+
type: "object",
|
|
3522
|
+
properties: {
|
|
3523
|
+
task: {
|
|
3524
|
+
type: "string",
|
|
3525
|
+
description: "The subtask the subagent should perform. Be specific and self-contained \u2014 the subagent has none of your conversation context, only what you write here."
|
|
3526
|
+
},
|
|
3527
|
+
system: {
|
|
3528
|
+
type: "string",
|
|
3529
|
+
description: "Optional override for the subagent's system prompt. The default tells it to stay focused and return a concise answer; override only when the subtask needs a specialized persona."
|
|
3530
|
+
},
|
|
3531
|
+
model: {
|
|
3532
|
+
type: "string",
|
|
3533
|
+
enum: ["deepseek-chat", "deepseek-reasoner"],
|
|
3534
|
+
description: "Which DeepSeek model the subagent runs on. 'deepseek-chat' (V3) is the default \u2014 fast and cheap. Use 'deepseek-reasoner' (R1) only when the subtask genuinely needs planning or multi-step reasoning; it is roughly 5-10x more expensive."
|
|
3535
|
+
}
|
|
3536
|
+
},
|
|
3537
|
+
required: ["task"]
|
|
3538
|
+
},
|
|
3539
|
+
fn: async (args, ctx) => {
|
|
3540
|
+
const task = typeof args.task === "string" ? args.task.trim() : "";
|
|
3541
|
+
if (!task) {
|
|
3542
|
+
return JSON.stringify({
|
|
3543
|
+
error: "spawn_subagent requires a non-empty 'task' argument."
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : defaultSystem;
|
|
3547
|
+
const model = typeof args.model === "string" && args.model.startsWith("deepseek-") ? args.model : defaultModel;
|
|
3548
|
+
const result = await spawnSubagent({
|
|
3549
|
+
client: opts.client,
|
|
3550
|
+
parentRegistry,
|
|
3551
|
+
system,
|
|
3552
|
+
task,
|
|
3553
|
+
model,
|
|
3554
|
+
maxToolIters,
|
|
3555
|
+
maxResultChars,
|
|
3556
|
+
sink,
|
|
3557
|
+
parentSignal: ctx?.signal
|
|
3558
|
+
});
|
|
3559
|
+
return formatSubagentResult(result);
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3562
|
+
return parentRegistry;
|
|
3563
|
+
}
|
|
3564
|
+
function forkRegistryExcluding(parent, exclude) {
|
|
3565
|
+
const child = new ToolRegistry();
|
|
3566
|
+
for (const spec of parent.specs()) {
|
|
3567
|
+
const name = spec.function.name;
|
|
3568
|
+
if (exclude.has(name)) continue;
|
|
3569
|
+
const def = parent.get(name);
|
|
3570
|
+
if (!def) continue;
|
|
3571
|
+
child.register(def);
|
|
3572
|
+
}
|
|
3573
|
+
if (parent.planMode) child.setPlanMode(true);
|
|
3574
|
+
return child;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3122
3577
|
// src/tools/shell.ts
|
|
3123
3578
|
import { spawn as spawn2 } from "child_process";
|
|
3124
3579
|
import { existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
@@ -3316,7 +3771,7 @@ function prepareSpawn(argv, opts = {}) {
|
|
|
3316
3771
|
const cmdline = [resolved, ...tail].map(quoteForCmdExe).join(" ");
|
|
3317
3772
|
return {
|
|
3318
3773
|
bin: "cmd.exe",
|
|
3319
|
-
args: ["/d", "/s", "/c", cmdline],
|
|
3774
|
+
args: ["/d", "/s", "/c", withUtf8Codepage(cmdline)],
|
|
3320
3775
|
// windowsVerbatimArguments prevents Node from re-quoting the /c
|
|
3321
3776
|
// payload — we've already composed an exact cmd.exe command
|
|
3322
3777
|
// line. Without this Node wraps our already-quoted string in
|
|
@@ -3328,12 +3783,36 @@ function prepareSpawn(argv, opts = {}) {
|
|
|
3328
3783
|
const cmdline = [head, ...tail].map(quoteForCmdExe).join(" ");
|
|
3329
3784
|
return {
|
|
3330
3785
|
bin: "cmd.exe",
|
|
3331
|
-
args: ["/d", "/s", "/c", cmdline],
|
|
3786
|
+
args: ["/d", "/s", "/c", withUtf8Codepage(cmdline)],
|
|
3332
3787
|
spawnOverrides: { windowsVerbatimArguments: true }
|
|
3333
3788
|
};
|
|
3334
3789
|
}
|
|
3790
|
+
if (isPowerShellExe(resolved)) {
|
|
3791
|
+
const patched = injectPowerShellUtf8(tail);
|
|
3792
|
+
if (patched) {
|
|
3793
|
+
return { bin: resolved, args: patched, spawnOverrides: {} };
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3335
3796
|
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
3336
3797
|
}
|
|
3798
|
+
function isPowerShellExe(resolved) {
|
|
3799
|
+
return /(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?$/i.test(resolved);
|
|
3800
|
+
}
|
|
3801
|
+
function injectPowerShellUtf8(args) {
|
|
3802
|
+
const prelude = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;$OutputEncoding=[System.Text.Encoding]::UTF8;";
|
|
3803
|
+
for (let i = 0; i < args.length; i++) {
|
|
3804
|
+
const a = args[i] ?? "";
|
|
3805
|
+
if (/^-(?:Command|c)$/i.test(a) && i + 1 < args.length) {
|
|
3806
|
+
const out = [...args];
|
|
3807
|
+
out[i + 1] = `${prelude}${args[i + 1] ?? ""}`;
|
|
3808
|
+
return out;
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
function withUtf8Codepage(cmdline) {
|
|
3814
|
+
return `chcp 65001 >nul & ${cmdline}`;
|
|
3815
|
+
}
|
|
3337
3816
|
function isBareWindowsName(s) {
|
|
3338
3817
|
if (!s) return false;
|
|
3339
3818
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
@@ -4882,6 +5361,26 @@ The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit con
|
|
|
4882
5361
|
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
4883
5362
|
|
|
4884
5363
|
|
|
5364
|
+
# Delegating to subagents via Skills (\u{1F9EC})
|
|
5365
|
+
|
|
5366
|
+
The pinned Skills index below lists playbooks you can invoke with \`run_skill\`. Skills marked with **\u{1F9EC}** spawn an **isolated subagent** \u2014 a fresh child loop that runs the playbook in its own context and returns only the final answer. The subagent's tool calls and reasoning never enter your context, so \u{1F9EC} skills are how you keep the main session lean.
|
|
5367
|
+
|
|
5368
|
+
Two built-ins ship by default:
|
|
5369
|
+
- **\u{1F9EC} explore** \u2014 read-only investigation across the codebase. Use when the user says things like "find all places that...", "how does X work across the project", "survey the code for Y". Pass \`arguments\` describing the concrete question.
|
|
5370
|
+
- **\u{1F9EC} research** \u2014 combines web search + code reading. Use for "is X supported by lib Y", "what's the canonical way to Z", "compare our impl to the spec".
|
|
5371
|
+
|
|
5372
|
+
When to delegate (call \`run_skill\` with a \u{1F9EC} skill):
|
|
5373
|
+
- The task would otherwise need >5 file reads or searches.
|
|
5374
|
+
- You only need the conclusion, not the exploration trail.
|
|
5375
|
+
- The work is self-contained (you can describe it in one paragraph).
|
|
5376
|
+
|
|
5377
|
+
When NOT to delegate:
|
|
5378
|
+
- Direct, narrow questions answerable in 1-2 tool calls \u2014 just do them.
|
|
5379
|
+
- Anything where you need to track intermediate results yourself (planning, multi-step edits).
|
|
5380
|
+
- Anything that requires user interaction (subagents can't submit plans or ask you for clarification).
|
|
5381
|
+
|
|
5382
|
+
Always pass a clear, self-contained \`arguments\` \u2014 that text is the **only** context the subagent gets.
|
|
5383
|
+
|
|
4885
5384
|
# When to edit vs. when to explore
|
|
4886
5385
|
|
|
4887
5386
|
Only propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:
|
|
@@ -4923,13 +5422,21 @@ Before exploring the filesystem to answer a factual question, check whether the
|
|
|
4923
5422
|
# Exploration
|
|
4924
5423
|
|
|
4925
5424
|
- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
|
|
4926
|
-
- Prefer search_files
|
|
5425
|
+
- Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
|
|
5426
|
+
- Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
|
|
5427
|
+
|
|
5428
|
+
# Path conventions
|
|
5429
|
+
|
|
5430
|
+
Two different rules depending on which tool:
|
|
5431
|
+
|
|
5432
|
+
- **Filesystem tools** (\`read_file\`, \`list_directory\`, \`search_files\`, \`edit_file\`, etc.): paths are sandbox-relative. \`/\` means the project root, \`/src/foo.ts\` means \`<project>/src/foo.ts\`. Both relative (\`src/foo.ts\`) and POSIX-absolute (\`/src/foo.ts\`) forms work.
|
|
5433
|
+
- **\`run_command\`**: the command runs in a real OS shell with cwd pinned to the project root. Paths inside the shell command are interpreted by THAT shell, not by us. **Never use leading \`/\` in run_command arguments** \u2014 Windows treats \`/tests\` as drive-root \`F:\\tests\` (non-existent), POSIX shells treat it as filesystem root. Use plain relative paths (\`tests\`, \`./tests\`, \`src/loop.ts\`) instead.
|
|
4927
5434
|
|
|
4928
5435
|
# Style
|
|
4929
5436
|
|
|
4930
5437
|
- Show edits; don't narrate them in prose. "Here's the fix:" is enough.
|
|
4931
5438
|
- One short paragraph explaining *why*, then the blocks.
|
|
4932
|
-
- If you need to explore first (list /
|
|
5439
|
+
- If you need to explore first (list / read / search), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
|
|
4933
5440
|
`;
|
|
4934
5441
|
function codeSystemPrompt(rootDir) {
|
|
4935
5442
|
const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
|
|
@@ -5284,6 +5791,7 @@ export {
|
|
|
5284
5791
|
fetchWithRetry,
|
|
5285
5792
|
flattenMcpResult,
|
|
5286
5793
|
flattenSchema,
|
|
5794
|
+
forkRegistryExcluding,
|
|
5287
5795
|
formatCommandResult,
|
|
5288
5796
|
formatHookOutcomeMessage,
|
|
5289
5797
|
formatLogSize,
|
|
@@ -5294,6 +5802,7 @@ export {
|
|
|
5294
5802
|
harvest,
|
|
5295
5803
|
healLoadedMessages,
|
|
5296
5804
|
htmlToText,
|
|
5805
|
+
injectPowerShellUtf8,
|
|
5297
5806
|
inputCostUsd,
|
|
5298
5807
|
inspectMcpServer,
|
|
5299
5808
|
isAllowed,
|
|
@@ -5329,6 +5838,7 @@ export {
|
|
|
5329
5838
|
registerMemoryTools,
|
|
5330
5839
|
registerPlanTool,
|
|
5331
5840
|
registerShellTools,
|
|
5841
|
+
registerSubagentTool,
|
|
5332
5842
|
registerWebTools,
|
|
5333
5843
|
renderMarkdown as renderDiffMarkdown,
|
|
5334
5844
|
renderSummaryTable as renderDiffSummary,
|
|
@@ -5352,6 +5862,7 @@ export {
|
|
|
5352
5862
|
truncateForModel,
|
|
5353
5863
|
webFetch,
|
|
5354
5864
|
webSearch,
|
|
5865
|
+
withUtf8Codepage,
|
|
5355
5866
|
writeConfig,
|
|
5356
5867
|
writeMeta,
|
|
5357
5868
|
writeRecord
|