toolnexus 0.21.0 → 0.23.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/README.md +50 -4
- package/dist/agents/agent.d.ts +12 -1
- package/dist/agents/agent.js +43 -7
- package/dist/agents/compaction.d.ts +19 -0
- package/dist/agents/compaction.js +26 -4
- package/dist/agents/runtime.d.ts +5 -0
- package/dist/agents/runtime.js +13 -2
- package/dist/builtin.d.ts +16 -0
- package/dist/builtin.js +315 -17
- package/dist/builtin_extras.d.ts +80 -0
- package/dist/builtin_extras.js +248 -0
- package/dist/client.d.ts +77 -2
- package/dist/client.js +304 -61
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/mcp.d.ts +32 -1
- package/dist/mcp.js +184 -6
- package/dist/models.d.ts +20 -0
- package/dist/models.js +46 -0
- package/dist/promptcache.d.ts +21 -0
- package/dist/promptcache.js +59 -0
- package/dist/store.d.ts +32 -0
- package/dist/store.js +131 -0
- package/dist/toolkit.d.ts +2 -2
- package/dist/toolkit.js +2 -2
- package/dist/tooltiers.d.ts +49 -0
- package/dist/tooltiers.js +197 -0
- package/dist/types.d.ts +19 -0
- package/package.json +4 -3
- package/src/agents/agent.ts +39 -8
- package/src/agents/compaction.ts +45 -4
- package/src/agents/runtime.ts +16 -2
- package/src/builtin.ts +333 -18
- package/src/builtin_extras.ts +285 -0
- package/src/client.ts +330 -66
- package/src/index.ts +5 -0
- package/src/mcp.ts +204 -7
- package/src/models.ts +56 -0
- package/src/promptcache.ts +63 -0
- package/src/store.ts +142 -0
- package/src/toolkit.ts +2 -2
- package/src/tooltiers.ts +221 -0
- package/src/types.ts +20 -0
package/dist/builtin.js
CHANGED
|
@@ -14,11 +14,12 @@ import { compareCodePoints, sortEntriesByName, toPosixPath } from "./order.js";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { pending } from "./types.js";
|
|
16
16
|
import { mediaTypeFor } from "./content.js";
|
|
17
|
+
import { bashOutput, killShell, runWebSearch, editNotebook, goJsonIndent, } from "./builtin_extras.js";
|
|
17
18
|
/**
|
|
18
|
-
* How long a job gets between "please stop" and "stop"
|
|
19
|
-
* every port, so a timeout means the same thing everywhere.
|
|
19
|
+
* How long a job gets between "please stop" and "stop" when `killGraceMs` is
|
|
20
|
+
* not set. Identical in every port, so a timeout means the same thing everywhere.
|
|
20
21
|
*/
|
|
21
|
-
const
|
|
22
|
+
const DEFAULT_KILL_GRACE_MS = 2000;
|
|
22
23
|
const WIN_RESERVED = new Set([
|
|
23
24
|
"CON", "PRN", "AUX", "NUL",
|
|
24
25
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
@@ -68,8 +69,16 @@ class BuiltinEnv {
|
|
|
68
69
|
shellError;
|
|
69
70
|
baseDir;
|
|
70
71
|
confine;
|
|
72
|
+
killGraceMs;
|
|
73
|
+
background;
|
|
74
|
+
webSearch;
|
|
75
|
+
notebookEdit;
|
|
71
76
|
constructor(cfg) {
|
|
72
77
|
const obj = typeof cfg === "object" && cfg !== null ? cfg : {};
|
|
78
|
+
this.background = obj.background;
|
|
79
|
+
this.webSearch = obj.webSearch;
|
|
80
|
+
this.notebookEdit = obj.notebookEdit === true;
|
|
81
|
+
this.killGraceMs = typeof obj.killGraceMs === "number" && obj.killGraceMs > 0 ? obj.killGraceMs : DEFAULT_KILL_GRACE_MS;
|
|
73
82
|
this.baseDir = obj.baseDir ?? "";
|
|
74
83
|
this.confine = obj.confineToBaseDir === true;
|
|
75
84
|
if (obj.shell && obj.shell.length > 0) {
|
|
@@ -276,14 +285,21 @@ function walkFiles(root) {
|
|
|
276
285
|
// individual tools
|
|
277
286
|
// ---------------------------------------------------------------------------
|
|
278
287
|
function bashTool(env) {
|
|
288
|
+
const properties = {
|
|
289
|
+
command: { type: "string", description: "The shell command to run" },
|
|
290
|
+
workdir: { type: "string", description: "Working directory (default: process cwd)" },
|
|
291
|
+
timeout: { type: "number", description: "Timeout in milliseconds (default 60000)" },
|
|
292
|
+
description: { type: "string", description: "Human-readable description of the command" },
|
|
293
|
+
};
|
|
294
|
+
if (env.background) {
|
|
295
|
+
properties.run_in_background = {
|
|
296
|
+
type: "boolean",
|
|
297
|
+
description: "Start the command and return at once with a background shell id; read it with bash_output, stop it with kill_shell. No timeout applies.",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
279
300
|
return builtin("bash", "Run a shell command and return its combined stdout+stderr. Non-zero exit is an error.", {
|
|
280
301
|
type: "object",
|
|
281
|
-
properties
|
|
282
|
-
command: { type: "string", description: "The shell command to run" },
|
|
283
|
-
workdir: { type: "string", description: "Working directory (default: process cwd)" },
|
|
284
|
-
timeout: { type: "number", description: "Timeout in milliseconds (default 60000)" },
|
|
285
|
-
description: { type: "string", description: "Human-readable description of the command" },
|
|
286
|
-
},
|
|
302
|
+
properties,
|
|
287
303
|
required: ["command"],
|
|
288
304
|
additionalProperties: false,
|
|
289
305
|
}, (args, ctx) => new Promise((resolve) => {
|
|
@@ -301,6 +317,10 @@ function bashTool(env) {
|
|
|
301
317
|
}
|
|
302
318
|
const timeout = typeof args.timeout === "number" ? args.timeout : 60_000;
|
|
303
319
|
const meta = { shell: env.shellLabel };
|
|
320
|
+
if (env.background && args.run_in_background === true) {
|
|
321
|
+
const j = env.background.start(argv, command, workdir, env.killGraceMs);
|
|
322
|
+
return resolve(ok(`Started background shell ${j.id}. Read its output with bash_output, stop it with kill_shell.`, { ...meta, backgroundId: j.id }));
|
|
323
|
+
}
|
|
304
324
|
// NOT `shell: true`: on Windows that silently means cmd.exe while every
|
|
305
325
|
// other port fails loudly, and it hides WHICH interpreter ran. `detached`
|
|
306
326
|
// puts the child in its own process group so a kill reaches the command
|
|
@@ -362,7 +382,7 @@ function bashTool(env) {
|
|
|
362
382
|
graceTimer = setTimeout(() => {
|
|
363
383
|
if (killJob(false))
|
|
364
384
|
killedTree = true;
|
|
365
|
-
},
|
|
385
|
+
}, env.killGraceMs);
|
|
366
386
|
};
|
|
367
387
|
let graceTimer;
|
|
368
388
|
const timer = setTimeout(() => stop("timeout"), timeout);
|
|
@@ -459,7 +479,16 @@ function readTool(env) {
|
|
|
459
479
|
const offset = typeof args.offset === "number" ? Math.max(1, Math.trunc(args.offset)) : 1;
|
|
460
480
|
const start = offset - 1;
|
|
461
481
|
const limit = typeof args.limit === "number" ? Math.max(0, Math.trunc(args.limit)) : lines.length - start;
|
|
462
|
-
|
|
482
|
+
const end = Math.min(Math.max(start, start + limit), lines.length);
|
|
483
|
+
const out = lines.slice(start, start + limit).join("\n");
|
|
484
|
+
// A window that stops before the end of the file says so in metadata, with the offset to
|
|
485
|
+
// continue from (SPEC §4C). The text stays byte-identical. A trailing newline is not a line.
|
|
486
|
+
let total = lines.length;
|
|
487
|
+
if (total > 0 && lines[total - 1] === "")
|
|
488
|
+
total--;
|
|
489
|
+
if (end < total)
|
|
490
|
+
return ok(out, { truncated: true, totalLines: total, nextOffset: end + 1 });
|
|
491
|
+
return ok(out);
|
|
463
492
|
});
|
|
464
493
|
}
|
|
465
494
|
function writeTool(env) {
|
|
@@ -483,10 +512,11 @@ function writeTool(env) {
|
|
|
483
512
|
catch (e) {
|
|
484
513
|
return err(`write: ${e instanceof Error ? e.message : String(e)}`);
|
|
485
514
|
}
|
|
515
|
+
const before = await readIfExists(full);
|
|
486
516
|
await fs.mkdir(path.dirname(path.resolve(full)), { recursive: true });
|
|
487
517
|
await fs.writeFile(full, content, "utf8");
|
|
488
518
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
489
|
-
return ok(`Wrote ${bytes} bytes to ${p}`, { bytes });
|
|
519
|
+
return ok(`Wrote ${bytes} bytes to ${p}`, { bytes, ...editMetadata(env, [{ path: full, before, after: content }]) });
|
|
490
520
|
});
|
|
491
521
|
}
|
|
492
522
|
function editTool(env) {
|
|
@@ -530,9 +560,10 @@ function editTool(env) {
|
|
|
530
560
|
return err(`edit: oldString is not unique in ${p} (${count} occurrences); use replaceAll`);
|
|
531
561
|
next = content.replace(oldString, newString);
|
|
532
562
|
}
|
|
533
|
-
await fs.writeFile(
|
|
563
|
+
await fs.writeFile(full, next, "utf8");
|
|
534
564
|
return ok(`Edited ${p} (${args.replaceAll === true ? count : 1} replacement${(args.replaceAll === true ? count : 1) === 1 ? "" : "s"})`, {
|
|
535
565
|
replacements: args.replaceAll === true ? count : 1,
|
|
566
|
+
...editMetadata(env, [{ path: full, before: content, after: next }]),
|
|
536
567
|
});
|
|
537
568
|
});
|
|
538
569
|
}
|
|
@@ -544,6 +575,9 @@ function grepTool(env) {
|
|
|
544
575
|
path: { type: "string", description: "Directory to search (default: process cwd)" },
|
|
545
576
|
include: { type: "string", description: "Glob filter for file names" },
|
|
546
577
|
limit: { type: "number", description: "Maximum number of matches (default 100)" },
|
|
578
|
+
context: { type: "number", description: "Lines of context before and after each match" },
|
|
579
|
+
before: { type: "number", description: "Lines of context before each match (overrides context)" },
|
|
580
|
+
after: { type: "number", description: "Lines of context after each match (overrides context)" },
|
|
547
581
|
},
|
|
548
582
|
required: ["pattern"],
|
|
549
583
|
additionalProperties: false,
|
|
@@ -571,6 +605,14 @@ function grepTool(env) {
|
|
|
571
605
|
// CONTENT and the order of the results were whatever the walk happened to reach first.
|
|
572
606
|
// Now: collect every match, order by relative path (code point) then line number ascending,
|
|
573
607
|
// and truncate last. The emitted path and the sort key are the same string (A28).
|
|
608
|
+
let before = 0, after = 0;
|
|
609
|
+
if (typeof args.context === "number" && args.context > 0)
|
|
610
|
+
before = after = Math.trunc(args.context);
|
|
611
|
+
if (typeof args.before === "number" && args.before >= 0)
|
|
612
|
+
before = Math.trunc(args.before);
|
|
613
|
+
if (typeof args.after === "number" && args.after >= 0)
|
|
614
|
+
after = Math.trunc(args.after);
|
|
615
|
+
const fileLines = new Map(); // only filled when context is asked for
|
|
574
616
|
const hits = [];
|
|
575
617
|
for (const file of walkFiles(root)) {
|
|
576
618
|
const rel = toPosixPath(path.relative(root, file), path.sep);
|
|
@@ -584,14 +626,58 @@ function grepTool(env) {
|
|
|
584
626
|
continue;
|
|
585
627
|
}
|
|
586
628
|
const lines = text.split("\n");
|
|
629
|
+
if (before > 0 || after > 0)
|
|
630
|
+
fileLines.set(rel, lines);
|
|
587
631
|
for (let i = 0; i < lines.length; i++) {
|
|
588
632
|
if (re.test(lines[i]))
|
|
589
633
|
hits.push({ rel, line: i + 1, text: lines[i] });
|
|
590
634
|
}
|
|
591
635
|
}
|
|
592
636
|
hits.sort((a, b) => compareCodePoints(a.rel, b.rel) || a.line - b.line);
|
|
593
|
-
const
|
|
594
|
-
|
|
637
|
+
const total = hits.length;
|
|
638
|
+
const kept = hits.slice(0, limit);
|
|
639
|
+
let outLines;
|
|
640
|
+
if (before === 0 && after === 0) {
|
|
641
|
+
outLines = kept.map((h) => `${h.rel}:${h.line}:${h.text}`);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// ripgrep-style: match lines path:N:text, context lines path-N-text, "--" between
|
|
645
|
+
// groups that do not touch.
|
|
646
|
+
outLines = [];
|
|
647
|
+
const matchAt = new Map();
|
|
648
|
+
for (const h of kept) {
|
|
649
|
+
if (!matchAt.has(h.rel))
|
|
650
|
+
matchAt.set(h.rel, new Set());
|
|
651
|
+
matchAt.get(h.rel).add(h.line);
|
|
652
|
+
}
|
|
653
|
+
let lastRel = "", lastLine = 0;
|
|
654
|
+
for (const h of kept) {
|
|
655
|
+
const lines = fileLines.get(h.rel) ?? [];
|
|
656
|
+
let n = lines.length;
|
|
657
|
+
if (n > 0 && lines[n - 1] === "")
|
|
658
|
+
n--; // a trailing newline is not a line
|
|
659
|
+
let from = Math.max(1, h.line - before);
|
|
660
|
+
const to = Math.min(n, h.line + after);
|
|
661
|
+
if (h.rel === lastRel && from <= lastLine + 1)
|
|
662
|
+
from = lastLine + 1;
|
|
663
|
+
else if (lastRel !== "")
|
|
664
|
+
outLines.push("--");
|
|
665
|
+
for (let k = from; k <= to; k++) {
|
|
666
|
+
const sep = matchAt.get(h.rel).has(k) ? ":" : "-";
|
|
667
|
+
outLines.push(`${h.rel}${sep}${k}${sep}${lines[k - 1]}`);
|
|
668
|
+
}
|
|
669
|
+
if (to > lastLine || h.rel !== lastRel)
|
|
670
|
+
lastLine = to;
|
|
671
|
+
lastRel = h.rel;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const meta = { count: kept.length };
|
|
675
|
+
// Say the list was cut, and by how much, in metadata (SPEC §4C). The text is unchanged.
|
|
676
|
+
if (total > kept.length) {
|
|
677
|
+
meta.truncated = true;
|
|
678
|
+
meta.total = total;
|
|
679
|
+
}
|
|
680
|
+
return ok(outLines.join("\n"), meta);
|
|
595
681
|
});
|
|
596
682
|
}
|
|
597
683
|
function globTool(env) {
|
|
@@ -872,21 +958,26 @@ function applyPatchTool(env) {
|
|
|
872
958
|
// Stage every write/delete first; only touch the filesystem once all hunks apply.
|
|
873
959
|
const writes = [];
|
|
874
960
|
const deletes = [];
|
|
961
|
+
const changes = [];
|
|
875
962
|
try {
|
|
876
963
|
for (const op of ops) {
|
|
877
964
|
if (op.type === "add") {
|
|
878
965
|
if (existsSync(op.path))
|
|
879
966
|
throw new Error(`file already exists: ${op.path}`);
|
|
880
967
|
writes.push({ path: op.path, content: op.content });
|
|
968
|
+
changes.push({ path: op.path, before: undefined, after: op.content });
|
|
881
969
|
}
|
|
882
970
|
else if (op.type === "delete") {
|
|
883
971
|
if (!existsSync(op.path))
|
|
884
972
|
throw new Error(`file not found: ${op.path}`);
|
|
885
973
|
deletes.push(op.path);
|
|
974
|
+
changes.push({ path: op.path, before: await fs.readFile(op.path, "utf8"), after: undefined });
|
|
886
975
|
}
|
|
887
976
|
else {
|
|
888
977
|
const content = await fs.readFile(op.path, "utf8");
|
|
889
|
-
|
|
978
|
+
const next = applyUpdate(content, op.body);
|
|
979
|
+
writes.push({ path: op.path, content: next });
|
|
980
|
+
changes.push({ path: op.path, before: content, after: next });
|
|
890
981
|
}
|
|
891
982
|
}
|
|
892
983
|
}
|
|
@@ -904,9 +995,119 @@ function applyPatchTool(env) {
|
|
|
904
995
|
added: ops.filter((o) => o.type === "add").length,
|
|
905
996
|
updated: ops.filter((o) => o.type === "update").length,
|
|
906
997
|
deleted: ops.filter((o) => o.type === "delete").length,
|
|
998
|
+
...editMetadata(env, changes),
|
|
907
999
|
});
|
|
908
1000
|
});
|
|
909
1001
|
}
|
|
1002
|
+
async function readIfExists(p) {
|
|
1003
|
+
try {
|
|
1004
|
+
return await fs.readFile(p, "utf8");
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/** Lines of a file's text; a trailing newline does not make an extra empty line. */
|
|
1011
|
+
function diffLines(text) {
|
|
1012
|
+
if (!text)
|
|
1013
|
+
return [];
|
|
1014
|
+
const lines = text.split("\n");
|
|
1015
|
+
if (lines[lines.length - 1] === "")
|
|
1016
|
+
lines.pop();
|
|
1017
|
+
return lines;
|
|
1018
|
+
}
|
|
1019
|
+
/** Line diff by longest common subsequence — minimal, so the counts agree across ports. */
|
|
1020
|
+
function lineDiff(a0, b0) {
|
|
1021
|
+
// Common prefix/suffix first: an edit touches a few lines, so the LCS table stays small.
|
|
1022
|
+
let pre = 0;
|
|
1023
|
+
while (pre < a0.length && pre < b0.length && a0[pre] === b0[pre])
|
|
1024
|
+
pre++;
|
|
1025
|
+
let suf = 0;
|
|
1026
|
+
while (suf < a0.length - pre && suf < b0.length - pre && a0[a0.length - 1 - suf] === b0[b0.length - 1 - suf])
|
|
1027
|
+
suf++;
|
|
1028
|
+
const head = a0.slice(0, pre).map((line) => ({ kind: " ", line }));
|
|
1029
|
+
const tail = a0.slice(a0.length - suf).map((line) => ({ kind: " ", line }));
|
|
1030
|
+
const a = a0.slice(pre, a0.length - suf), b = b0.slice(pre, b0.length - suf);
|
|
1031
|
+
const n = a.length, m = b.length;
|
|
1032
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1033
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1034
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
1035
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const ops = [];
|
|
1039
|
+
let i = 0, j = 0;
|
|
1040
|
+
while (i < n && j < m) {
|
|
1041
|
+
if (a[i] === b[j]) {
|
|
1042
|
+
ops.push({ kind: " ", line: a[i] });
|
|
1043
|
+
i++;
|
|
1044
|
+
j++;
|
|
1045
|
+
}
|
|
1046
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1])
|
|
1047
|
+
ops.push({ kind: "-", line: a[i++] });
|
|
1048
|
+
else
|
|
1049
|
+
ops.push({ kind: "+", line: b[j++] });
|
|
1050
|
+
}
|
|
1051
|
+
while (i < n)
|
|
1052
|
+
ops.push({ kind: "-", line: a[i++] });
|
|
1053
|
+
while (j < m)
|
|
1054
|
+
ops.push({ kind: "+", line: b[j++] });
|
|
1055
|
+
return [...head, ...ops, ...tail];
|
|
1056
|
+
}
|
|
1057
|
+
/** Unified hunks with 3 lines of context. */
|
|
1058
|
+
function unifiedHunks(ops) {
|
|
1059
|
+
const CTX = 3;
|
|
1060
|
+
const out = [];
|
|
1061
|
+
const changed = ops.map((o, k) => (o.kind === " " ? -1 : k)).filter((k) => k >= 0);
|
|
1062
|
+
if (!changed.length)
|
|
1063
|
+
return out;
|
|
1064
|
+
// group change indices whose gap of context is <= 2*CTX
|
|
1065
|
+
const groups = [];
|
|
1066
|
+
for (const k of changed) {
|
|
1067
|
+
const g = groups[groups.length - 1];
|
|
1068
|
+
if (g && k - g[1] <= 2 * CTX)
|
|
1069
|
+
g[1] = k;
|
|
1070
|
+
else
|
|
1071
|
+
groups.push([k, k]);
|
|
1072
|
+
}
|
|
1073
|
+
// old/new line number before each op
|
|
1074
|
+
const oldAt = [], newAt = [];
|
|
1075
|
+
let o = 1, nn = 1;
|
|
1076
|
+
for (const op of ops) {
|
|
1077
|
+
oldAt.push(o);
|
|
1078
|
+
newAt.push(nn);
|
|
1079
|
+
if (op.kind !== "+")
|
|
1080
|
+
o++;
|
|
1081
|
+
if (op.kind !== "-")
|
|
1082
|
+
nn++;
|
|
1083
|
+
}
|
|
1084
|
+
for (const [s0, e0] of groups) {
|
|
1085
|
+
const s = Math.max(0, s0 - CTX), e = Math.min(ops.length - 1, e0 + CTX);
|
|
1086
|
+
const slice = ops.slice(s, e + 1);
|
|
1087
|
+
const oldLen = slice.filter((x) => x.kind !== "+").length;
|
|
1088
|
+
const newLen = slice.filter((x) => x.kind !== "-").length;
|
|
1089
|
+
const oldStart = oldLen ? oldAt[s] : oldAt[s] - 1;
|
|
1090
|
+
const newStart = newLen ? newAt[s] : newAt[s] - 1;
|
|
1091
|
+
out.push(`@@ -${oldStart},${oldLen} +${newStart},${newLen} @@`);
|
|
1092
|
+
for (const x of slice)
|
|
1093
|
+
out.push(x.kind + x.line);
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
1097
|
+
function editMetadata(env, changes) {
|
|
1098
|
+
const files = [];
|
|
1099
|
+
const diff = [];
|
|
1100
|
+
for (const c of changes) {
|
|
1101
|
+
const rel = toPosixPath(env.baseDir ? path.relative(path.resolve(env.baseDir), path.resolve(c.path)) : c.path, path.sep);
|
|
1102
|
+
const ops = lineDiff(diffLines(c.before), diffLines(c.after));
|
|
1103
|
+
const additions = ops.filter((x) => x.kind === "+").length;
|
|
1104
|
+
const deletions = ops.filter((x) => x.kind === "-").length;
|
|
1105
|
+
const status = c.before === undefined ? "added" : c.after === undefined ? "deleted" : "modified";
|
|
1106
|
+
files.push({ path: rel, status, additions, deletions });
|
|
1107
|
+
diff.push(`--- ${c.before === undefined ? "/dev/null" : `a/${rel}`}`, `+++ ${c.after === undefined ? "/dev/null" : `b/${rel}`}`, ...unifiedHunks(ops));
|
|
1108
|
+
}
|
|
1109
|
+
return { files, diff: diff.length ? diff.join("\n") + "\n" : "" };
|
|
1110
|
+
}
|
|
910
1111
|
/**
|
|
911
1112
|
* Build the ten built-in tools (each source:"builtin"). The order is fixed
|
|
912
1113
|
* for parity: bash, read, write, edit, grep, glob, webfetch, question,
|
|
@@ -914,7 +1115,7 @@ function applyPatchTool(env) {
|
|
|
914
1115
|
*/
|
|
915
1116
|
export function createBuiltinTools(cfg) {
|
|
916
1117
|
const env = new BuiltinEnv(cfg);
|
|
917
|
-
|
|
1118
|
+
const tools = [
|
|
918
1119
|
bashTool(env),
|
|
919
1120
|
readTool(env),
|
|
920
1121
|
writeTool(env),
|
|
@@ -926,6 +1127,103 @@ export function createBuiltinTools(cfg) {
|
|
|
926
1127
|
applyPatchTool(env),
|
|
927
1128
|
todowriteTool(),
|
|
928
1129
|
];
|
|
1130
|
+
if (env.background)
|
|
1131
|
+
tools.push(bashOutputTool(env.background), killShellTool(env.background));
|
|
1132
|
+
if (env.webSearch)
|
|
1133
|
+
tools.push(webSearchTool(env.webSearch));
|
|
1134
|
+
if (env.notebookEdit)
|
|
1135
|
+
tools.push(notebookEditTool(env));
|
|
1136
|
+
for (const t of tools)
|
|
1137
|
+
t.hints = { ...BUILTIN_HINTS[t.name] };
|
|
1138
|
+
return tools;
|
|
1139
|
+
}
|
|
1140
|
+
const hint = (readOnly, destructive, openWorld) => ({ readOnly, destructive, openWorld });
|
|
1141
|
+
/**
|
|
1142
|
+
* Hints on every builtin, for host permission gates (SPEC §4C). bash can do anything, so it is
|
|
1143
|
+
* mutating, destructive and open-world. todowrite and question only touch the agent's own state
|
|
1144
|
+
* or ask the human, so they count as read-only for the host's files and systems.
|
|
1145
|
+
*/
|
|
1146
|
+
const BUILTIN_HINTS = {
|
|
1147
|
+
bash: hint(false, true, true),
|
|
1148
|
+
read: hint(true, false, false),
|
|
1149
|
+
write: hint(false, true, false),
|
|
1150
|
+
edit: hint(false, true, false),
|
|
1151
|
+
grep: hint(true, false, false),
|
|
1152
|
+
glob: hint(true, false, false),
|
|
1153
|
+
webfetch: hint(true, false, true),
|
|
1154
|
+
question: hint(true, false, false),
|
|
1155
|
+
apply_patch: hint(false, true, false),
|
|
1156
|
+
todowrite: hint(true, false, false),
|
|
1157
|
+
bash_output: hint(true, false, false),
|
|
1158
|
+
kill_shell: hint(false, true, false),
|
|
1159
|
+
websearch: hint(true, false, true),
|
|
1160
|
+
notebook_edit: hint(false, true, false),
|
|
1161
|
+
};
|
|
1162
|
+
const idSchema = {
|
|
1163
|
+
type: "object",
|
|
1164
|
+
properties: { id: { type: "string", description: "The background shell id bash returned" } },
|
|
1165
|
+
required: ["id"],
|
|
1166
|
+
additionalProperties: false,
|
|
1167
|
+
};
|
|
1168
|
+
function bashOutputTool(bg) {
|
|
1169
|
+
return builtin("bash_output", "Read the output a background bash command (started with run_in_background) produced since the last read, and whether it is still running.", idSchema, async (args) => bashOutput(bg, typeof args.id === "string" ? args.id : ""));
|
|
1170
|
+
}
|
|
1171
|
+
function killShellTool(bg) {
|
|
1172
|
+
return builtin("kill_shell", "Stop a background bash command and everything it started.", idSchema, async (args) => killShell(bg, typeof args.id === "string" ? args.id : ""));
|
|
1173
|
+
}
|
|
1174
|
+
function webSearchTool(search) {
|
|
1175
|
+
return builtin("websearch", "Search the web. Returns numbered results with title, URL and snippet.", {
|
|
1176
|
+
type: "object",
|
|
1177
|
+
properties: {
|
|
1178
|
+
query: { type: "string", description: "The search query" },
|
|
1179
|
+
limit: { type: "number", description: "Maximum number of results (default 5)" },
|
|
1180
|
+
},
|
|
1181
|
+
required: ["query"],
|
|
1182
|
+
additionalProperties: false,
|
|
1183
|
+
}, async (args, ctx) => runWebSearch(search, args, ctx?.signal));
|
|
1184
|
+
}
|
|
1185
|
+
function notebookEditTool(env) {
|
|
1186
|
+
return builtin("notebook_edit", "Edit a Jupyter notebook (.ipynb) cell: replace its source, insert a new cell after it, or delete it. Replacing a code cell clears its outputs.", {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
properties: {
|
|
1189
|
+
path: { type: "string", description: "Path to the .ipynb file" },
|
|
1190
|
+
cell_id: { type: "string", description: "The cell's id (or use cell_index)" },
|
|
1191
|
+
cell_index: { type: "number", description: "0-based cell position (used when cell_id is absent)" },
|
|
1192
|
+
new_source: { type: "string", description: "The new cell source (replace/insert)" },
|
|
1193
|
+
cell_type: { type: "string", enum: ["code", "markdown"], description: "Cell type for insert (default code) or to change on replace" },
|
|
1194
|
+
edit_mode: { type: "string", enum: ["replace", "insert", "delete"], description: "Default replace. insert adds the new cell AFTER the given cell, or first when none is given" },
|
|
1195
|
+
},
|
|
1196
|
+
required: ["path"],
|
|
1197
|
+
additionalProperties: false,
|
|
1198
|
+
}, async (args) => {
|
|
1199
|
+
const p = typeof args.path === "string" ? args.path : "";
|
|
1200
|
+
if (!p)
|
|
1201
|
+
return err("notebook_edit: path is required");
|
|
1202
|
+
let full;
|
|
1203
|
+
let raw;
|
|
1204
|
+
try {
|
|
1205
|
+
full = env.resolvePath(p);
|
|
1206
|
+
raw = await fs.readFile(full, "utf8");
|
|
1207
|
+
}
|
|
1208
|
+
catch (e) {
|
|
1209
|
+
return err(`notebook_edit: ${e instanceof Error ? e.message : String(e)}`);
|
|
1210
|
+
}
|
|
1211
|
+
let nb;
|
|
1212
|
+
try {
|
|
1213
|
+
nb = JSON.parse(raw);
|
|
1214
|
+
if (!nb || typeof nb !== "object" || Array.isArray(nb))
|
|
1215
|
+
throw new Error("not a JSON object");
|
|
1216
|
+
}
|
|
1217
|
+
catch (e) {
|
|
1218
|
+
return err(`notebook_edit: ${p} is not a notebook: ${e instanceof Error ? e.message : String(e)}`);
|
|
1219
|
+
}
|
|
1220
|
+
const r = editNotebook(nb, args, p);
|
|
1221
|
+
if (!("summary" in r))
|
|
1222
|
+
return r;
|
|
1223
|
+
const out = goJsonIndent(nb) + "\n";
|
|
1224
|
+
await fs.writeFile(full, out, "utf8");
|
|
1225
|
+
return ok(r.summary, { cells: nb.cells.length, ...editMetadata(env, [{ path: full, before: raw, after: out }]) });
|
|
1226
|
+
});
|
|
929
1227
|
}
|
|
930
1228
|
/**
|
|
931
1229
|
* The interpreter the builtins resolved for `bash` — what a host prints, and
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in builtins for coding-agent hosts (SPEC §4C): background shells (`bash`
|
|
3
|
+
* run_in_background + `bash_output` + `kill_shell`), `websearch` over a host-supplied provider,
|
|
4
|
+
* and `notebook_edit`. None is on by default: a host that sets nothing sees the same ten builtins.
|
|
5
|
+
*/
|
|
6
|
+
import { type ChildProcess } from "node:child_process";
|
|
7
|
+
import type { ToolResult } from "./types.js";
|
|
8
|
+
/** One background command, for a host UI. */
|
|
9
|
+
export interface BackgroundShellInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
command: string;
|
|
12
|
+
started: Date;
|
|
13
|
+
running: boolean;
|
|
14
|
+
/** Valid when not running; -1 when killed by a signal. */
|
|
15
|
+
exitCode: number;
|
|
16
|
+
killed: boolean;
|
|
17
|
+
}
|
|
18
|
+
interface BgJob {
|
|
19
|
+
id: string;
|
|
20
|
+
command: string;
|
|
21
|
+
started: Date;
|
|
22
|
+
child: ChildProcess;
|
|
23
|
+
pgid?: number;
|
|
24
|
+
grace: number;
|
|
25
|
+
out: string;
|
|
26
|
+
read: number;
|
|
27
|
+
running: boolean;
|
|
28
|
+
exitCode: number;
|
|
29
|
+
killed: boolean;
|
|
30
|
+
done: Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Owns the commands `bash` started with `run_in_background`. Pass one as
|
|
34
|
+
* `builtins.background`, and call `closeAll()` when the host shuts down — a background command
|
|
35
|
+
* is NOT tied to the turn that started it, so nothing else stops it.
|
|
36
|
+
*/
|
|
37
|
+
export declare class BackgroundShells {
|
|
38
|
+
private next;
|
|
39
|
+
private readonly jobs;
|
|
40
|
+
/** @internal Start `command` under `argv` in its own process group. */
|
|
41
|
+
start(argv: string[], command: string, workdir: string, grace: number): BgJob;
|
|
42
|
+
/** @internal */
|
|
43
|
+
get(id: string): BgJob | undefined;
|
|
44
|
+
private signal;
|
|
45
|
+
/** @internal Stop the job's whole process group: SIGTERM, the grace window, SIGKILL.
|
|
46
|
+
* Resolves whether it was still running. */
|
|
47
|
+
kill(j: BgJob): Promise<boolean>;
|
|
48
|
+
static info(j: BgJob): BackgroundShellInfo;
|
|
49
|
+
/** Every background command, oldest first. */
|
|
50
|
+
list(): BackgroundShellInfo[];
|
|
51
|
+
/** Stop one background command's process group. Unknown id ⇒ throws. */
|
|
52
|
+
killShell(id: string): Promise<void>;
|
|
53
|
+
/** Stop every background command still running, concurrently. */
|
|
54
|
+
closeAll(): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export declare function bgStatus(i: BackgroundShellInfo): string;
|
|
57
|
+
/** `bash_output(id)`. */
|
|
58
|
+
export declare function bashOutput(bg: BackgroundShells, id: string): ToolResult;
|
|
59
|
+
/** `kill_shell(id)`. */
|
|
60
|
+
export declare function killShell(bg: BackgroundShells, id: string): Promise<ToolResult>;
|
|
61
|
+
/** One hit from a host search provider. */
|
|
62
|
+
export interface WebSearchResult {
|
|
63
|
+
title: string;
|
|
64
|
+
url: string;
|
|
65
|
+
snippet?: string;
|
|
66
|
+
}
|
|
67
|
+
/** The host's search provider. The library ships none. */
|
|
68
|
+
export type WebSearchFunc = (query: string, limit: number, ctx: {
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}) => Promise<WebSearchResult[]>;
|
|
71
|
+
export declare function runWebSearch(search: WebSearchFunc, args: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult>;
|
|
72
|
+
/** Split text into nbformat's list of lines, each keeping its "\n". */
|
|
73
|
+
export declare function notebookSource(s: string): string[];
|
|
74
|
+
/** Go's json.MarshalIndent(v, "", " ") with sorted keys, plus Go's HTML escaping. */
|
|
75
|
+
export declare function goJsonIndent(v: unknown): string;
|
|
76
|
+
/** Apply one notebook_edit to the parsed notebook, in place. Returns the summary, or an error result. */
|
|
77
|
+
export declare function editNotebook(nb: Record<string, any>, args: Record<string, unknown>, p: string): {
|
|
78
|
+
summary: string;
|
|
79
|
+
} | ToolResult;
|
|
80
|
+
export {};
|