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.
- package/dist/cli/{chunk-R2L5YEEF.js → chunk-COFBA5FV.js} +7 -2
- package/dist/cli/chunk-COFBA5FV.js.map +1 -0
- package/dist/cli/index.js +311 -116
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-YUL7CYKY.js → prompt-VF7B6BWR.js} +2 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +249 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-R2L5YEEF.js.map +0 -1
- /package/dist/cli/{prompt-YUL7CYKY.js.map → prompt-VF7B6BWR.js.map} +0 -0
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CODE_SYSTEM_PROMPT,
|
|
4
4
|
codeSystemPrompt
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-COFBA5FV.js";
|
|
6
6
|
export {
|
|
7
7
|
CODE_SYSTEM_PROMPT,
|
|
8
8
|
codeSystemPrompt
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=prompt-
|
|
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
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
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
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
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: "
|
|
3189
|
-
content:
|
|
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
|
-
|
|
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
|
|
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: {
|