@mku1tra/que-pasa 0.1.0 → 0.2.1
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 +20 -1
- package/dist/define/index.d.ts +1 -0
- package/dist/define/index.js +1 -0
- package/dist/define/resource.d.ts +15 -0
- package/dist/define/resource.js +8 -0
- package/dist/define/runtime.d.ts +6 -3
- package/dist/define/tool.d.ts +3 -2
- package/dist/define/tool.js +8 -3
- package/dist/loop/extension.d.ts +2 -1
- package/dist/loop/extension.js +17 -7
- package/dist/loop/index.d.ts +1 -0
- package/dist/loop/index.js +1 -0
- package/dist/loop/resources.d.ts +34 -0
- package/dist/loop/resources.js +57 -0
- package/dist/loop/runtime.d.ts +7 -1
- package/dist/loop/runtime.js +10 -2
- package/dist/sandbox/backend.d.ts +82 -0
- package/dist/sandbox/backend.js +10 -0
- package/dist/sandbox/index.d.ts +7 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/local.d.ts +8 -0
- package/dist/sandbox/local.js +20 -0
- package/dist/sandbox/paths.d.ts +10 -0
- package/dist/sandbox/paths.js +31 -0
- package/dist/sandbox/podman.d.ts +38 -0
- package/dist/sandbox/podman.js +137 -0
- package/dist/sandbox/process.d.ts +27 -0
- package/dist/sandbox/process.js +52 -0
- package/dist/sandbox/shell.d.ts +17 -0
- package/dist/sandbox/shell.js +206 -0
- package/dist/sandbox/tools.d.ts +80 -0
- package/dist/sandbox/tools.js +242 -0
- package/dist/sandbox/view.d.ts +17 -0
- package/dist/sandbox/view.js +105 -0
- package/package.json +5 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import { run } from "./process.js";
|
|
4
|
+
import { shellBackend } from "./shell.js";
|
|
5
|
+
/** The label that holds a hash of the options a container was made with. */
|
|
6
|
+
const label = "que-pasa.sandbox";
|
|
7
|
+
/** podman's exit code for a container that is not there. */
|
|
8
|
+
const noContainer = 125;
|
|
9
|
+
/**
|
|
10
|
+
* A Podman container as a backend: every operation execs into it, so its mounts, its network and its limits are the
|
|
11
|
+
* wall. It is made on its first call and kept: started again if it stopped, made again if it is gone, so a run that
|
|
12
|
+
* resumes after a restart works with no host step. One made with other options is refused, never used.
|
|
13
|
+
*/
|
|
14
|
+
export function podman(options) {
|
|
15
|
+
const { name, image, mounts, workdir = "/work" } = options;
|
|
16
|
+
if (!posix.isAbsolute(workdir))
|
|
17
|
+
throw new Error(`podman: the workdir must be absolute, not "${workdir}"`);
|
|
18
|
+
const config = createHash("sha256")
|
|
19
|
+
.update(JSON.stringify([
|
|
20
|
+
image,
|
|
21
|
+
mounts,
|
|
22
|
+
workdir,
|
|
23
|
+
options.network ?? "none",
|
|
24
|
+
options.user ?? null,
|
|
25
|
+
options.memory ?? null,
|
|
26
|
+
options.cpus ?? null,
|
|
27
|
+
options.runArgs ?? [],
|
|
28
|
+
]))
|
|
29
|
+
.digest("hex")
|
|
30
|
+
.slice(0, 16);
|
|
31
|
+
const podmanOf = async (args, stdin, signal) => run("podman", args, {
|
|
32
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
33
|
+
...(signal === undefined ? {} : { signal }),
|
|
34
|
+
});
|
|
35
|
+
const must = (what, ran) => {
|
|
36
|
+
if (ran.code !== 0)
|
|
37
|
+
throw new Error(`podman ${what} ${name}: exit ${ran.code}: ${(ran.stderr || ran.stdout).trim().slice(-400)}`);
|
|
38
|
+
return ran.stdout.trim();
|
|
39
|
+
};
|
|
40
|
+
/** The container's label and whether it runs, or undefined when there is none. */
|
|
41
|
+
async function inspect() {
|
|
42
|
+
const ran = await podmanOf([
|
|
43
|
+
"container",
|
|
44
|
+
"inspect",
|
|
45
|
+
"--format",
|
|
46
|
+
`{{index .Config.Labels "${label}"}} {{.State.Running}}`,
|
|
47
|
+
name,
|
|
48
|
+
]);
|
|
49
|
+
if (ran.code !== 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const [made, running] = ran.stdout.trim().split(" ");
|
|
52
|
+
return { made, running: running === "true" };
|
|
53
|
+
}
|
|
54
|
+
/** Keep it, start it, or make it: and refuse one made with other options. */
|
|
55
|
+
async function ensure() {
|
|
56
|
+
let found = await inspect();
|
|
57
|
+
if (!found) {
|
|
58
|
+
const made = await podmanOf([
|
|
59
|
+
"run",
|
|
60
|
+
"--detach",
|
|
61
|
+
"--name",
|
|
62
|
+
name,
|
|
63
|
+
"--label",
|
|
64
|
+
`${label}=${config}`,
|
|
65
|
+
"--network",
|
|
66
|
+
options.network ?? "none",
|
|
67
|
+
...(options.user ? ["--user", options.user] : []),
|
|
68
|
+
...(options.memory ? ["--memory", options.memory] : []),
|
|
69
|
+
...(options.cpus ? ["--cpus", String(options.cpus)] : []),
|
|
70
|
+
...(options.runArgs ?? []),
|
|
71
|
+
...mounts.flatMap((m) => [
|
|
72
|
+
"--volume",
|
|
73
|
+
`${m.source}:${m.target}${m.readOnly ? ":ro" : ""}`,
|
|
74
|
+
]),
|
|
75
|
+
"--workdir",
|
|
76
|
+
workdir,
|
|
77
|
+
image,
|
|
78
|
+
"sleep",
|
|
79
|
+
"infinity",
|
|
80
|
+
]);
|
|
81
|
+
if (made.code === 0)
|
|
82
|
+
return;
|
|
83
|
+
// Another process made it first: it is checked as any found one.
|
|
84
|
+
found = await inspect();
|
|
85
|
+
if (!found)
|
|
86
|
+
must("run", made);
|
|
87
|
+
}
|
|
88
|
+
if (found.made !== config) {
|
|
89
|
+
throw new Error(`podman: the container "${name}" was made with other options: remove it, or give this sandbox another name`);
|
|
90
|
+
}
|
|
91
|
+
if (!found.running)
|
|
92
|
+
must("start", await podmanOf(["start", name]));
|
|
93
|
+
}
|
|
94
|
+
let ready;
|
|
95
|
+
const ensured = () => {
|
|
96
|
+
ready ??= ensure().catch((error) => {
|
|
97
|
+
ready = undefined;
|
|
98
|
+
throw error;
|
|
99
|
+
});
|
|
100
|
+
return ready;
|
|
101
|
+
};
|
|
102
|
+
const backend = shellBackend({
|
|
103
|
+
id: name,
|
|
104
|
+
root: workdir,
|
|
105
|
+
async exec({ script, args = [], stdin, signal }) {
|
|
106
|
+
const call = async () => {
|
|
107
|
+
await ensured();
|
|
108
|
+
return podmanOf([
|
|
109
|
+
"exec",
|
|
110
|
+
...(stdin === undefined ? [] : ["--interactive"]),
|
|
111
|
+
"--workdir",
|
|
112
|
+
workdir,
|
|
113
|
+
name,
|
|
114
|
+
"sh",
|
|
115
|
+
"-c",
|
|
116
|
+
script,
|
|
117
|
+
"sh",
|
|
118
|
+
...args,
|
|
119
|
+
], stdin, signal);
|
|
120
|
+
};
|
|
121
|
+
const ran = await call();
|
|
122
|
+
// Gone since it was made, such as by a host restart: made again, once.
|
|
123
|
+
if (ran.code === noContainer && !(await inspect())) {
|
|
124
|
+
ready = undefined;
|
|
125
|
+
return call();
|
|
126
|
+
}
|
|
127
|
+
return ran;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
...backend,
|
|
132
|
+
async close() {
|
|
133
|
+
ready = undefined;
|
|
134
|
+
must("rm", await podmanOf(["rm", "--force", "--ignore", "--time", "0", name]));
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** What a process gave back. */
|
|
2
|
+
export type Ran = {
|
|
3
|
+
/** null when it was killed by a signal. */
|
|
4
|
+
code: number | null;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
/** Its output was cut to its tail. */
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** One call of a script: its arguments are `$1`…, and its input goes over stdin, so neither is read by the shell. */
|
|
11
|
+
export type Script = {
|
|
12
|
+
script: string;
|
|
13
|
+
args?: readonly string[];
|
|
14
|
+
stdin?: string;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
/** The most of each stream a process keeps: its tail, where a failure is. */
|
|
18
|
+
export declare const keep = 1000000;
|
|
19
|
+
/**
|
|
20
|
+
* Run a command to its end. It never rejects on a non-zero exit, only when it cannot start or the signal aborts it,
|
|
21
|
+
* which kills it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function run(command: string, args: readonly string[], { stdin, signal, cwd, }?: {
|
|
24
|
+
stdin?: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}): Promise<Ran>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** The most of each stream a process keeps: its tail, where a failure is. */
|
|
3
|
+
export const keep = 1_000_000;
|
|
4
|
+
/** Keep the last `keep` characters of a stream. */
|
|
5
|
+
function tail() {
|
|
6
|
+
let text = "";
|
|
7
|
+
let cut = false;
|
|
8
|
+
return {
|
|
9
|
+
add(chunk) {
|
|
10
|
+
text += chunk.toString("utf8");
|
|
11
|
+
if (text.length > keep) {
|
|
12
|
+
text = text.slice(-keep);
|
|
13
|
+
cut = true;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
get text() {
|
|
17
|
+
return text;
|
|
18
|
+
},
|
|
19
|
+
get cut() {
|
|
20
|
+
return cut;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run a command to its end. It never rejects on a non-zero exit, only when it cannot start or the signal aborts it,
|
|
26
|
+
* which kills it.
|
|
27
|
+
*/
|
|
28
|
+
export function run(command, args, { stdin, signal, cwd, } = {}) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(command, args, {
|
|
31
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
32
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
33
|
+
...(signal === undefined ? {} : { signal }),
|
|
34
|
+
});
|
|
35
|
+
const out = tail();
|
|
36
|
+
const err = tail();
|
|
37
|
+
child.stdout?.on("data", out.add);
|
|
38
|
+
child.stderr?.on("data", err.add);
|
|
39
|
+
child.on("error", reject);
|
|
40
|
+
child.on("close", (code) => resolve({
|
|
41
|
+
code,
|
|
42
|
+
stdout: out.text,
|
|
43
|
+
stderr: err.text,
|
|
44
|
+
truncated: out.cut || err.cut,
|
|
45
|
+
}));
|
|
46
|
+
if (child.stdin) {
|
|
47
|
+
// A script that exits before it reads its input closes the pipe: that is its answer, not an error.
|
|
48
|
+
child.stdin.on("error", () => { });
|
|
49
|
+
child.stdin.end(stdin);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SandboxBackend } from "./backend.ts";
|
|
2
|
+
import type { Ran, Script } from "./process.ts";
|
|
3
|
+
/** Runs one script in the backend's root: `sh -c <script> sh <args…>`, with its stdin. */
|
|
4
|
+
export type Exec = (call: Script) => Promise<Ran>;
|
|
5
|
+
/** The most matches grep gives back. */
|
|
6
|
+
export declare const maxMatches = 1000;
|
|
7
|
+
/**
|
|
8
|
+
* A whole sandbox backend in POSIX `sh`, on one primitive that runs a script in the root (Deep Agents' `BaseSandbox`).
|
|
9
|
+
* A path is always a positional argument and content always goes over stdin: nothing a model gives is part of a
|
|
10
|
+
* script. It needs no other runtime in the sandbox: `find`, `awk`, `grep`, `cat`, `mkdir` and `dirname`, which
|
|
11
|
+
* BusyBox has too.
|
|
12
|
+
*/
|
|
13
|
+
export declare function shellBackend({ id, root, exec, }: {
|
|
14
|
+
id: string;
|
|
15
|
+
root: string;
|
|
16
|
+
exec: Exec;
|
|
17
|
+
}): SandboxBackend;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { asArg, fromRoot } from "./paths.js";
|
|
3
|
+
/** How a script says why it did nothing. Any other failure is the backend's, and throws. */
|
|
4
|
+
const missing = 3;
|
|
5
|
+
const directory = 4;
|
|
6
|
+
/** The most matches grep gives back. */
|
|
7
|
+
export const maxMatches = 1000;
|
|
8
|
+
/** How many files one grep call reads. */
|
|
9
|
+
const batch = 200;
|
|
10
|
+
const invalid = { error: "invalid_path" };
|
|
11
|
+
/** A path as the backend gives it in a result: from the root, with no `./`. */
|
|
12
|
+
const clean = (p) => posix.normalize(p).replace(/^\.\//, "");
|
|
13
|
+
/** A script's failure that is none of the ones it says: the backend's own, such as a read-only file system. */
|
|
14
|
+
function failed(what, ran) {
|
|
15
|
+
throw new Error(`${what}: exit ${ran.code ?? "killed"}: ${(ran.stderr || ran.stdout).trim().slice(-400)}`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A whole sandbox backend in POSIX `sh`, on one primitive that runs a script in the root (Deep Agents' `BaseSandbox`).
|
|
19
|
+
* A path is always a positional argument and content always goes over stdin: nothing a model gives is part of a
|
|
20
|
+
* script. It needs no other runtime in the sandbox: `find`, `awk`, `grep`, `cat`, `mkdir` and `dirname`, which
|
|
21
|
+
* BusyBox has too.
|
|
22
|
+
*/
|
|
23
|
+
export function shellBackend({ id, root, exec, }) {
|
|
24
|
+
/** Every file under a path, from the root, `.git` left out. undefined when the path is not there. */
|
|
25
|
+
async function filesUnder(at) {
|
|
26
|
+
const ran = await exec({
|
|
27
|
+
script: `[ -e "$1" ] || exit ${missing}; find "$1" -name .git -prune -o -type f -print0`,
|
|
28
|
+
args: [asArg(at)],
|
|
29
|
+
});
|
|
30
|
+
if (ran.code === missing)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (ran.code !== 0)
|
|
33
|
+
failed("glob", ran);
|
|
34
|
+
const names = ran.stdout.split("\0").filter(Boolean);
|
|
35
|
+
// A cut output starts mid-name: that one is dropped.
|
|
36
|
+
if (ran.truncated)
|
|
37
|
+
names.shift();
|
|
38
|
+
return names.map(clean);
|
|
39
|
+
}
|
|
40
|
+
/** The files under a path whose path from it matches a glob. */
|
|
41
|
+
async function matching(at, pattern) {
|
|
42
|
+
const files = await filesUnder(at);
|
|
43
|
+
return files?.filter((f) => {
|
|
44
|
+
const from = f === at ? posix.basename(f) : at ? f.slice(at.length + 1) : f;
|
|
45
|
+
return posix.matchesGlob(from, pattern);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
root,
|
|
51
|
+
async ls(path) {
|
|
52
|
+
const at = fromRoot(root, path);
|
|
53
|
+
if (at === undefined)
|
|
54
|
+
return invalid;
|
|
55
|
+
const ran = await exec({
|
|
56
|
+
script: `if [ -d "$1" ]; then
|
|
57
|
+
find "$1" -mindepth 1 -maxdepth 1 -exec sh -c 'for f; do if [ -d "$f" ]; then printf "d%s\\0" "$f"; else printf "f%s\\0" "$f"; fi; done' sh {} +
|
|
58
|
+
elif [ -e "$1" ]; then printf "f%s\\0" "$1"
|
|
59
|
+
else exit ${missing}; fi`,
|
|
60
|
+
args: [asArg(at)],
|
|
61
|
+
});
|
|
62
|
+
if (ran.code === missing)
|
|
63
|
+
return { error: "file_not_found" };
|
|
64
|
+
if (ran.code !== 0)
|
|
65
|
+
failed("ls", ran);
|
|
66
|
+
const files = ran.stdout
|
|
67
|
+
.split("\0")
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map((entry) => ({
|
|
70
|
+
path: clean(entry.slice(1)),
|
|
71
|
+
isDir: entry[0] === "d",
|
|
72
|
+
}))
|
|
73
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
74
|
+
return { files };
|
|
75
|
+
},
|
|
76
|
+
async read(path, offset, limit) {
|
|
77
|
+
const at = fromRoot(root, path);
|
|
78
|
+
if (at === undefined)
|
|
79
|
+
return invalid;
|
|
80
|
+
const ran = await exec({
|
|
81
|
+
script: `[ -e "$1" ] || exit ${missing}; [ -d "$1" ] && exit ${directory}; awk -v o="$2" -v l="$3" 'NR > o && NR <= o + l { print } END { printf "%d", NR }' "$1"`,
|
|
82
|
+
args: [asArg(at), String(offset), String(limit)],
|
|
83
|
+
});
|
|
84
|
+
if (ran.code === missing)
|
|
85
|
+
return { error: "file_not_found" };
|
|
86
|
+
if (ran.code === directory)
|
|
87
|
+
return { error: "is_directory" };
|
|
88
|
+
if (ran.code !== 0)
|
|
89
|
+
failed("read", ran);
|
|
90
|
+
// The lines, then the file's line count, with no newline after it.
|
|
91
|
+
const last = ran.stdout.lastIndexOf("\n");
|
|
92
|
+
return {
|
|
93
|
+
content: ran.stdout.slice(0, last + 1),
|
|
94
|
+
totalLines: Number(ran.stdout.slice(last + 1)),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
async write(path, content) {
|
|
98
|
+
const at = fromRoot(root, path);
|
|
99
|
+
if (at === undefined)
|
|
100
|
+
return invalid;
|
|
101
|
+
const ran = await exec({
|
|
102
|
+
script: `[ -d "$1" ] && exit ${directory}; mkdir -p -- "$(dirname -- "$1")" && cat > "$1"`,
|
|
103
|
+
args: [asArg(at)],
|
|
104
|
+
stdin: content,
|
|
105
|
+
});
|
|
106
|
+
if (ran.code === directory)
|
|
107
|
+
return { error: "is_directory" };
|
|
108
|
+
if (ran.code !== 0)
|
|
109
|
+
failed("write", ran);
|
|
110
|
+
return { path: at };
|
|
111
|
+
},
|
|
112
|
+
async edit(path, old, next, all) {
|
|
113
|
+
const at = fromRoot(root, path);
|
|
114
|
+
if (at === undefined)
|
|
115
|
+
return invalid;
|
|
116
|
+
const read = await exec({
|
|
117
|
+
script: `[ -e "$1" ] || exit ${missing}; [ -d "$1" ] && exit ${directory}; cat -- "$1"`,
|
|
118
|
+
args: [asArg(at)],
|
|
119
|
+
});
|
|
120
|
+
if (read.code === missing)
|
|
121
|
+
return { error: "file_not_found" };
|
|
122
|
+
if (read.code === directory)
|
|
123
|
+
return { error: "is_directory" };
|
|
124
|
+
if (read.code !== 0)
|
|
125
|
+
failed("edit", read);
|
|
126
|
+
const occurrences = read.stdout.split(old).length - 1;
|
|
127
|
+
if (occurrences === 0)
|
|
128
|
+
return { error: "string_not_found" };
|
|
129
|
+
if (occurrences > 1 && !all)
|
|
130
|
+
return { error: "string_not_unique", occurrences };
|
|
131
|
+
// The function form: a `$` in the new text is text, never a replacement pattern.
|
|
132
|
+
const text = all
|
|
133
|
+
? read.stdout.split(old).join(next)
|
|
134
|
+
: read.stdout.replace(old, () => next);
|
|
135
|
+
const wrote = await exec({
|
|
136
|
+
script: `cat > "$1"`,
|
|
137
|
+
args: [asArg(at)],
|
|
138
|
+
stdin: text,
|
|
139
|
+
});
|
|
140
|
+
if (wrote.code !== 0)
|
|
141
|
+
failed("edit", wrote);
|
|
142
|
+
return { path: at, occurrences: all ? occurrences : 1 };
|
|
143
|
+
},
|
|
144
|
+
async glob(pattern, path) {
|
|
145
|
+
const at = fromRoot(root, path);
|
|
146
|
+
if (at === undefined)
|
|
147
|
+
return invalid;
|
|
148
|
+
const files = await matching(at, pattern);
|
|
149
|
+
if (!files)
|
|
150
|
+
return { error: "file_not_found" };
|
|
151
|
+
return { files: files.sort() };
|
|
152
|
+
},
|
|
153
|
+
async grep(pattern, path, glob) {
|
|
154
|
+
const at = fromRoot(root, path);
|
|
155
|
+
if (at === undefined)
|
|
156
|
+
return invalid;
|
|
157
|
+
const files = glob ? await matching(at, glob) : await filesUnder(at);
|
|
158
|
+
if (!files)
|
|
159
|
+
return { error: "file_not_found" };
|
|
160
|
+
// No line holds a newline, so a text with one is in no file.
|
|
161
|
+
if (pattern.includes("\n"))
|
|
162
|
+
return { matches: [] };
|
|
163
|
+
const matches = [];
|
|
164
|
+
for (let i = 0; i < files.length && matches.length <= maxMatches; i += batch) {
|
|
165
|
+
const some = files.slice(i, i + batch);
|
|
166
|
+
const ran = await exec({
|
|
167
|
+
script: `p="$1"; shift; grep -nHF -e "$p" -- "$@"`,
|
|
168
|
+
args: [pattern, ...some],
|
|
169
|
+
});
|
|
170
|
+
// 1: no match. 2: a file it could not read, such as one gone since the listing; the others still count.
|
|
171
|
+
if (ran.code !== 0 && ran.code !== 1 && ran.code !== 2)
|
|
172
|
+
failed("grep", ran);
|
|
173
|
+
// A line is "<file>:<n>:<text>", and a file's name can hold ":": the longest file of the batch that starts it.
|
|
174
|
+
const named = [...some].sort((a, b) => b.length - a.length);
|
|
175
|
+
for (const line of ran.stdout.split("\n")) {
|
|
176
|
+
const file = named.find((f) => line.startsWith(`${f}:`));
|
|
177
|
+
if (!file)
|
|
178
|
+
continue;
|
|
179
|
+
const rest = line.slice(file.length + 1);
|
|
180
|
+
const colon = rest.indexOf(":");
|
|
181
|
+
const n = Number(rest.slice(0, colon));
|
|
182
|
+
if (colon < 1 || !Number.isInteger(n))
|
|
183
|
+
continue;
|
|
184
|
+
matches.push({ path: file, line: n, text: rest.slice(colon + 1) });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const truncated = matches.length > maxMatches;
|
|
188
|
+
return {
|
|
189
|
+
matches: matches.slice(0, maxMatches),
|
|
190
|
+
...(truncated ? { truncated } : {}),
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
async execute(command, options = {}) {
|
|
194
|
+
const ran = await exec({
|
|
195
|
+
script: `exec 2>&1; eval "$1"`,
|
|
196
|
+
args: [command],
|
|
197
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
output: ran.stdout + ran.stderr,
|
|
201
|
+
exitCode: ran.code,
|
|
202
|
+
truncated: ran.truncated,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file and command tools of LangChain Deep Agents, as que-pasa tools, with their names, parameters and wording:
|
|
3
|
+
* ls, read_file, write_file, edit_file, glob, grep and execute. Each asks its run for the `Sandbox` the host provides,
|
|
4
|
+
* and works through it: the tools hold no backend and no policy.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { type Json } from "../define/index.ts";
|
|
8
|
+
import { type FileBackend } from "./backend.ts";
|
|
9
|
+
/** The backend a run's file tools work in: the host provides it per run, such as a container, or a view over one. */
|
|
10
|
+
export declare const Sandbox: import("../define/resource.ts").Resource<FileBackend>;
|
|
11
|
+
/** The most of a tool's answer the model gets (Deep Agents: 20,000 tokens, at four characters each). */
|
|
12
|
+
export declare const maxAnswer = 80000;
|
|
13
|
+
export declare const ls: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
14
|
+
path: z.ZodDefault<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>, Json | undefined>;
|
|
16
|
+
export declare const read_file: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
17
|
+
file_path: z.ZodString;
|
|
18
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
}, z.core.$strip>, Json | undefined>;
|
|
21
|
+
export declare const write_file: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
22
|
+
file_path: z.ZodString;
|
|
23
|
+
content: z.ZodString;
|
|
24
|
+
}, z.core.$strip>, Json | undefined>;
|
|
25
|
+
export declare const edit_file: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
26
|
+
file_path: z.ZodString;
|
|
27
|
+
old_string: z.ZodString;
|
|
28
|
+
new_string: z.ZodString;
|
|
29
|
+
replace_all: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
}, z.core.$strip>, Json | undefined>;
|
|
31
|
+
export declare const glob: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
32
|
+
pattern: z.ZodString;
|
|
33
|
+
path: z.ZodDefault<z.ZodString>;
|
|
34
|
+
}, z.core.$strip>, Json | undefined>;
|
|
35
|
+
export declare const grep: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
36
|
+
pattern: z.ZodString;
|
|
37
|
+
path: z.ZodDefault<z.ZodString>;
|
|
38
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, z.core.$strip>, Json | undefined>;
|
|
40
|
+
export declare const execute: import("../define/tool.ts").Tool<z.ZodObject<{
|
|
41
|
+
command: z.ZodString;
|
|
42
|
+
}, z.core.$strip>, Json | undefined>;
|
|
43
|
+
/** The tools that only read. */
|
|
44
|
+
export declare const readTools: readonly [import("../define/tool.ts").Tool<z.ZodObject<{
|
|
45
|
+
path: z.ZodDefault<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
47
|
+
file_path: z.ZodString;
|
|
48
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
49
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
51
|
+
pattern: z.ZodString;
|
|
52
|
+
path: z.ZodDefault<z.ZodString>;
|
|
53
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
54
|
+
pattern: z.ZodString;
|
|
55
|
+
path: z.ZodDefault<z.ZodString>;
|
|
56
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>, Json | undefined>];
|
|
58
|
+
/** The tools that read and write files. `execute` is given on its own. */
|
|
59
|
+
export declare const fileTools: readonly [import("../define/tool.ts").Tool<z.ZodObject<{
|
|
60
|
+
path: z.ZodDefault<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
62
|
+
file_path: z.ZodString;
|
|
63
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
64
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
65
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
66
|
+
pattern: z.ZodString;
|
|
67
|
+
path: z.ZodDefault<z.ZodString>;
|
|
68
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
69
|
+
pattern: z.ZodString;
|
|
70
|
+
path: z.ZodDefault<z.ZodString>;
|
|
71
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
72
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
73
|
+
file_path: z.ZodString;
|
|
74
|
+
content: z.ZodString;
|
|
75
|
+
}, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
|
|
76
|
+
file_path: z.ZodString;
|
|
77
|
+
old_string: z.ZodString;
|
|
78
|
+
new_string: z.ZodString;
|
|
79
|
+
replace_all: z.ZodDefault<z.ZodBoolean>;
|
|
80
|
+
}, z.core.$strip>, Json | undefined>];
|