reasonix 0.27.3 → 0.29.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.
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  CODE_SYSTEM_PROMPT,
4
4
  codeSystemPrompt
5
- } from "./chunk-R2L5YEEF.js";
5
+ } from "./chunk-COFBA5FV.js";
6
6
  export {
7
7
  CODE_SYSTEM_PROMPT,
8
8
  codeSystemPrompt
9
9
  };
10
- //# sourceMappingURL=prompt-YUL7CYKY.js.map
10
+ //# sourceMappingURL=prompt-VF7B6BWR.js.map
package/dist/index.d.ts CHANGED
@@ -694,6 +694,8 @@ interface ToolDefinition<A = any, R = any> {
694
694
  readOnly?: boolean;
695
695
  /** Per-args check; takes precedence over `readOnly`. e.g. `run_command` + allowlisted argv. */
696
696
  readOnlyCheck?: (args: A) => boolean;
697
+ /** Safe to dispatch concurrently with other parallel-safe calls in the same turn. Default false — opt-in only. */
698
+ parallelSafe?: boolean;
697
699
  fn: (args: A, ctx?: ToolCallContext) => R | Promise<R>;
698
700
  }
699
701
  interface ToolRegistryOptions {
@@ -729,6 +731,8 @@ declare class ToolRegistry {
729
731
  get size(): number;
730
732
  /** True if a registered tool's schema was flattened for the model. */
731
733
  wasFlattened(name: string): boolean;
734
+ /** Unknown / unannotated tools default to false — third-party MCP tools must opt in. */
735
+ isParallelSafe(name: string): boolean;
732
736
  specs(): ToolSpec[];
733
737
  dispatch(name: string, argumentsRaw: string | Record<string, unknown>, opts?: {
734
738
  signal?: AbortSignal;
@@ -840,6 +844,7 @@ declare class CacheFirstLoop {
840
844
  private modelForCurrentCall;
841
845
  /** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
842
846
  private noteToolFailureSignal;
847
+ private runOneToolCall;
843
848
  private buildMessages;
844
849
  abort(): void;
845
850
  /** Drop the last user message + everything after; caller re-sends. Persists to session file. */
@@ -1102,6 +1107,8 @@ declare function registerPlanTool(registry: ToolRegistry, opts?: PlanToolOptions
1102
1107
  /** Side-channel — subagents run inside a tool-dispatch frame, can't go through parent's `LoopEvent` stream. */
1103
1108
  interface SubagentEvent {
1104
1109
  kind: "start" | "progress" | "end" | "inner" | "phase";
1110
+ /** Stable per-spawn id; lets the UI key parallel runs apart instead of overwriting one shared row. */
1111
+ runId: string;
1105
1112
  task: string;
1106
1113
  skillName?: string;
1107
1114
  model?: string;
package/dist/index.js CHANGED
@@ -1046,6 +1046,10 @@ var ToolRegistry = class {
1046
1046
  wasFlattened(name) {
1047
1047
  return Boolean(this._tools.get(name)?.flatSchema);
1048
1048
  }
1049
+ /** Unknown / unannotated tools default to false — third-party MCP tools must opt in. */
1050
+ isParallelSafe(name) {
1051
+ return this._tools.get(name)?.parallelSafe === true;
1052
+ }
1049
1053
  specs() {
1050
1054
  return [...this._tools.values()].map((t) => ({
1051
1055
  type: "function",
@@ -2640,6 +2644,48 @@ var CacheFirstLoop = class {
2640
2644
  this._escalateThisTurn = true;
2641
2645
  return true;
2642
2646
  }
2647
+ async runOneToolCall(call, signal) {
2648
+ const name = call.function?.name ?? "";
2649
+ const args = call.function?.arguments ?? "{}";
2650
+ const parsedArgs = safeParseToolArgs(args);
2651
+ const preReport = await runHooks({
2652
+ hooks: this.hooks,
2653
+ payload: {
2654
+ event: "PreToolUse",
2655
+ cwd: this.hookCwd,
2656
+ toolName: name,
2657
+ toolArgs: parsedArgs
2658
+ }
2659
+ });
2660
+ const preWarnings = [...hookWarnings(preReport.outcomes, this._turn)];
2661
+ if (preReport.blocked) {
2662
+ const blocking = preReport.outcomes[preReport.outcomes.length - 1];
2663
+ const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
2664
+ return {
2665
+ preWarnings,
2666
+ postWarnings: [],
2667
+ result: `[hook block] ${blocking?.hook.command ?? "<unknown>"}
2668
+ ${reason}`
2669
+ };
2670
+ }
2671
+ const result = await this.tools.dispatch(name, args, {
2672
+ signal,
2673
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
2674
+ confirmationGate: this.confirmationGate
2675
+ });
2676
+ const postReport = await runHooks({
2677
+ hooks: this.hooks,
2678
+ payload: {
2679
+ event: "PostToolUse",
2680
+ cwd: this.hookCwd,
2681
+ toolName: name,
2682
+ toolArgs: parsedArgs,
2683
+ toolResult: result
2684
+ }
2685
+ });
2686
+ const postWarnings = [...hookWarnings(postReport.outcomes, this._turn)];
2687
+ return { preWarnings, postWarnings, result };
2688
+ }
2643
2689
  buildMessages(pendingUser) {
2644
2690
  const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
2645
2691
  const msgs = [...this.prefix.toMessages(), ...healed.messages];
@@ -3131,71 +3177,69 @@ var CacheFirstLoop = class {
3131
3177
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
3132
3178
  return;
3133
3179
  }
3134
- for (const call of repairedCalls) {
3135
- const name = call.function?.name ?? "";
3136
- const args = call.function?.arguments ?? "{}";
3137
- yield {
3138
- turn: this._turn,
3139
- role: "tool_start",
3140
- content: "",
3141
- toolName: name,
3142
- toolArgs: args
3143
- };
3144
- const parsedArgs = safeParseToolArgs(args);
3145
- const preReport = await runHooks({
3146
- hooks: this.hooks,
3147
- payload: {
3148
- event: "PreToolUse",
3149
- cwd: this.hookCwd,
3150
- toolName: name,
3151
- toolArgs: parsedArgs
3180
+ const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
3181
+ const parallelMaxParsed = Number.parseInt(process.env.REASONIX_PARALLEL_MAX ?? "", 10);
3182
+ const parallelMax = Number.isFinite(parallelMaxParsed) && parallelMaxParsed >= 1 ? Math.min(parallelMaxParsed, 16) : 3;
3183
+ let callIdx = 0;
3184
+ while (callIdx < repairedCalls.length) {
3185
+ const chunk = [];
3186
+ if (!dispatchSerial) {
3187
+ while (callIdx < repairedCalls.length && chunk.length < parallelMax && this.tools.isParallelSafe(repairedCalls[callIdx]?.function?.name ?? "")) {
3188
+ chunk.push(repairedCalls[callIdx++]);
3152
3189
  }
3153
- });
3154
- for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
3155
- let result;
3156
- if (preReport.blocked) {
3157
- const blocking = preReport.outcomes[preReport.outcomes.length - 1];
3158
- const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
3159
- result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
3160
- ${reason}`;
3161
- } else {
3162
- result = await this.tools.dispatch(name, args, {
3163
- signal,
3164
- maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
3165
- confirmationGate: this.confirmationGate
3166
- });
3167
- const postReport = await runHooks({
3168
- hooks: this.hooks,
3169
- payload: {
3170
- event: "PostToolUse",
3171
- cwd: this.hookCwd,
3172
- toolName: name,
3173
- toolArgs: parsedArgs,
3174
- toolResult: result
3175
- }
3176
- });
3177
- for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
3178
3190
  }
3179
- this.appendAndPersist({
3180
- role: "tool",
3181
- tool_call_id: call.id ?? "",
3182
- name,
3183
- content: result
3184
- });
3185
- if (this.noteToolFailureSignal(result)) {
3191
+ if (chunk.length === 0) {
3192
+ chunk.push(repairedCalls[callIdx++]);
3193
+ }
3194
+ for (const call of chunk) {
3186
3195
  yield {
3187
3196
  turn: this._turn,
3188
- role: "warning",
3189
- content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
3197
+ role: "tool_start",
3198
+ content: "",
3199
+ toolName: call.function?.name ?? "",
3200
+ toolArgs: call.function?.arguments ?? "{}"
3201
+ };
3202
+ }
3203
+ const settled = await Promise.allSettled(chunk.map((c) => this.runOneToolCall(c, signal)));
3204
+ for (let k = 0; k < chunk.length; k++) {
3205
+ const call = chunk[k];
3206
+ const name = call.function?.name ?? "";
3207
+ const args = call.function?.arguments ?? "{}";
3208
+ const s = settled[k];
3209
+ let result;
3210
+ let preWarnings = [];
3211
+ let postWarnings = [];
3212
+ if (s.status === "fulfilled") {
3213
+ preWarnings = s.value.preWarnings;
3214
+ postWarnings = s.value.postWarnings;
3215
+ result = s.value.result;
3216
+ } else {
3217
+ const err = s.reason instanceof Error ? s.reason : new Error(String(s.reason));
3218
+ result = JSON.stringify({ error: `${err.name}: ${err.message}` });
3219
+ }
3220
+ for (const w of preWarnings) yield w;
3221
+ for (const w of postWarnings) yield w;
3222
+ this.appendAndPersist({
3223
+ role: "tool",
3224
+ tool_call_id: call.id ?? "",
3225
+ name,
3226
+ content: result
3227
+ });
3228
+ if (this.noteToolFailureSignal(result)) {
3229
+ yield {
3230
+ turn: this._turn,
3231
+ role: "warning",
3232
+ content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
3233
+ };
3234
+ }
3235
+ yield {
3236
+ turn: this._turn,
3237
+ role: "tool",
3238
+ content: result,
3239
+ toolName: name,
3240
+ toolArgs: args
3190
3241
  };
3191
3242
  }
3192
- yield {
3193
- turn: this._turn,
3194
- role: "tool",
3195
- content: result,
3196
- toolName: name,
3197
- toolArgs: args
3198
- };
3199
3243
  }
3200
3244
  }
3201
3245
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
@@ -3636,6 +3680,11 @@ function parseFrontmatter(raw) {
3636
3680
  function isValidSkillName(name) {
3637
3681
  return VALID_SKILL_NAME.test(name);
3638
3682
  }
3683
+ function parseAllowedTools(raw) {
3684
+ if (raw === void 0) return void 0;
3685
+ const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
3686
+ return names.length > 0 ? Object.freeze(names) : void 0;
3687
+ }
3639
3688
  var SkillStore = class {
3640
3689
  homeDir;
3641
3690
  projectRoot;
@@ -3735,7 +3784,7 @@ var SkillStore = class {
3735
3784
  body: body.trim(),
3736
3785
  scope,
3737
3786
  path: path2,
3738
- allowedTools: data["allowed-tools"],
3787
+ allowedTools: parseAllowedTools(data["allowed-tools"]),
3739
3788
  runAs: parseRunAs(data.runAs),
3740
3789
  model: data.model?.startsWith("deepseek-") ? data.model : void 0
3741
3790
  };
@@ -4599,6 +4648,7 @@ function registerFilesystemTools(registry, opts) {
4599
4648
  };
4600
4649
  registry.register({
4601
4650
  name: "read_file",
4651
+ parallelSafe: true,
4602
4652
  description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
4603
4653
  - head: N \u2192 first N lines (imports, public API, small configs)
4604
4654
  - tail: N \u2192 last N lines (recently-added code, log tails)
@@ -4682,6 +4732,7 @@ ${slice.join("\n")}`;
4682
4732
  });
4683
4733
  registry.register({
4684
4734
  name: "list_directory",
4735
+ parallelSafe: true,
4685
4736
  description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
4686
4737
  readOnly: true,
4687
4738
  parameters: {
@@ -4702,6 +4753,7 @@ ${slice.join("\n")}`;
4702
4753
  });
4703
4754
  registry.register({
4704
4755
  name: "directory_tree",
4756
+ parallelSafe: true,
4705
4757
  description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
4706
4758
  - maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
4707
4759
  - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
@@ -4780,6 +4832,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4780
4832
  });
4781
4833
  registry.register({
4782
4834
  name: "search_files",
4835
+ parallelSafe: true,
4783
4836
  description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
4784
4837
  readOnly: true,
4785
4838
  parameters: {
@@ -4805,6 +4858,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4805
4858
  });
4806
4859
  registry.register({
4807
4860
  name: "search_content",
4861
+ parallelSafe: true,
4808
4862
  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.",
4809
4863
  readOnly: true,
4810
4864
  parameters: {
@@ -4847,6 +4901,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4847
4901
  });
4848
4902
  registry.register({
4849
4903
  name: "get_file_info",
4904
+ parallelSafe: true,
4850
4905
  description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
4851
4906
  readOnly: true,
4852
4907
  parameters: {
@@ -5027,6 +5082,7 @@ function registerMemoryTools(registry, opts = {}) {
5027
5082
  name: "recall_memory",
5028
5083
  description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
5029
5084
  readOnly: true,
5085
+ parallelSafe: true,
5030
5086
  parameters: {
5031
5087
  type: "object",
5032
5088
  properties: {
@@ -5408,7 +5464,56 @@ function registerPlanTool(registry, opts = {}) {
5408
5464
  return registry;
5409
5465
  }
5410
5466
 
5467
+ // src/tools/subagent-types.ts
5468
+ var EXPLORE_SYSTEM = `You are an exploration subagent. Wide-net read-only investigation; return one distilled answer.
5469
+
5470
+ How to operate:
5471
+ - Read-only tools only (read_file, search_files, search_content, directory_tree, list_directory, get_file_info).
5472
+ - For "find all places that call / reference / use X" \u2014 use search_content (content grep), NOT search_files (which only matches names).
5473
+ - Cast a wide net first to map the territory, then read the 3-10 most relevant files in full. Stop as soon as you can answer.
5474
+ - The parent does not see your tool calls \u2014 over-exploration is pure waste.
5475
+
5476
+ Final answer:
5477
+ - One paragraph or short bullets; lead with the conclusion.
5478
+ - Cite file:line ranges when they back the claim.
5479
+ - No follow-up offers, no "let me know if you need more" \u2014 the parent will ask again.
5480
+
5481
+ ${NEGATIVE_CLAIM_RULE}
5482
+
5483
+ ${TUI_FORMATTING_RULES}`;
5484
+ var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES / NO / INCONCLUSIVE with evidence. Do not expand scope.
5485
+
5486
+ How to operate:
5487
+ - Read only what's needed to verify the specific claim. No exploration past the claim.
5488
+ - Use search_content / read_file to confirm the exact behavior, type, or call site in question.
5489
+ - Cap at 6-8 tool calls. If you can't verify in that, return INCONCLUSIVE plus what's missing.
5490
+
5491
+ Final answer:
5492
+ - Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
5493
+ - Cite file:line for the evidence.
5494
+ - One paragraph or a few bullets. No follow-up offers.
5495
+
5496
+ ${NEGATIVE_CLAIM_RULE}
5497
+
5498
+ ${TUI_FORMATTING_RULES}`;
5499
+ var TYPES = {
5500
+ explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
5501
+ verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
5502
+ };
5503
+ var SUBAGENT_TYPE_NAMES = Object.freeze(
5504
+ Object.keys(TYPES)
5505
+ );
5506
+ function getSubagentType(name) {
5507
+ if (typeof name !== "string") return void 0;
5508
+ return TYPES[name];
5509
+ }
5510
+
5411
5511
  // src/tools/subagent.ts
5512
+ var runIdCounter = 0;
5513
+ function nextRunId() {
5514
+ runIdCounter++;
5515
+ return `sub-${runIdCounter.toString(36)}`;
5516
+ }
5412
5517
  var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
5413
5518
 
5414
5519
  Rules:
@@ -5424,6 +5529,8 @@ ${ESCALATION_CONTRACT}
5424
5529
  ${TUI_FORMATTING_RULES}`;
5425
5530
  var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
5426
5531
  var DEFAULT_MAX_ITERS = 16;
5532
+ var MIN_MAX_ITERS = 1;
5533
+ var MAX_MAX_ITERS = 32;
5427
5534
  var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
5428
5535
  var DEFAULT_SUBAGENT_EFFORT = "high";
5429
5536
  var SUBAGENT_TOOL_NAME = "spawn_subagent";
@@ -5435,16 +5542,53 @@ async function spawnSubagent(opts) {
5435
5542
  const sink = opts.sink;
5436
5543
  const skillName = opts.skillName;
5437
5544
  const startedAt = Date.now();
5545
+ const runId = nextRunId();
5438
5546
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
5439
5547
  sink?.current?.({
5440
5548
  kind: "start",
5549
+ runId,
5441
5550
  task: taskPreview,
5442
5551
  skillName,
5443
5552
  model,
5444
5553
  iter: 0,
5445
5554
  elapsedMs: 0
5446
5555
  });
5447
- const childTools = forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
5556
+ if (opts.allowedTools) {
5557
+ const missing = opts.allowedTools.filter((n) => !opts.parentRegistry.has(n));
5558
+ if (missing.length > 0) {
5559
+ const errorMessage2 = `subagent allow-list names tool(s) not registered in the parent: ${missing.join(", ")}. Fix the skill's \`allowed-tools\` frontmatter or check spelling.`;
5560
+ sink?.current?.({
5561
+ kind: "end",
5562
+ runId,
5563
+ task: taskPreview,
5564
+ skillName,
5565
+ model,
5566
+ iter: 0,
5567
+ elapsedMs: Date.now() - startedAt,
5568
+ error: errorMessage2,
5569
+ turns: 0,
5570
+ costUsd: 0,
5571
+ usage: new Usage()
5572
+ });
5573
+ return {
5574
+ success: false,
5575
+ output: "",
5576
+ error: errorMessage2,
5577
+ turns: 0,
5578
+ toolIters: 0,
5579
+ elapsedMs: Date.now() - startedAt,
5580
+ costUsd: 0,
5581
+ model,
5582
+ skillName,
5583
+ usage: new Usage()
5584
+ };
5585
+ }
5586
+ }
5587
+ const childTools = opts.allowedTools ? forkRegistryWithAllowList(
5588
+ opts.parentRegistry,
5589
+ new Set(opts.allowedTools),
5590
+ NEVER_INHERITED_TOOLS
5591
+ ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
5448
5592
  const childPrefix = new ImmutablePrefix({
5449
5593
  system: opts.system,
5450
5594
  toolSpecs: childTools.specs()
@@ -5477,12 +5621,13 @@ async function spawnSubagent(opts) {
5477
5621
  let summarisingEmitted = false;
5478
5622
  try {
5479
5623
  for await (const ev of childLoop.step(opts.task)) {
5480
- sink?.current?.({ kind: "inner", task: taskPreview, skillName, model, inner: ev });
5624
+ sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
5481
5625
  if (ev.role === "tool") {
5482
5626
  toolIter++;
5483
5627
  summarisingEmitted = false;
5484
5628
  sink?.current?.({
5485
5629
  kind: "progress",
5630
+ runId,
5486
5631
  task: taskPreview,
5487
5632
  skillName,
5488
5633
  model,
@@ -5494,6 +5639,7 @@ async function spawnSubagent(opts) {
5494
5639
  summarisingEmitted = true;
5495
5640
  sink?.current?.({
5496
5641
  kind: "phase",
5642
+ runId,
5497
5643
  task: taskPreview,
5498
5644
  skillName,
5499
5645
  model,
@@ -5530,6 +5676,7 @@ async function spawnSubagent(opts) {
5530
5676
  [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
5531
5677
  sink?.current?.({
5532
5678
  kind: "end",
5679
+ runId,
5533
5680
  task: taskPreview,
5534
5681
  skillName,
5535
5682
  model,
@@ -5593,6 +5740,7 @@ function registerSubagentTool(parentRegistry, opts) {
5593
5740
  const sink = opts.sink;
5594
5741
  parentRegistry.register({
5595
5742
  name: SUBAGENT_TOOL_NAME,
5743
+ parallelSafe: true,
5596
5744
  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.",
5597
5745
  parameters: {
5598
5746
  type: "object",
@@ -5609,6 +5757,17 @@ function registerSubagentTool(parentRegistry, opts) {
5609
5757
  type: "string",
5610
5758
  enum: ["deepseek-v4-flash", "deepseek-v4-pro"],
5611
5759
  description: "Which DeepSeek model the subagent runs on. Default is 'deepseek-v4-flash' \u2014 cheap and fast, fine for explore/research-style subtasks. Override to 'deepseek-v4-pro' (~12\xD7 more expensive) when the subtask genuinely needs the stronger model: cross-file architecture, subtle bug hunts, anything where flash has empirically underperformed."
5760
+ },
5761
+ max_iters: {
5762
+ type: "integer",
5763
+ minimum: MIN_MAX_ITERS,
5764
+ maximum: MAX_MAX_ITERS,
5765
+ description: `Cap on the subagent's tool-call iterations. Default 16 (or the type's default when 'type' is set). Hard range: ${MIN_MAX_ITERS}-${MAX_MAX_ITERS}; out-of-range values are clamped to the nearest end.`
5766
+ },
5767
+ type: {
5768
+ type: "string",
5769
+ enum: [...SUBAGENT_TYPE_NAMES],
5770
+ description: "Optional persona shaping the system prompt and default iter budget. 'explore' = wide-net read-only investigation (20-iter budget, returns a distilled answer). 'verify' = narrow yes/no check with evidence (8-iter budget). Omit when supplying your own 'system' prompt or when the default generic persona fits. Caller-supplied 'system' / 'max_iters' override the type's defaults."
5612
5771
  }
5613
5772
  },
5614
5773
  required: ["task"]
@@ -5620,15 +5779,17 @@ function registerSubagentTool(parentRegistry, opts) {
5620
5779
  error: "spawn_subagent requires a non-empty 'task' argument."
5621
5780
  });
5622
5781
  }
5623
- const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : defaultSystem;
5782
+ const typeSpec = getSubagentType(args.type);
5783
+ const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : typeSpec?.system ?? defaultSystem;
5624
5784
  const model = typeof args.model === "string" && args.model.startsWith("deepseek-") ? args.model : defaultModel;
5785
+ const callerIters = clampMaxIters(args.max_iters);
5625
5786
  const result = await spawnSubagent({
5626
5787
  client: opts.client,
5627
5788
  parentRegistry,
5628
5789
  system,
5629
5790
  task,
5630
5791
  model,
5631
- maxToolIters,
5792
+ maxToolIters: callerIters ?? typeSpec?.maxToolIters ?? maxToolIters,
5632
5793
  maxResultChars,
5633
5794
  sink,
5634
5795
  parentSignal: ctx?.signal
@@ -5638,6 +5799,13 @@ function registerSubagentTool(parentRegistry, opts) {
5638
5799
  });
5639
5800
  return parentRegistry;
5640
5801
  }
5802
+ function clampMaxIters(raw) {
5803
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
5804
+ const n = Math.floor(raw);
5805
+ if (n < MIN_MAX_ITERS) return MIN_MAX_ITERS;
5806
+ if (n > MAX_MAX_ITERS) return MAX_MAX_ITERS;
5807
+ return n;
5808
+ }
5641
5809
  function forkRegistryExcluding(parent, exclude) {
5642
5810
  const child = new ToolRegistry();
5643
5811
  for (const spec of parent.specs()) {
@@ -5650,6 +5818,19 @@ function forkRegistryExcluding(parent, exclude) {
5650
5818
  if (parent.planMode) child.setPlanMode(true);
5651
5819
  return child;
5652
5820
  }
5821
+ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
5822
+ const child = new ToolRegistry();
5823
+ for (const spec of parent.specs()) {
5824
+ const name = spec.function.name;
5825
+ if (!allow.has(name)) continue;
5826
+ if (alsoExclude.has(name)) continue;
5827
+ const def = parent.get(name);
5828
+ if (!def) continue;
5829
+ child.register(def);
5830
+ }
5831
+ if (parent.planMode) child.setPlanMode(true);
5832
+ return child;
5833
+ }
5653
5834
 
5654
5835
  // src/tools/shell.ts
5655
5836
  import * as pathMod7 from "path";
@@ -6985,6 +7166,7 @@ function registerShellTools(registry, opts) {
6985
7166
  name: "job_output",
6986
7167
  description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
6987
7168
  readOnly: true,
7169
+ parallelSafe: true,
6988
7170
  parameters: {
6989
7171
  type: "object",
6990
7172
  properties: {
@@ -7029,6 +7211,7 @@ function registerShellTools(registry, opts) {
7029
7211
  name: "list_jobs",
7030
7212
  description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
7031
7213
  readOnly: true,
7214
+ parallelSafe: true,
7032
7215
  parameters: { type: "object", properties: {} },
7033
7216
  fn: async () => {
7034
7217
  const all = jobs.list();
@@ -7286,6 +7469,7 @@ function registerWebTools(registry, opts = {}) {
7286
7469
  name: "web_search",
7287
7470
  description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this.",
7288
7471
  readOnly: true,
7472
+ parallelSafe: true,
7289
7473
  parameters: {
7290
7474
  type: "object",
7291
7475
  properties: {
@@ -7309,6 +7493,7 @@ function registerWebTools(registry, opts = {}) {
7309
7493
  name: "web_fetch",
7310
7494
  description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
7311
7495
  readOnly: true,
7496
+ parallelSafe: true,
7312
7497
  parameters: {
7313
7498
  type: "object",
7314
7499
  properties: {