reasonix 0.28.0 → 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/index.js +220 -113
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +127 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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" });
|
|
@@ -4604,6 +4648,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
4604
4648
|
};
|
|
4605
4649
|
registry.register({
|
|
4606
4650
|
name: "read_file",
|
|
4651
|
+
parallelSafe: true,
|
|
4607
4652
|
description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
|
|
4608
4653
|
- head: N \u2192 first N lines (imports, public API, small configs)
|
|
4609
4654
|
- tail: N \u2192 last N lines (recently-added code, log tails)
|
|
@@ -4687,6 +4732,7 @@ ${slice.join("\n")}`;
|
|
|
4687
4732
|
});
|
|
4688
4733
|
registry.register({
|
|
4689
4734
|
name: "list_directory",
|
|
4735
|
+
parallelSafe: true,
|
|
4690
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.",
|
|
4691
4737
|
readOnly: true,
|
|
4692
4738
|
parameters: {
|
|
@@ -4707,6 +4753,7 @@ ${slice.join("\n")}`;
|
|
|
4707
4753
|
});
|
|
4708
4754
|
registry.register({
|
|
4709
4755
|
name: "directory_tree",
|
|
4756
|
+
parallelSafe: true,
|
|
4710
4757
|
description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
|
|
4711
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.
|
|
4712
4759
|
- Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
|
|
@@ -4785,6 +4832,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4785
4832
|
});
|
|
4786
4833
|
registry.register({
|
|
4787
4834
|
name: "search_files",
|
|
4835
|
+
parallelSafe: true,
|
|
4788
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.",
|
|
4789
4837
|
readOnly: true,
|
|
4790
4838
|
parameters: {
|
|
@@ -4810,6 +4858,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4810
4858
|
});
|
|
4811
4859
|
registry.register({
|
|
4812
4860
|
name: "search_content",
|
|
4861
|
+
parallelSafe: true,
|
|
4813
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.",
|
|
4814
4863
|
readOnly: true,
|
|
4815
4864
|
parameters: {
|
|
@@ -4852,6 +4901,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
4852
4901
|
});
|
|
4853
4902
|
registry.register({
|
|
4854
4903
|
name: "get_file_info",
|
|
4904
|
+
parallelSafe: true,
|
|
4855
4905
|
description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
|
|
4856
4906
|
readOnly: true,
|
|
4857
4907
|
parameters: {
|
|
@@ -5032,6 +5082,7 @@ function registerMemoryTools(registry, opts = {}) {
|
|
|
5032
5082
|
name: "recall_memory",
|
|
5033
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.",
|
|
5034
5084
|
readOnly: true,
|
|
5085
|
+
parallelSafe: true,
|
|
5035
5086
|
parameters: {
|
|
5036
5087
|
type: "object",
|
|
5037
5088
|
properties: {
|
|
@@ -5458,6 +5509,11 @@ function getSubagentType(name) {
|
|
|
5458
5509
|
}
|
|
5459
5510
|
|
|
5460
5511
|
// src/tools/subagent.ts
|
|
5512
|
+
var runIdCounter = 0;
|
|
5513
|
+
function nextRunId() {
|
|
5514
|
+
runIdCounter++;
|
|
5515
|
+
return `sub-${runIdCounter.toString(36)}`;
|
|
5516
|
+
}
|
|
5461
5517
|
var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
|
|
5462
5518
|
|
|
5463
5519
|
Rules:
|
|
@@ -5486,9 +5542,11 @@ async function spawnSubagent(opts) {
|
|
|
5486
5542
|
const sink = opts.sink;
|
|
5487
5543
|
const skillName = opts.skillName;
|
|
5488
5544
|
const startedAt = Date.now();
|
|
5545
|
+
const runId = nextRunId();
|
|
5489
5546
|
const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
|
|
5490
5547
|
sink?.current?.({
|
|
5491
5548
|
kind: "start",
|
|
5549
|
+
runId,
|
|
5492
5550
|
task: taskPreview,
|
|
5493
5551
|
skillName,
|
|
5494
5552
|
model,
|
|
@@ -5501,6 +5559,7 @@ async function spawnSubagent(opts) {
|
|
|
5501
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.`;
|
|
5502
5560
|
sink?.current?.({
|
|
5503
5561
|
kind: "end",
|
|
5562
|
+
runId,
|
|
5504
5563
|
task: taskPreview,
|
|
5505
5564
|
skillName,
|
|
5506
5565
|
model,
|
|
@@ -5562,12 +5621,13 @@ async function spawnSubagent(opts) {
|
|
|
5562
5621
|
let summarisingEmitted = false;
|
|
5563
5622
|
try {
|
|
5564
5623
|
for await (const ev of childLoop.step(opts.task)) {
|
|
5565
|
-
sink?.current?.({ kind: "inner", task: taskPreview, skillName, model, inner: ev });
|
|
5624
|
+
sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
|
|
5566
5625
|
if (ev.role === "tool") {
|
|
5567
5626
|
toolIter++;
|
|
5568
5627
|
summarisingEmitted = false;
|
|
5569
5628
|
sink?.current?.({
|
|
5570
5629
|
kind: "progress",
|
|
5630
|
+
runId,
|
|
5571
5631
|
task: taskPreview,
|
|
5572
5632
|
skillName,
|
|
5573
5633
|
model,
|
|
@@ -5579,6 +5639,7 @@ async function spawnSubagent(opts) {
|
|
|
5579
5639
|
summarisingEmitted = true;
|
|
5580
5640
|
sink?.current?.({
|
|
5581
5641
|
kind: "phase",
|
|
5642
|
+
runId,
|
|
5582
5643
|
task: taskPreview,
|
|
5583
5644
|
skillName,
|
|
5584
5645
|
model,
|
|
@@ -5615,6 +5676,7 @@ async function spawnSubagent(opts) {
|
|
|
5615
5676
|
[\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
|
|
5616
5677
|
sink?.current?.({
|
|
5617
5678
|
kind: "end",
|
|
5679
|
+
runId,
|
|
5618
5680
|
task: taskPreview,
|
|
5619
5681
|
skillName,
|
|
5620
5682
|
model,
|
|
@@ -5678,6 +5740,7 @@ function registerSubagentTool(parentRegistry, opts) {
|
|
|
5678
5740
|
const sink = opts.sink;
|
|
5679
5741
|
parentRegistry.register({
|
|
5680
5742
|
name: SUBAGENT_TOOL_NAME,
|
|
5743
|
+
parallelSafe: true,
|
|
5681
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.",
|
|
5682
5745
|
parameters: {
|
|
5683
5746
|
type: "object",
|
|
@@ -7103,6 +7166,7 @@ function registerShellTools(registry, opts) {
|
|
|
7103
7166
|
name: "job_output",
|
|
7104
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.",
|
|
7105
7168
|
readOnly: true,
|
|
7169
|
+
parallelSafe: true,
|
|
7106
7170
|
parameters: {
|
|
7107
7171
|
type: "object",
|
|
7108
7172
|
properties: {
|
|
@@ -7147,6 +7211,7 @@ function registerShellTools(registry, opts) {
|
|
|
7147
7211
|
name: "list_jobs",
|
|
7148
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.",
|
|
7149
7213
|
readOnly: true,
|
|
7214
|
+
parallelSafe: true,
|
|
7150
7215
|
parameters: { type: "object", properties: {} },
|
|
7151
7216
|
fn: async () => {
|
|
7152
7217
|
const all = jobs.list();
|
|
@@ -7404,6 +7469,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
7404
7469
|
name: "web_search",
|
|
7405
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.",
|
|
7406
7471
|
readOnly: true,
|
|
7472
|
+
parallelSafe: true,
|
|
7407
7473
|
parameters: {
|
|
7408
7474
|
type: "object",
|
|
7409
7475
|
properties: {
|
|
@@ -7427,6 +7493,7 @@ function registerWebTools(registry, opts = {}) {
|
|
|
7427
7493
|
name: "web_fetch",
|
|
7428
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.",
|
|
7429
7495
|
readOnly: true,
|
|
7496
|
+
parallelSafe: true,
|
|
7430
7497
|
parameters: {
|
|
7431
7498
|
type: "object",
|
|
7432
7499
|
properties: {
|