reasonix 0.4.23 → 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/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. Sorted by name
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 max = 130 - s.name.length;
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 user-defined prompt packs",
2353
+ "# Skills \u2014 playbooks you can invoke",
2332
2354
  "",
2333
- 'One-liner index. Each skill is a self-contained instruction block (plus optional tool hints) the user or an earlier session saved. To load the full body, call `run_skill({ name: "<skill-name>" })` \u2014 the body is NOT in this prompt, only the name and description are. The user can also invoke a skill directly as `/skill <name>`.',
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
- const resolved = pathMod.resolve(rootDir, raw);
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 / grep over list_directory when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees.
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 / grep / read), do it with tool calls before writing any prose \u2014 silence while exploring is fine.
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);
@@ -5099,6 +5606,132 @@ function isNpxInstall() {
5099
5606
  if (ua.includes("npx/")) return true;
5100
5607
  return false;
5101
5608
  }
5609
+
5610
+ // src/usage.ts
5611
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
5612
+ import { homedir as homedir7 } from "os";
5613
+ import { dirname as dirname6, join as join10 } from "path";
5614
+ function defaultUsageLogPath(homeDirOverride) {
5615
+ return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5616
+ }
5617
+ function appendUsage(input) {
5618
+ const record = {
5619
+ ts: input.now ?? Date.now(),
5620
+ session: input.session,
5621
+ model: input.model,
5622
+ promptTokens: input.usage.promptTokens,
5623
+ completionTokens: input.usage.completionTokens,
5624
+ cacheHitTokens: input.usage.promptCacheHitTokens,
5625
+ cacheMissTokens: input.usage.promptCacheMissTokens,
5626
+ costUsd: costUsd(input.model, input.usage),
5627
+ claudeEquivUsd: claudeEquivalentCost(input.usage)
5628
+ };
5629
+ const path = input.path ?? defaultUsageLogPath();
5630
+ try {
5631
+ mkdirSync6(dirname6(path), { recursive: true });
5632
+ appendFileSync2(path, `${JSON.stringify(record)}
5633
+ `, "utf8");
5634
+ } catch {
5635
+ }
5636
+ return record;
5637
+ }
5638
+ function readUsageLog(path = defaultUsageLogPath()) {
5639
+ if (!existsSync10(path)) return [];
5640
+ let raw;
5641
+ try {
5642
+ raw = readFileSync12(path, "utf8");
5643
+ } catch {
5644
+ return [];
5645
+ }
5646
+ const out = [];
5647
+ for (const line of raw.split(/\r?\n/)) {
5648
+ if (!line.trim()) continue;
5649
+ try {
5650
+ const rec = JSON.parse(line);
5651
+ if (isValidRecord(rec)) out.push(rec);
5652
+ } catch {
5653
+ }
5654
+ }
5655
+ return out;
5656
+ }
5657
+ function isValidRecord(rec) {
5658
+ if (!rec || typeof rec !== "object") return false;
5659
+ const r = rec;
5660
+ return typeof r.ts === "number" && typeof r.model === "string" && typeof r.promptTokens === "number" && typeof r.completionTokens === "number" && typeof r.cacheHitTokens === "number" && typeof r.cacheMissTokens === "number" && typeof r.costUsd === "number" && typeof r.claudeEquivUsd === "number";
5661
+ }
5662
+ function bucketCacheHitRatio(b) {
5663
+ const denom = b.cacheHitTokens + b.cacheMissTokens;
5664
+ return denom > 0 ? b.cacheHitTokens / denom : 0;
5665
+ }
5666
+ function bucketSavingsFraction(b) {
5667
+ return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;
5668
+ }
5669
+ function emptyBucket(label, since) {
5670
+ return {
5671
+ label,
5672
+ since,
5673
+ turns: 0,
5674
+ promptTokens: 0,
5675
+ completionTokens: 0,
5676
+ cacheHitTokens: 0,
5677
+ cacheMissTokens: 0,
5678
+ costUsd: 0,
5679
+ claudeEquivUsd: 0
5680
+ };
5681
+ }
5682
+ function addToBucket(b, r) {
5683
+ b.turns += 1;
5684
+ b.promptTokens += r.promptTokens;
5685
+ b.completionTokens += r.completionTokens;
5686
+ b.cacheHitTokens += r.cacheHitTokens;
5687
+ b.cacheMissTokens += r.cacheMissTokens;
5688
+ b.costUsd += r.costUsd;
5689
+ b.claudeEquivUsd += r.claudeEquivUsd;
5690
+ }
5691
+ function aggregateUsage(records, opts = {}) {
5692
+ const now = opts.now ?? Date.now();
5693
+ const day = 24 * 60 * 60 * 1e3;
5694
+ const today = emptyBucket("today", now - day);
5695
+ const week = emptyBucket("week", now - 7 * day);
5696
+ const month = emptyBucket("month", now - 30 * day);
5697
+ const all = emptyBucket("all-time", 0);
5698
+ const modelCounts = /* @__PURE__ */ new Map();
5699
+ const sessionCounts = /* @__PURE__ */ new Map();
5700
+ let firstSeen = null;
5701
+ let lastSeen = null;
5702
+ for (const r of records) {
5703
+ addToBucket(all, r);
5704
+ if (r.ts >= today.since) addToBucket(today, r);
5705
+ if (r.ts >= week.since) addToBucket(week, r);
5706
+ if (r.ts >= month.since) addToBucket(month, r);
5707
+ modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
5708
+ const sessKey = r.session ?? "(ephemeral)";
5709
+ sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
5710
+ if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
5711
+ if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
5712
+ }
5713
+ const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
5714
+ const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
5715
+ return {
5716
+ buckets: [today, week, month, all],
5717
+ byModel,
5718
+ bySession,
5719
+ firstSeen,
5720
+ lastSeen
5721
+ };
5722
+ }
5723
+ function formatLogSize(path = defaultUsageLogPath()) {
5724
+ if (!existsSync10(path)) return "";
5725
+ try {
5726
+ const s = statSync4(path);
5727
+ const bytes = s.size;
5728
+ if (bytes < 1024) return `${bytes} B`;
5729
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5730
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5731
+ } catch {
5732
+ return "";
5733
+ }
5734
+ }
5102
5735
  export {
5103
5736
  AppendOnlyLog,
5104
5737
  CODE_SYSTEM_PROMPT,
@@ -5131,14 +5764,18 @@ export {
5131
5764
  VERSION,
5132
5765
  VolatileScratch,
5133
5766
  aggregateBranchUsage,
5767
+ aggregateUsage,
5134
5768
  analyzeSchema,
5135
5769
  appendSessionMessage,
5770
+ appendUsage,
5136
5771
  applyEditBlock,
5137
5772
  applyEditBlocks,
5138
5773
  applyMemoryStack,
5139
5774
  applyProjectMemory,
5140
5775
  applyUserMemory,
5141
5776
  bridgeMcpTools,
5777
+ bucketCacheHitRatio,
5778
+ bucketSavingsFraction,
5142
5779
  claudeEquivalentCost,
5143
5780
  codeSystemPrompt,
5144
5781
  compareVersions,
@@ -5147,14 +5784,17 @@ export {
5147
5784
  decideOutcome,
5148
5785
  defaultConfigPath,
5149
5786
  defaultSelector,
5787
+ defaultUsageLogPath,
5150
5788
  deleteSession,
5151
5789
  diffTranscripts,
5152
5790
  emptyPlanState,
5153
5791
  fetchWithRetry,
5154
5792
  flattenMcpResult,
5155
5793
  flattenSchema,
5794
+ forkRegistryExcluding,
5156
5795
  formatCommandResult,
5157
5796
  formatHookOutcomeMessage,
5797
+ formatLogSize,
5158
5798
  formatLoopError,
5159
5799
  formatSearchResults,
5160
5800
  getLatestVersion,
@@ -5162,6 +5802,7 @@ export {
5162
5802
  harvest,
5163
5803
  healLoadedMessages,
5164
5804
  htmlToText,
5805
+ injectPowerShellUtf8,
5165
5806
  inputCostUsd,
5166
5807
  inspectMcpServer,
5167
5808
  isAllowed,
@@ -5190,12 +5831,14 @@ export {
5190
5831
  readConfig,
5191
5832
  readProjectMemory,
5192
5833
  readTranscript,
5834
+ readUsageLog,
5193
5835
  recordFromLoopEvent,
5194
5836
  redactKey,
5195
5837
  registerFilesystemTools,
5196
5838
  registerMemoryTools,
5197
5839
  registerPlanTool,
5198
5840
  registerShellTools,
5841
+ registerSubagentTool,
5199
5842
  registerWebTools,
5200
5843
  renderMarkdown as renderDiffMarkdown,
5201
5844
  renderSummaryTable as renderDiffSummary,
@@ -5219,6 +5862,7 @@ export {
5219
5862
  truncateForModel,
5220
5863
  webFetch,
5221
5864
  webSearch,
5865
+ withUtf8Codepage,
5222
5866
  writeConfig,
5223
5867
  writeMeta,
5224
5868
  writeRecord