zidane 2.2.3 → 3.0.2
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/README.md +120 -16
- package/dist/{agent-CEO3IeZj.d.ts → agent-4zeSbdXy.d.ts} +97 -3
- package/dist/{chunk-MDVZX6GM.js → chunk-2VM47IBI.js} +5 -3
- package/dist/{chunk-ZSEMKVHP.js → chunk-D45PXTY2.js} +29 -15
- package/dist/chunk-JH6IAAFA.js +28 -0
- package/dist/{chunk-O2XZLJMG.js → chunk-QFHGWKK3.js} +746 -34
- package/dist/{chunk-DRAYZZ23.js → chunk-R74LQKAM.js} +11 -3
- package/dist/{chunk-CYWF2U62.js → chunk-VF4A7HAC.js} +2 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -9
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -1
- package/dist/presets.d.ts +12 -2
- package/dist/presets.js +4 -3
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +1 -1
- package/dist/{skills-use-CvHmgpmO.d.ts → skills-use-DhxQaluD.d.ts} +16 -2
- package/dist/skills.d.ts +2 -2
- package/dist/tools.d.ts +19 -5
- package/dist/tools.js +9 -2
- package/dist/types.d.ts +2 -3
- package/dist/types.js +3 -1
- package/dist/{spawn-BJhCzli9.d.ts → validation-CYISGVTn.d.ts} +35 -2
- package/package.json +1 -1
- package/dist/chunk-MYWDHD7C.js +0 -14
- package/dist/validation-DOY_k7lW.d.ts +0 -11
|
@@ -11,13 +11,104 @@ import {
|
|
|
11
11
|
} from "./chunk-2EQT4EHD.js";
|
|
12
12
|
import {
|
|
13
13
|
connectMcpServers
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-R74LQKAM.js";
|
|
15
|
+
import {
|
|
16
|
+
toolOutputByteLength
|
|
17
|
+
} from "./chunk-JH6IAAFA.js";
|
|
15
18
|
import {
|
|
16
19
|
AgentAbortedError,
|
|
17
20
|
AgentProviderError,
|
|
18
21
|
toTypedError
|
|
19
22
|
} from "./chunk-LNN5UTS2.js";
|
|
20
23
|
|
|
24
|
+
// src/tools/edit-utils.ts
|
|
25
|
+
function countExactMatches(haystack, needle) {
|
|
26
|
+
if (needle.length === 0)
|
|
27
|
+
return 0;
|
|
28
|
+
let count = 0;
|
|
29
|
+
let idx = 0;
|
|
30
|
+
while (true) {
|
|
31
|
+
const next = haystack.indexOf(needle, idx);
|
|
32
|
+
if (next === -1)
|
|
33
|
+
break;
|
|
34
|
+
count++;
|
|
35
|
+
idx = next + needle.length;
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/tools/edit.ts
|
|
41
|
+
var edit = {
|
|
42
|
+
spec: {
|
|
43
|
+
name: "edit",
|
|
44
|
+
description: "Replace exact `old_string` with `new_string` in a file. Fails if `old_string` is not unique unless `replace_all: true`. Prefer over `write_file` for surgical changes \u2014 preserves the rest of the file.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
path: { type: "string", description: "Relative file path." },
|
|
49
|
+
old_string: { type: "string", description: "Exact substring to find." },
|
|
50
|
+
new_string: { type: "string", description: "Replacement substring." },
|
|
51
|
+
replace_all: { type: "boolean", description: "Replace every occurrence. Default: false." }
|
|
52
|
+
},
|
|
53
|
+
required: ["path", "old_string", "new_string"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async execute({ path, old_string, new_string, replace_all }, ctx) {
|
|
57
|
+
const target = path;
|
|
58
|
+
const find = old_string;
|
|
59
|
+
const replacement = new_string;
|
|
60
|
+
const replaceAll = replace_all === true;
|
|
61
|
+
if (find === replacement)
|
|
62
|
+
return `Edit error: old_string and new_string are identical \u2014 nothing to change in ${target}.`;
|
|
63
|
+
if (find.length === 0)
|
|
64
|
+
return `Edit error: old_string is empty. Use write_file to create or fully overwrite a file.`;
|
|
65
|
+
let original;
|
|
66
|
+
try {
|
|
67
|
+
original = await ctx.execution.readFile(ctx.handle, target);
|
|
68
|
+
} catch {
|
|
69
|
+
return `Edit error: file not found: ${target}`;
|
|
70
|
+
}
|
|
71
|
+
const occurrences = countExactMatches(original, find);
|
|
72
|
+
if (occurrences === 0) {
|
|
73
|
+
const preview = nearestMatchPreview(original, find);
|
|
74
|
+
return preview ? `Edit error: old_string not found in ${target}. Closest match in the file: ${preview}` : `Edit error: old_string not found in ${target}.`;
|
|
75
|
+
}
|
|
76
|
+
if (occurrences > 1 && !replaceAll)
|
|
77
|
+
return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
|
|
78
|
+
const updated = replaceAll ? original.split(find).join(replacement) : original.replace(find, replacement);
|
|
79
|
+
if (updated === original)
|
|
80
|
+
return `Edit error: replacement produced no change in ${target}.`;
|
|
81
|
+
await ctx.execution.writeFile(ctx.handle, target, updated);
|
|
82
|
+
return `Edited ${target}: replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"}.`;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function nearestMatchPreview(haystack, needle) {
|
|
86
|
+
const needleFirstLine = needle.split("\n")[0];
|
|
87
|
+
if (needleFirstLine.length < 3)
|
|
88
|
+
return null;
|
|
89
|
+
const lines = haystack.split("\n");
|
|
90
|
+
let bestScore = 0;
|
|
91
|
+
let bestIdx = -1;
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
const score = sharedPrefixLength(lines[i], needleFirstLine);
|
|
94
|
+
if (score > bestScore) {
|
|
95
|
+
bestScore = score;
|
|
96
|
+
bestIdx = i;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (bestIdx < 0 || bestScore < Math.min(8, Math.floor(needleFirstLine.length / 2)))
|
|
100
|
+
return null;
|
|
101
|
+
const snippet = lines[bestIdx].slice(0, 80);
|
|
102
|
+
return `line ${bestIdx + 1}: ${JSON.stringify(snippet)}`;
|
|
103
|
+
}
|
|
104
|
+
function sharedPrefixLength(a, b) {
|
|
105
|
+
const max = Math.min(a.length, b.length);
|
|
106
|
+
let i = 0;
|
|
107
|
+
while (i < max && a.charCodeAt(i) === b.charCodeAt(i))
|
|
108
|
+
i++;
|
|
109
|
+
return i;
|
|
110
|
+
}
|
|
111
|
+
|
|
21
112
|
// src/tools/glob.ts
|
|
22
113
|
var DEFAULT_LIMIT = 1e3;
|
|
23
114
|
var SAFE_GLOB_PATTERN_RE = /^[\w./*?[\]{}!,^@+-]+$/;
|
|
@@ -74,6 +165,207 @@ var glob = {
|
|
|
74
165
|
}
|
|
75
166
|
};
|
|
76
167
|
|
|
168
|
+
// src/tools/grep.ts
|
|
169
|
+
var DEFAULT_HEAD_LIMIT = 250;
|
|
170
|
+
var DEFAULT_OUTPUT_MODE = "files_with_matches";
|
|
171
|
+
var grep = {
|
|
172
|
+
spec: {
|
|
173
|
+
name: "grep",
|
|
174
|
+
description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available with a Bun.Glob fallback for in-process runs.",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
"pattern": { type: "string", description: "Regex (PCRE-flavored via ripgrep, JS regex via fallback)." },
|
|
179
|
+
"path": { type: "string", description: 'File or directory to search. Default: ".".' },
|
|
180
|
+
"glob": { type: "string", description: 'Restrict to files matching this glob, e.g. "**/*.ts".' },
|
|
181
|
+
"type": { type: "string", description: 'rg file type filter, e.g. "ts", "py", "rust". Ignored by the fallback.' },
|
|
182
|
+
"output_mode": { type: "string", enum: ["content", "files_with_matches", "count"], description: 'Default: "files_with_matches".' },
|
|
183
|
+
"-i": { type: "boolean", description: "Case-insensitive match." },
|
|
184
|
+
"-n": { type: "boolean", description: "Show line numbers (content mode). Default: true." },
|
|
185
|
+
"-A": { type: "integer", description: "Lines of trailing context (content mode)." },
|
|
186
|
+
"-B": { type: "integer", description: "Lines of leading context (content mode)." },
|
|
187
|
+
"-C": { type: "integer", description: "Lines of surrounding context (content mode). Overridden by -A/-B if set." },
|
|
188
|
+
"multiline": { type: "boolean", description: "Allow patterns to match across line boundaries." },
|
|
189
|
+
"head_limit": { type: "integer", description: "Cap output entries. Default: 250. Set 0 for unlimited." },
|
|
190
|
+
"offset": { type: "integer", description: "Skip first N entries. Default: 0." }
|
|
191
|
+
},
|
|
192
|
+
required: ["pattern"]
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async execute(rawInput, ctx) {
|
|
196
|
+
const input = rawInput;
|
|
197
|
+
const useRg = await isRipgrepAvailable(ctx);
|
|
198
|
+
if (useRg)
|
|
199
|
+
return runViaRipgrep(input, ctx);
|
|
200
|
+
if (ctx.execution.type === "process")
|
|
201
|
+
return runInProcess(input, ctx);
|
|
202
|
+
return "grep error: ripgrep is not available in the execution context. Install `rg` or use the `shell` tool with grep/awk.";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
async function isRipgrepAvailable(ctx) {
|
|
206
|
+
const result = await ctx.execution.exec(ctx.handle, "rg --version");
|
|
207
|
+
return result.exitCode === 0;
|
|
208
|
+
}
|
|
209
|
+
async function runViaRipgrep(input, ctx) {
|
|
210
|
+
const args = ["rg"];
|
|
211
|
+
const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
|
|
212
|
+
if (mode === "files_with_matches")
|
|
213
|
+
args.push("--files-with-matches");
|
|
214
|
+
else if (mode === "count")
|
|
215
|
+
args.push("--count");
|
|
216
|
+
else
|
|
217
|
+
args.push(input["-n"] ?? true ? "--line-number" : "--no-line-number");
|
|
218
|
+
if (input["-i"])
|
|
219
|
+
args.push("-i");
|
|
220
|
+
if (mode === "content") {
|
|
221
|
+
if (typeof input["-A"] === "number")
|
|
222
|
+
args.push("-A", String(input["-A"]));
|
|
223
|
+
if (typeof input["-B"] === "number")
|
|
224
|
+
args.push("-B", String(input["-B"]));
|
|
225
|
+
if (typeof input["-C"] === "number" && typeof input["-A"] !== "number" && typeof input["-B"] !== "number")
|
|
226
|
+
args.push("-C", String(input["-C"]));
|
|
227
|
+
}
|
|
228
|
+
if (input.multiline)
|
|
229
|
+
args.push("--multiline", "--multiline-dotall");
|
|
230
|
+
if (input.glob)
|
|
231
|
+
args.push("--glob", input.glob);
|
|
232
|
+
if (input.type)
|
|
233
|
+
args.push("--type", input.type);
|
|
234
|
+
args.push("--", input.pattern);
|
|
235
|
+
args.push(input.path ?? ".");
|
|
236
|
+
const command = args.map(shellQuote).join(" ");
|
|
237
|
+
const result = await ctx.execution.exec(ctx.handle, command);
|
|
238
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
239
|
+
return `grep error: ${result.stderr.trim() || `rg exited with code ${result.exitCode}`}`;
|
|
240
|
+
}
|
|
241
|
+
return formatPaginated(result.stdout, input);
|
|
242
|
+
}
|
|
243
|
+
function shellQuote(arg) {
|
|
244
|
+
if (/^[\w@%+=:,./-]+$/.test(arg))
|
|
245
|
+
return arg;
|
|
246
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
247
|
+
}
|
|
248
|
+
async function runInProcess(input, ctx) {
|
|
249
|
+
const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
|
|
250
|
+
const flags = `${input["-i"] ? "i" : ""}${input.multiline ? "s" : ""}${mode !== "content" ? "" : "g"}`;
|
|
251
|
+
let regex;
|
|
252
|
+
try {
|
|
253
|
+
regex = new RegExp(input.pattern, flags || void 0);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return `grep error: invalid regex: ${err.message}`;
|
|
256
|
+
}
|
|
257
|
+
const files = await enumerateFiles(input, ctx);
|
|
258
|
+
const showLineNumbers = input["-n"] ?? true;
|
|
259
|
+
const before = input["-B"] ?? input["-C"] ?? 0;
|
|
260
|
+
const after = input["-A"] ?? input["-C"] ?? 0;
|
|
261
|
+
const lines = [];
|
|
262
|
+
for (const path of files) {
|
|
263
|
+
let content;
|
|
264
|
+
try {
|
|
265
|
+
content = await ctx.execution.readFile(ctx.handle, path);
|
|
266
|
+
} catch {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (input.multiline) {
|
|
270
|
+
const allMatches = [...content.matchAll(new RegExp(regex.source, `${flags.replace(/g/, "")}g`))];
|
|
271
|
+
if (allMatches.length === 0)
|
|
272
|
+
continue;
|
|
273
|
+
if (mode === "files_with_matches") {
|
|
274
|
+
lines.push(path);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (mode === "count") {
|
|
278
|
+
lines.push(`${path}:${allMatches.length}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (const m of allMatches) {
|
|
282
|
+
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
283
|
+
const lineEnd = content.indexOf("\n", m.index);
|
|
284
|
+
const snippet = content.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
|
|
285
|
+
const lineNo = content.slice(0, m.index).split("\n").length;
|
|
286
|
+
lines.push(formatContentLine(path, lineNo, snippet, showLineNumbers));
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const fileLines = content.split("\n");
|
|
291
|
+
const matched = [];
|
|
292
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
293
|
+
regex.lastIndex = 0;
|
|
294
|
+
if (regex.test(fileLines[i]))
|
|
295
|
+
matched.push(i);
|
|
296
|
+
}
|
|
297
|
+
if (matched.length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
if (mode === "files_with_matches") {
|
|
300
|
+
lines.push(path);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (mode === "count") {
|
|
304
|
+
lines.push(`${path}:${matched.length}`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const includeLineNos = /* @__PURE__ */ new Set();
|
|
308
|
+
for (const m of matched) {
|
|
309
|
+
for (let i = Math.max(0, m - before); i <= Math.min(fileLines.length - 1, m + after); i++)
|
|
310
|
+
includeLineNos.add(i);
|
|
311
|
+
}
|
|
312
|
+
const sorted = [...includeLineNos].sort((a, b) => a - b);
|
|
313
|
+
let prev = -2;
|
|
314
|
+
for (const lineNo of sorted) {
|
|
315
|
+
if (lineNo > prev + 1 && lines.length > 0)
|
|
316
|
+
lines.push("--");
|
|
317
|
+
const snippet = fileLines[lineNo];
|
|
318
|
+
lines.push(formatContentLine(path, lineNo + 1, snippet, showLineNumbers));
|
|
319
|
+
prev = lineNo;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return formatPaginated(lines.join("\n"), input);
|
|
323
|
+
}
|
|
324
|
+
function formatContentLine(path, lineNo, snippet, showLineNumbers) {
|
|
325
|
+
return showLineNumbers ? `${path}:${lineNo}:${snippet}` : `${path}:${snippet}`;
|
|
326
|
+
}
|
|
327
|
+
async function enumerateFiles(input, ctx) {
|
|
328
|
+
const cwd = ctx.handle.cwd;
|
|
329
|
+
const root = input.path ?? ".";
|
|
330
|
+
if (input.path && !input.path.includes("*") && !input.path.includes("?")) {
|
|
331
|
+
try {
|
|
332
|
+
const stat = await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`);
|
|
333
|
+
if (stat.stdout.trim() === "file")
|
|
334
|
+
return [input.path];
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const pattern = input.glob ?? "**/*";
|
|
339
|
+
const glob2 = new Bun.Glob(pattern);
|
|
340
|
+
const out = [];
|
|
341
|
+
const scanRoot = root === "." ? cwd : `${cwd.replace(/\/$/, "")}/${root.replace(/^\.\//, "")}`;
|
|
342
|
+
for await (const file of glob2.scan({ cwd: scanRoot, onlyFiles: true })) {
|
|
343
|
+
out.push(root === "." ? file : `${root.replace(/\/$/, "")}/${file}`);
|
|
344
|
+
}
|
|
345
|
+
return out.sort();
|
|
346
|
+
}
|
|
347
|
+
function formatPaginated(text, input) {
|
|
348
|
+
const headLimit = typeof input.head_limit === "number" && input.head_limit >= 0 ? input.head_limit : DEFAULT_HEAD_LIMIT;
|
|
349
|
+
const offset = typeof input.offset === "number" && input.offset > 0 ? Math.floor(input.offset) : 0;
|
|
350
|
+
if (!text.trim())
|
|
351
|
+
return "(no matches)";
|
|
352
|
+
const lines = text.split("\n").filter((l) => l.length > 0);
|
|
353
|
+
const total = lines.length;
|
|
354
|
+
const sliced = headLimit === 0 ? lines.slice(offset) : lines.slice(offset, offset + headLimit);
|
|
355
|
+
if (sliced.length === 0)
|
|
356
|
+
return "(no matches in this slice)";
|
|
357
|
+
const truncatedHead = offset > 0;
|
|
358
|
+
const truncatedTail = headLimit > 0 && offset + headLimit < total;
|
|
359
|
+
let out = sliced.join("\n");
|
|
360
|
+
if (truncatedHead)
|
|
361
|
+
out = `\u2026(${offset} earlier matches skipped)\u2026
|
|
362
|
+
${out}`;
|
|
363
|
+
if (truncatedTail)
|
|
364
|
+
out = `${out}
|
|
365
|
+
\u2026(${total - offset - headLimit} more matches; re-run with offset=${offset + headLimit} or larger head_limit)`;
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
|
|
77
369
|
// src/tools/interaction.ts
|
|
78
370
|
function createInteractionTool(options) {
|
|
79
371
|
const name = options.name ?? "interaction";
|
|
@@ -114,55 +406,251 @@ var listFiles = {
|
|
|
114
406
|
}
|
|
115
407
|
};
|
|
116
408
|
|
|
409
|
+
// src/tools/multi-edit.ts
|
|
410
|
+
var multiEdit = {
|
|
411
|
+
spec: {
|
|
412
|
+
name: "multi_edit",
|
|
413
|
+
description: "Apply a sequential list of edits to a file atomically. Each edit operates on the result of the previous edit. All edits must succeed for any to be written. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file.",
|
|
414
|
+
inputSchema: {
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
path: { type: "string", description: "Relative file path." },
|
|
418
|
+
edits: {
|
|
419
|
+
type: "array",
|
|
420
|
+
description: "List of edits applied in order; each operates on the previous edit's output.",
|
|
421
|
+
items: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
old_string: { type: "string" },
|
|
425
|
+
new_string: { type: "string" },
|
|
426
|
+
replace_all: { type: "boolean" }
|
|
427
|
+
},
|
|
428
|
+
required: ["old_string", "new_string"]
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
required: ["path", "edits"]
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
async execute({ path, edits }, ctx) {
|
|
436
|
+
const target = path;
|
|
437
|
+
const steps = edits;
|
|
438
|
+
if (!Array.isArray(steps) || steps.length === 0)
|
|
439
|
+
return `multi_edit error: edits must be a non-empty array.`;
|
|
440
|
+
let current;
|
|
441
|
+
try {
|
|
442
|
+
current = await ctx.execution.readFile(ctx.handle, target);
|
|
443
|
+
} catch {
|
|
444
|
+
return `multi_edit error: file not found: ${target}`;
|
|
445
|
+
}
|
|
446
|
+
let applied = 0;
|
|
447
|
+
for (let i = 0; i < steps.length; i++) {
|
|
448
|
+
const step = steps[i];
|
|
449
|
+
const find = step.old_string;
|
|
450
|
+
const replacement = step.new_string;
|
|
451
|
+
const replaceAll = step.replace_all === true;
|
|
452
|
+
if (typeof find !== "string" || typeof replacement !== "string")
|
|
453
|
+
return `multi_edit error: edit #${i + 1} is missing old_string or new_string.`;
|
|
454
|
+
if (find.length === 0)
|
|
455
|
+
return `multi_edit error: edit #${i + 1} has empty old_string. Use write_file to fully replace a file.`;
|
|
456
|
+
if (find === replacement)
|
|
457
|
+
return `multi_edit error: edit #${i + 1} old_string and new_string are identical.`;
|
|
458
|
+
const occurrences = countExactMatches(current, find);
|
|
459
|
+
if (occurrences === 0)
|
|
460
|
+
return `multi_edit error: edit #${i + 1} old_string not found in ${target}.`;
|
|
461
|
+
if (occurrences > 1 && !replaceAll)
|
|
462
|
+
return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
|
|
463
|
+
current = replaceAll ? current.split(find).join(replacement) : current.replace(find, replacement);
|
|
464
|
+
applied += occurrences;
|
|
465
|
+
}
|
|
466
|
+
await ctx.execution.writeFile(ctx.handle, target, current);
|
|
467
|
+
return `Edited ${target}: applied ${steps.length} edit${steps.length === 1 ? "" : "s"} (${applied} replacement${applied === 1 ? "" : "s"}).`;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
117
471
|
// src/tools/read-file.ts
|
|
472
|
+
import { Buffer } from "buffer";
|
|
473
|
+
var DEFAULT_LINE_LIMIT = 2e3;
|
|
474
|
+
var DEFAULT_BYTE_CAP = 65536;
|
|
475
|
+
var BINARY_PROBE_BYTES = 8e3;
|
|
118
476
|
var readFile = {
|
|
119
477
|
spec: {
|
|
120
478
|
name: "read_file",
|
|
121
|
-
description: "Read
|
|
479
|
+
description: "Read a file by path. Returns lines [offset..offset+limit). Default offset=1, limit=2000. A trailing footer explains how to read the rest when truncated. Binary files return a short marker rather than mojibake.",
|
|
122
480
|
inputSchema: {
|
|
123
481
|
type: "object",
|
|
124
482
|
properties: {
|
|
125
|
-
path: { type: "string", description: "Relative file path" }
|
|
483
|
+
path: { type: "string", description: "Relative file path." },
|
|
484
|
+
offset: { type: "integer", description: "1-indexed line number to start from. Default: 1." },
|
|
485
|
+
limit: { type: "integer", description: "Max lines to return. Default: 2000. Set 0 for unlimited." },
|
|
486
|
+
maxBytes: { type: "integer", description: "Hard byte cap regardless of line count. Default: 65536. Set 0 for unlimited." }
|
|
126
487
|
},
|
|
127
488
|
required: ["path"]
|
|
128
489
|
}
|
|
129
490
|
},
|
|
130
|
-
async execute({ path }, ctx) {
|
|
491
|
+
async execute({ path, offset, limit, maxBytes }, ctx) {
|
|
492
|
+
let raw;
|
|
131
493
|
try {
|
|
132
|
-
|
|
494
|
+
raw = await ctx.execution.readFile(ctx.handle, path);
|
|
133
495
|
} catch {
|
|
134
496
|
return `File not found: ${path}`;
|
|
135
497
|
}
|
|
498
|
+
const totalBytes = Buffer.byteLength(raw);
|
|
499
|
+
if (looksBinary(raw)) {
|
|
500
|
+
return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
|
|
501
|
+
}
|
|
502
|
+
const offsetN = normalizeInteger(offset, 1);
|
|
503
|
+
const limitN = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
|
|
504
|
+
const maxBytesN = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
|
|
505
|
+
const lines = raw.split("\n");
|
|
506
|
+
const totalLines = lines.length;
|
|
507
|
+
const startIdx = Math.max(0, offsetN - 1);
|
|
508
|
+
const endIdx = limitN > 0 ? Math.min(totalLines, startIdx + limitN) : totalLines;
|
|
509
|
+
let slice = lines.slice(startIdx, endIdx);
|
|
510
|
+
let bytesUsed = 0;
|
|
511
|
+
let bytesCut = false;
|
|
512
|
+
if (maxBytesN > 0) {
|
|
513
|
+
const truncatedSlice = [];
|
|
514
|
+
for (const line of slice) {
|
|
515
|
+
const lineBytes = Buffer.byteLength(line) + 1;
|
|
516
|
+
if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
|
|
517
|
+
bytesCut = true;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
truncatedSlice.push(line);
|
|
521
|
+
bytesUsed += lineBytes;
|
|
522
|
+
if (bytesUsed >= maxBytesN) {
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (truncatedSlice.length < slice.length)
|
|
527
|
+
bytesCut = true;
|
|
528
|
+
slice = truncatedSlice;
|
|
529
|
+
}
|
|
530
|
+
let midLineCut = false;
|
|
531
|
+
if (maxBytesN > 0 && slice.length > 0) {
|
|
532
|
+
const bodyBytes = Buffer.byteLength(slice.join("\n"));
|
|
533
|
+
if (bodyBytes > maxBytesN) {
|
|
534
|
+
const lastIdx = slice.length - 1;
|
|
535
|
+
const lastLine = slice[lastIdx];
|
|
536
|
+
const otherBytes = lastIdx > 0 ? Buffer.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
|
|
537
|
+
const budgetForLast = Math.max(0, maxBytesN - otherBytes);
|
|
538
|
+
let cut = Math.min(lastLine.length, budgetForLast);
|
|
539
|
+
while (cut > 0 && Buffer.byteLength(lastLine.slice(0, cut)) > budgetForLast)
|
|
540
|
+
cut--;
|
|
541
|
+
slice[lastIdx] = lastLine.slice(0, cut);
|
|
542
|
+
midLineCut = true;
|
|
543
|
+
bytesCut = true;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const body = slice.join("\n");
|
|
547
|
+
const linesReturned = slice.length;
|
|
548
|
+
const lastLineRead = startIdx + linesReturned;
|
|
549
|
+
const linesTruncated = endIdx < totalLines || bytesCut;
|
|
550
|
+
if (!linesTruncated && offsetN === 1)
|
|
551
|
+
return body;
|
|
552
|
+
if (!linesTruncated) {
|
|
553
|
+
return `${body}
|
|
554
|
+
|
|
555
|
+
\u2026read lines ${offsetN}-${lastLineRead} of ${totalLines}.`;
|
|
556
|
+
}
|
|
557
|
+
if (midLineCut) {
|
|
558
|
+
return `${body}
|
|
559
|
+
|
|
560
|
+
\u2026truncated mid-line at line ${lastLineRead} (byte cap ${maxBytesN} reached). File has ${totalLines} lines, ${totalBytes} bytes total. Raise maxBytes, or use shell with sed/awk to read the remainder of this line.`;
|
|
561
|
+
}
|
|
562
|
+
const reason = bytesCut ? `byte cap (${maxBytesN}) reached` : `line limit (${limitN}) reached`;
|
|
563
|
+
return `${body}
|
|
564
|
+
|
|
565
|
+
\u2026truncated at line ${lastLineRead} (${reason}). File has ${totalLines} lines, ${totalBytes} bytes total \u2014 re-read with offset=${lastLineRead + 1} to continue.`;
|
|
136
566
|
}
|
|
137
567
|
};
|
|
568
|
+
function normalizeInteger(value, fallback) {
|
|
569
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
570
|
+
return fallback;
|
|
571
|
+
if (value < 0)
|
|
572
|
+
return fallback;
|
|
573
|
+
return Math.floor(value);
|
|
574
|
+
}
|
|
575
|
+
var REPLACEMENT_RATIO_THRESHOLD = 0.01;
|
|
576
|
+
var REPLACEMENT_MIN_COUNT = 5;
|
|
577
|
+
function looksBinary(text) {
|
|
578
|
+
const sample = text.length > BINARY_PROBE_BYTES ? text.slice(0, BINARY_PROBE_BYTES) : text;
|
|
579
|
+
if (sample.includes("\0"))
|
|
580
|
+
return true;
|
|
581
|
+
if (sample.length === 0)
|
|
582
|
+
return false;
|
|
583
|
+
let replacementCount = 0;
|
|
584
|
+
for (let i = 0; i < sample.length; i++) {
|
|
585
|
+
if (sample.charCodeAt(i) === 65533)
|
|
586
|
+
replacementCount++;
|
|
587
|
+
}
|
|
588
|
+
return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
|
|
589
|
+
}
|
|
138
590
|
|
|
139
591
|
// src/tools/shell.ts
|
|
592
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
593
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 8192;
|
|
140
594
|
var shell = {
|
|
141
595
|
spec: {
|
|
142
596
|
name: "shell",
|
|
143
|
-
description: "Execute a shell command and return stdout
|
|
597
|
+
description: "Execute a shell command in the project root and return its combined stdout/stderr. Output is tail-priority truncated at 8 KB by default; errors and exit-code summaries live in the tail. Set maxOutputBytes=0 to disable truncation.",
|
|
144
598
|
inputSchema: {
|
|
145
599
|
type: "object",
|
|
146
600
|
properties: {
|
|
147
|
-
command: { type: "string", description: "
|
|
601
|
+
command: { type: "string", description: "Shell command to run." },
|
|
602
|
+
timeout: { type: "integer", description: "Per-call timeout in milliseconds." },
|
|
603
|
+
maxOutputBytes: { type: "integer", description: "Truncate combined stdout+stderr beyond this many bytes. Default: 8192. Set 0 for unlimited." }
|
|
148
604
|
},
|
|
149
605
|
required: ["command"]
|
|
150
606
|
}
|
|
151
607
|
},
|
|
152
|
-
async execute({ command }, ctx) {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
608
|
+
async execute({ command, timeout, maxOutputBytes }, ctx) {
|
|
609
|
+
const execOpts = {};
|
|
610
|
+
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0)
|
|
611
|
+
execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
|
|
612
|
+
const result = await ctx.execution.exec(ctx.handle, command, execOpts);
|
|
613
|
+
const cap = normalizeCap(maxOutputBytes);
|
|
614
|
+
if (result.exitCode === 0)
|
|
615
|
+
return truncateTail(result.stdout || "(no output)", cap);
|
|
616
|
+
const combined = `${result.stdout}
|
|
159
617
|
${result.stderr}`.trim();
|
|
618
|
+
return `Exit code ${result.exitCode}
|
|
619
|
+
${truncateTail(combined, cap)}`;
|
|
160
620
|
}
|
|
161
621
|
};
|
|
622
|
+
function normalizeCap(value) {
|
|
623
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
624
|
+
return DEFAULT_MAX_OUTPUT_BYTES;
|
|
625
|
+
if (value < 0)
|
|
626
|
+
return DEFAULT_MAX_OUTPUT_BYTES;
|
|
627
|
+
return Math.floor(value);
|
|
628
|
+
}
|
|
629
|
+
function truncateTail(text, cap) {
|
|
630
|
+
if (cap === 0)
|
|
631
|
+
return text;
|
|
632
|
+
const totalBytes = Buffer2.byteLength(text);
|
|
633
|
+
if (totalBytes <= cap)
|
|
634
|
+
return text;
|
|
635
|
+
let bytes = 0;
|
|
636
|
+
let charIdx = text.length;
|
|
637
|
+
while (charIdx > 0) {
|
|
638
|
+
const ch = text[charIdx - 1];
|
|
639
|
+
const chBytes = Buffer2.byteLength(ch);
|
|
640
|
+
if (bytes + chBytes > cap)
|
|
641
|
+
break;
|
|
642
|
+
bytes += chBytes;
|
|
643
|
+
charIdx--;
|
|
644
|
+
}
|
|
645
|
+
const tail = text.slice(charIdx);
|
|
646
|
+
const droppedBytes = totalBytes - Buffer2.byteLength(tail);
|
|
647
|
+
return `\u2026(${droppedBytes} bytes truncated from head)\u2026
|
|
648
|
+
${tail}`;
|
|
649
|
+
}
|
|
162
650
|
|
|
163
651
|
// src/tools/skills-read.ts
|
|
164
652
|
var SNIFF_BYTES = 8192;
|
|
165
|
-
function
|
|
653
|
+
function looksBinary2(text) {
|
|
166
654
|
const len = Math.min(text.length, SNIFF_BYTES);
|
|
167
655
|
for (let i = 0; i < len; i++) {
|
|
168
656
|
if (text.charCodeAt(i) === 0)
|
|
@@ -214,7 +702,7 @@ function createSkillsReadTool(options) {
|
|
|
214
702
|
const message = err instanceof Error ? err.message : String(err);
|
|
215
703
|
return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
|
|
216
704
|
}
|
|
217
|
-
if (
|
|
705
|
+
if (looksBinary2(content)) {
|
|
218
706
|
return JSON.stringify({
|
|
219
707
|
kind: "binary-unsupported",
|
|
220
708
|
path: validated.absolutePath,
|
|
@@ -456,14 +944,156 @@ function rewriteMessagesToWire(messages, maps) {
|
|
|
456
944
|
}
|
|
457
945
|
|
|
458
946
|
// src/tools/validation.ts
|
|
947
|
+
var TRUE_STRINGS = /* @__PURE__ */ new Set(["true", "True", "TRUE", "1", "yes", "Yes", "YES"]);
|
|
948
|
+
var FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "False", "FALSE", "0", "no", "No", "NO"]);
|
|
459
949
|
function validateToolArgs(input, schema) {
|
|
460
950
|
const required = schema.required ?? [];
|
|
951
|
+
const properties = schema.properties ?? {};
|
|
461
952
|
for (const field of required) {
|
|
462
953
|
if (!(field in input) || input[field] === void 0 || input[field] === null) {
|
|
463
954
|
return { valid: false, error: `Missing required field: ${field}` };
|
|
464
955
|
}
|
|
465
956
|
}
|
|
466
|
-
|
|
957
|
+
let coerced;
|
|
958
|
+
const coercions = [];
|
|
959
|
+
for (const [key, value] of Object.entries(input)) {
|
|
960
|
+
const propSchema = properties[key];
|
|
961
|
+
if (!propSchema?.type)
|
|
962
|
+
continue;
|
|
963
|
+
if (value === void 0 || value === null)
|
|
964
|
+
continue;
|
|
965
|
+
const outcome = coerceValue(value, propSchema);
|
|
966
|
+
if (outcome.error) {
|
|
967
|
+
return { valid: false, error: `Field "${key}": ${outcome.error}` };
|
|
968
|
+
}
|
|
969
|
+
if (outcome.changed) {
|
|
970
|
+
if (!coerced)
|
|
971
|
+
coerced = { ...input };
|
|
972
|
+
coerced[key] = outcome.value;
|
|
973
|
+
coercions.push(key);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
valid: true,
|
|
978
|
+
coercedInput: coerced ?? input,
|
|
979
|
+
coercions
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function coerceValue(value, schema) {
|
|
983
|
+
const declaredTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
984
|
+
for (const t of declaredTypes) {
|
|
985
|
+
if (matchesType(value, t)) {
|
|
986
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
987
|
+
return {
|
|
988
|
+
value,
|
|
989
|
+
changed: false,
|
|
990
|
+
error: `must be one of ${JSON.stringify(schema.enum)}, got ${formatValue(value)}`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
return { value, changed: false };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
for (const t of declaredTypes) {
|
|
997
|
+
const coerced = tryCoerce(value, t);
|
|
998
|
+
if (coerced.ok) {
|
|
999
|
+
if (schema.enum && !schema.enum.includes(coerced.value)) {
|
|
1000
|
+
return {
|
|
1001
|
+
value,
|
|
1002
|
+
changed: false,
|
|
1003
|
+
error: `must be one of ${JSON.stringify(schema.enum)}, got ${formatValue(coerced.value)}`
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return { value: coerced.value, changed: true };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const expected = declaredTypes.join(" | ");
|
|
1010
|
+
return {
|
|
1011
|
+
value,
|
|
1012
|
+
changed: false,
|
|
1013
|
+
error: `expected ${expected}, got ${jsonType(value)} ${formatValue(value)}`
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function matchesType(value, type) {
|
|
1017
|
+
switch (type) {
|
|
1018
|
+
case "string":
|
|
1019
|
+
return typeof value === "string";
|
|
1020
|
+
case "number":
|
|
1021
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1022
|
+
case "integer":
|
|
1023
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
1024
|
+
case "boolean":
|
|
1025
|
+
return typeof value === "boolean";
|
|
1026
|
+
case "array":
|
|
1027
|
+
return Array.isArray(value);
|
|
1028
|
+
case "object":
|
|
1029
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1030
|
+
case "null":
|
|
1031
|
+
return value === null;
|
|
1032
|
+
default:
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function tryCoerce(value, type) {
|
|
1037
|
+
if (typeof value === "string") {
|
|
1038
|
+
if (type === "boolean") {
|
|
1039
|
+
const trimmed = value.trim();
|
|
1040
|
+
if (TRUE_STRINGS.has(trimmed))
|
|
1041
|
+
return { ok: true, value: true };
|
|
1042
|
+
if (FALSE_STRINGS.has(trimmed))
|
|
1043
|
+
return { ok: true, value: false };
|
|
1044
|
+
return { ok: false };
|
|
1045
|
+
}
|
|
1046
|
+
if (type === "number") {
|
|
1047
|
+
const n = Number(value.trim());
|
|
1048
|
+
return Number.isFinite(n) ? { ok: true, value: n } : { ok: false };
|
|
1049
|
+
}
|
|
1050
|
+
if (type === "integer") {
|
|
1051
|
+
const n = Number(value.trim());
|
|
1052
|
+
return Number.isInteger(n) ? { ok: true, value: n } : { ok: false };
|
|
1053
|
+
}
|
|
1054
|
+
if (type === "array" || type === "object") {
|
|
1055
|
+
try {
|
|
1056
|
+
const parsed = JSON.parse(value);
|
|
1057
|
+
if (type === "array" && Array.isArray(parsed))
|
|
1058
|
+
return { ok: true, value: parsed };
|
|
1059
|
+
if (type === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1060
|
+
return { ok: true, value: parsed };
|
|
1061
|
+
return { ok: false };
|
|
1062
|
+
} catch {
|
|
1063
|
+
return { ok: false };
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (type === "null") {
|
|
1067
|
+
return value === "" || value === "null" ? { ok: true, value: null } : { ok: false };
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1071
|
+
if (type === "string")
|
|
1072
|
+
return { ok: true, value: String(value) };
|
|
1073
|
+
if (type === "integer" && Number.isInteger(value))
|
|
1074
|
+
return { ok: true, value };
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof value === "boolean" && type === "string")
|
|
1077
|
+
return { ok: true, value: String(value) };
|
|
1078
|
+
return { ok: false };
|
|
1079
|
+
}
|
|
1080
|
+
function jsonType(value) {
|
|
1081
|
+
if (value === null)
|
|
1082
|
+
return "null";
|
|
1083
|
+
if (Array.isArray(value))
|
|
1084
|
+
return "array";
|
|
1085
|
+
return typeof value;
|
|
1086
|
+
}
|
|
1087
|
+
function formatValue(value) {
|
|
1088
|
+
let s;
|
|
1089
|
+
try {
|
|
1090
|
+
s = JSON.stringify(value);
|
|
1091
|
+
} catch {
|
|
1092
|
+
s = String(value);
|
|
1093
|
+
}
|
|
1094
|
+
if (s === void 0)
|
|
1095
|
+
s = String(value);
|
|
1096
|
+
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
467
1097
|
}
|
|
468
1098
|
|
|
469
1099
|
// src/loop.ts
|
|
@@ -716,6 +1346,29 @@ async function executeTurn(ctx, turn) {
|
|
|
716
1346
|
content: toolResultMsg.content,
|
|
717
1347
|
createdAt: Date.now()
|
|
718
1348
|
});
|
|
1349
|
+
if (typeof ctx.toolOutputBudget === "number" && ctx.toolOutputBudget > 0) {
|
|
1350
|
+
const totalBytes = toolResults.reduce(
|
|
1351
|
+
(sum, r) => sum + toolOutputByteLength(r.content),
|
|
1352
|
+
0
|
|
1353
|
+
);
|
|
1354
|
+
if (totalBytes > ctx.toolOutputBudget) {
|
|
1355
|
+
const warning = `[Tool output budget exceeded: ${totalBytes} bytes returned in this turn (cap: ${ctx.toolOutputBudget}). Summarize the salient findings before calling more tools.]`;
|
|
1356
|
+
const userMsg = ctx.provider.userMessage(warning);
|
|
1357
|
+
ctx.turns.push({
|
|
1358
|
+
id: await ctx.generateTurnId(),
|
|
1359
|
+
runId: ctx.runId,
|
|
1360
|
+
role: userMsg.role,
|
|
1361
|
+
content: userMsg.content,
|
|
1362
|
+
createdAt: Date.now()
|
|
1363
|
+
});
|
|
1364
|
+
await ctx.hooks.callHook("budget:exceeded", {
|
|
1365
|
+
turn,
|
|
1366
|
+
turnId,
|
|
1367
|
+
bytes: totalBytes,
|
|
1368
|
+
budget: ctx.toolOutputBudget
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
719
1372
|
return { ended: false, turnId, usage: result.usage };
|
|
720
1373
|
}
|
|
721
1374
|
function stripImagesForNonVision(provider, output) {
|
|
@@ -742,29 +1395,64 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
742
1395
|
if (gateCtx.block) {
|
|
743
1396
|
return { result: { id: callId, content: `Blocked: ${gateCtx.reason}` } };
|
|
744
1397
|
}
|
|
745
|
-
|
|
1398
|
+
let effectiveInput = gateCtx.input;
|
|
746
1399
|
if (!toolDef) {
|
|
747
|
-
const
|
|
748
|
-
await ctx.hooks.callHook("tool:error", {
|
|
1400
|
+
const unknownCtx = {
|
|
749
1401
|
turnId,
|
|
750
1402
|
callId,
|
|
751
1403
|
name: call.name,
|
|
752
1404
|
displayName,
|
|
753
1405
|
input: effectiveInput,
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
|
|
1406
|
+
suppressError: false
|
|
1407
|
+
};
|
|
1408
|
+
await ctx.hooks.callHook("tool:unknown", unknownCtx);
|
|
1409
|
+
const content = unknownCtx.result ?? `Tool error: Unknown tool: ${call.name}`;
|
|
1410
|
+
if (!unknownCtx.suppressError) {
|
|
1411
|
+
const err = new Error(`Unknown tool: ${call.name}`);
|
|
1412
|
+
await ctx.hooks.callHook("tool:error", {
|
|
1413
|
+
turnId,
|
|
1414
|
+
callId,
|
|
1415
|
+
name: call.name,
|
|
1416
|
+
displayName,
|
|
1417
|
+
input: effectiveInput,
|
|
1418
|
+
error: err
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return { result: { id: callId, content } };
|
|
757
1422
|
}
|
|
758
1423
|
const validation = validateToolArgs(effectiveInput, toolDef.spec.inputSchema);
|
|
759
1424
|
if (!validation.valid) {
|
|
1425
|
+
await ctx.hooks.callHook("validation:reject", {
|
|
1426
|
+
turnId,
|
|
1427
|
+
callId,
|
|
1428
|
+
name: call.name,
|
|
1429
|
+
displayName,
|
|
1430
|
+
input: effectiveInput,
|
|
1431
|
+
reason: validation.error ?? "invalid input",
|
|
1432
|
+
schema: toolDef.spec.inputSchema
|
|
1433
|
+
});
|
|
760
1434
|
return { result: { id: callId, content: `Validation error: ${validation.error}` } };
|
|
761
1435
|
}
|
|
1436
|
+
effectiveInput = validation.coercedInput ?? effectiveInput;
|
|
1437
|
+
const coercions = validation.coercions && validation.coercions.length > 0 ? validation.coercions : void 0;
|
|
1438
|
+
if (coercions) {
|
|
1439
|
+
await ctx.hooks.callHook("validation:coerce", {
|
|
1440
|
+
turnId,
|
|
1441
|
+
callId,
|
|
1442
|
+
name: call.name,
|
|
1443
|
+
displayName,
|
|
1444
|
+
input: effectiveInput,
|
|
1445
|
+
coercions,
|
|
1446
|
+
schema: toolDef.spec.inputSchema
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
762
1449
|
await ctx.hooks.callHook("tool:before", {
|
|
763
1450
|
turnId,
|
|
764
1451
|
callId,
|
|
765
1452
|
name: call.name,
|
|
766
1453
|
displayName,
|
|
767
|
-
input: effectiveInput
|
|
1454
|
+
input: effectiveInput,
|
|
1455
|
+
...coercions ? { coercions } : {}
|
|
768
1456
|
});
|
|
769
1457
|
let output;
|
|
770
1458
|
let isError = false;
|
|
@@ -809,7 +1497,9 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
809
1497
|
displayName,
|
|
810
1498
|
input: effectiveInput,
|
|
811
1499
|
result: output,
|
|
812
|
-
isError
|
|
1500
|
+
isError,
|
|
1501
|
+
outputBytes: toolOutputByteLength(output),
|
|
1502
|
+
...coercions ? { coercions } : {}
|
|
813
1503
|
};
|
|
814
1504
|
await ctx.hooks.callHook("tool:transform", transformCtx);
|
|
815
1505
|
output = transformCtx.result;
|
|
@@ -821,7 +1511,9 @@ async function executeSingleTool(ctx, call, turnId) {
|
|
|
821
1511
|
name: call.name,
|
|
822
1512
|
displayName,
|
|
823
1513
|
input: effectiveInput,
|
|
824
|
-
result: output
|
|
1514
|
+
result: output,
|
|
1515
|
+
outputBytes: toolOutputByteLength(output),
|
|
1516
|
+
...coercions ? { coercions } : {}
|
|
825
1517
|
});
|
|
826
1518
|
return { result: { id: callId, content: output } };
|
|
827
1519
|
}
|
|
@@ -946,6 +1638,9 @@ var HOOK_EVENT_NAMES = [
|
|
|
946
1638
|
"tool:after",
|
|
947
1639
|
"tool:error",
|
|
948
1640
|
"tool:transform",
|
|
1641
|
+
"tool:unknown",
|
|
1642
|
+
"validation:reject",
|
|
1643
|
+
"validation:coerce",
|
|
949
1644
|
"context:transform",
|
|
950
1645
|
"steer:inject",
|
|
951
1646
|
"spawn:before",
|
|
@@ -974,6 +1669,7 @@ var HOOK_EVENT_NAMES = [
|
|
|
974
1669
|
"skills:deactivate",
|
|
975
1670
|
"usage",
|
|
976
1671
|
"output",
|
|
1672
|
+
"budget:exceeded",
|
|
977
1673
|
"agent:abort",
|
|
978
1674
|
"agent:done",
|
|
979
1675
|
"session:start",
|
|
@@ -993,7 +1689,8 @@ function resolveBehavior(agentBehavior, runBehavior) {
|
|
|
993
1689
|
maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
|
|
994
1690
|
thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
|
|
995
1691
|
schema: runBehavior?.schema ?? agentBehavior?.schema,
|
|
996
|
-
cache: runBehavior?.cache ?? agentBehavior?.cache ?? true
|
|
1692
|
+
cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
|
|
1693
|
+
toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget
|
|
997
1694
|
};
|
|
998
1695
|
}
|
|
999
1696
|
function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
|
|
@@ -1116,7 +1813,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
|
|
|
1116
1813
|
}
|
|
1117
1814
|
const thinking = options.thinking ?? "off";
|
|
1118
1815
|
const model = options.model ?? provider.meta.defaultModel;
|
|
1119
|
-
const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache } = resolveBehavior(agentBehavior, options.behavior);
|
|
1816
|
+
const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget } = resolveBehavior(agentBehavior, options.behavior);
|
|
1120
1817
|
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
1121
1818
|
if (skillsCatalog) {
|
|
1122
1819
|
system = `${system}
|
|
@@ -1251,6 +1948,7 @@ ${skillsCatalog}`;
|
|
|
1251
1948
|
thinkingBudget,
|
|
1252
1949
|
schema,
|
|
1253
1950
|
cache,
|
|
1951
|
+
toolOutputBudget,
|
|
1254
1952
|
runStartMs
|
|
1255
1953
|
});
|
|
1256
1954
|
const finalStats = {
|
|
@@ -1546,7 +2244,7 @@ function createSpawnTool(options = {}) {
|
|
|
1546
2244
|
},
|
|
1547
2245
|
spec: {
|
|
1548
2246
|
name: "spawn",
|
|
1549
|
-
description: "Spawn a sub-agent
|
|
2247
|
+
description: "Spawn a sub-agent for a self-contained task that benefits from isolation (separate context window, separate retries) \u2014 for example, a deep research dive or a long codegen pass on a specific file. The sub-agent runs independently with its own tool access and returns its final response. Do NOT spawn for sequential steps you could do yourself.",
|
|
1550
2248
|
inputSchema: {
|
|
1551
2249
|
type: "object",
|
|
1552
2250
|
properties: {
|
|
@@ -1695,22 +2393,33 @@ function createSpawnTool(options = {}) {
|
|
|
1695
2393
|
var spawn = createSpawnTool();
|
|
1696
2394
|
|
|
1697
2395
|
// src/tools/write-file.ts
|
|
2396
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
1698
2397
|
var writeFile = {
|
|
1699
2398
|
spec: {
|
|
1700
2399
|
name: "write_file",
|
|
1701
|
-
description:
|
|
2400
|
+
description: 'Write content to a file (creates parent directories). Returns Created / Updated / "No change needed" so the model can detect no-ops without a separate read.',
|
|
1702
2401
|
inputSchema: {
|
|
1703
2402
|
type: "object",
|
|
1704
2403
|
properties: {
|
|
1705
|
-
path: { type: "string", description: "Relative file path" },
|
|
1706
|
-
content: { type: "string", description: "File content
|
|
2404
|
+
path: { type: "string", description: "Relative file path." },
|
|
2405
|
+
content: { type: "string", description: "File content." }
|
|
1707
2406
|
},
|
|
1708
2407
|
required: ["path", "content"]
|
|
1709
2408
|
}
|
|
1710
2409
|
},
|
|
1711
2410
|
async execute({ path, content }, ctx) {
|
|
1712
|
-
|
|
1713
|
-
|
|
2411
|
+
const targetPath = path;
|
|
2412
|
+
const targetContent = content;
|
|
2413
|
+
let existing;
|
|
2414
|
+
try {
|
|
2415
|
+
existing = await ctx.execution.readFile(ctx.handle, targetPath);
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
const bytes = Buffer3.byteLength(targetContent);
|
|
2419
|
+
if (existing === targetContent)
|
|
2420
|
+
return `No change needed: ${targetPath} already at target state (${bytes} bytes).`;
|
|
2421
|
+
await ctx.execution.writeFile(ctx.handle, targetPath, targetContent);
|
|
2422
|
+
return existing === void 0 ? `Created ${targetPath} (${bytes} bytes).` : `Updated ${targetPath} (${bytes} bytes).`;
|
|
1714
2423
|
}
|
|
1715
2424
|
};
|
|
1716
2425
|
|
|
@@ -1720,9 +2429,12 @@ export {
|
|
|
1720
2429
|
createSkillsRunScriptTool,
|
|
1721
2430
|
createSkillsUseTool,
|
|
1722
2431
|
createAgent,
|
|
2432
|
+
edit,
|
|
1723
2433
|
glob,
|
|
2434
|
+
grep,
|
|
1724
2435
|
createInteractionTool,
|
|
1725
2436
|
listFiles,
|
|
2437
|
+
multiEdit,
|
|
1726
2438
|
readFile,
|
|
1727
2439
|
shell,
|
|
1728
2440
|
createSpawnTool,
|