@mku1tra/que-pasa 0.2.0 → 0.2.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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * A backend: the place where a run's files are and its commands run, behind one interface. Two abilities, as in
3
+ * LangChain Deep Agents' backend protocol: files (`FileBackend`), and commands on top of files (`SandboxBackend`).
4
+ * Every path is the backend's own: relative to its root, or absolute under it. Every path a result gives is relative to
5
+ * the root. A backend gives its errors back in the result, and throws only when it cannot run at all.
6
+ */
7
+ /** Why an operation did nothing. Deep Agents' file errors, and the two of an edit. */
8
+ export type FileError = "file_not_found" | "is_directory" | "invalid_path" | "permission_denied" | "string_not_found" | "string_not_unique";
9
+ /** What every result can say: that the operation did nothing, and why. `message` says more, for the model. */
10
+ export type Failed = {
11
+ error?: FileError;
12
+ message?: string;
13
+ };
14
+ export type FileInfo = {
15
+ path: string;
16
+ isDir: boolean;
17
+ };
18
+ export type LsResult = Failed & {
19
+ files?: FileInfo[];
20
+ };
21
+ export type ReadResult = Failed & {
22
+ /** The lines asked for, each with its newline. */
23
+ content?: string;
24
+ /** How many lines the file has. */
25
+ totalLines?: number;
26
+ };
27
+ export type WriteResult = Failed & {
28
+ path?: string;
29
+ };
30
+ export type EditResult = Failed & {
31
+ path?: string;
32
+ occurrences?: number;
33
+ };
34
+ export type GlobResult = Failed & {
35
+ files?: string[];
36
+ truncated?: boolean;
37
+ };
38
+ export type GrepMatch = {
39
+ path: string;
40
+ line: number;
41
+ text: string;
42
+ };
43
+ export type GrepResult = Failed & {
44
+ matches?: GrepMatch[];
45
+ truncated?: boolean;
46
+ };
47
+ export type ExecuteResult = {
48
+ /** stdout and stderr, as they came. */
49
+ output: string;
50
+ /** null when the command was killed by a signal. */
51
+ exitCode: number | null;
52
+ /** The output kept only its tail. */
53
+ truncated: boolean;
54
+ };
55
+ /** Files: read, write and search them. */
56
+ export interface FileBackend {
57
+ /** The folder every path is under. */
58
+ readonly root: string;
59
+ /** What a folder holds, or the file itself. */
60
+ ls(path: string): Promise<LsResult>;
61
+ /** `limit` lines of a file, after the first `offset`. */
62
+ read(path: string, offset: number, limit: number): Promise<ReadResult>;
63
+ /** A file, whole, made with its folder if it has none. It replaces what was there. */
64
+ write(path: string, content: string): Promise<WriteResult>;
65
+ /** Replace `old` with `next` in a file: it must occur once, unless `all`. */
66
+ edit(path: string, old: string, next: string, all: boolean): Promise<EditResult>;
67
+ /** The files under a folder whose path from that folder matches a glob. */
68
+ glob(pattern: string, path: string): Promise<GlobResult>;
69
+ /** The lines of the files under a path that hold a text, as is (not a pattern), in files matching `glob` if given: by path, then line. */
70
+ grep(pattern: string, path: string, glob?: string): Promise<GrepResult>;
71
+ }
72
+ /** Files, and commands: a backend that can run a shell command. */
73
+ export interface SandboxBackend extends FileBackend {
74
+ /** Names this sandbox, such as its container. */
75
+ readonly id: string;
76
+ /** Run a shell command in the root. The signal kills it. */
77
+ execute(command: string, options?: {
78
+ signal?: AbortSignal;
79
+ }): Promise<ExecuteResult>;
80
+ }
81
+ /** Whether a backend can run commands. */
82
+ export declare function canExecute(backend: FileBackend): backend is SandboxBackend;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A backend: the place where a run's files are and its commands run, behind one interface. Two abilities, as in
3
+ * LangChain Deep Agents' backend protocol: files (`FileBackend`), and commands on top of files (`SandboxBackend`).
4
+ * Every path is the backend's own: relative to its root, or absolute under it. Every path a result gives is relative to
5
+ * the root. A backend gives its errors back in the result, and throws only when it cannot run at all.
6
+ */
7
+ /** Whether a backend can run commands. */
8
+ export function canExecute(backend) {
9
+ return typeof backend.execute === "function";
10
+ }
@@ -0,0 +1,7 @@
1
+ export { canExecute, type EditResult, type ExecuteResult, type FileBackend, type FileError, type FileInfo, type GlobResult, type GrepMatch, type GrepResult, type LsResult, type ReadResult, type SandboxBackend, type WriteResult, } from "./backend.ts";
2
+ export { local } from "./local.ts";
3
+ export { podman, type Mount, type Podman, type PodmanOptions, } from "./podman.ts";
4
+ export { shellBackend, type Exec } from "./shell.ts";
5
+ export type { Ran, Script } from "./process.ts";
6
+ export { Sandbox, commandAnswer, edit_file, execute, fileTools, glob, grep, ls, maxAnswer, read_file, readTools, write_file, } from "./tools.ts";
7
+ export { view, type ViewPaths } from "./view.ts";
@@ -0,0 +1,6 @@
1
+ export { canExecute, } from "./backend.js";
2
+ export { local } from "./local.js";
3
+ export { podman, } from "./podman.js";
4
+ export { shellBackend } from "./shell.js";
5
+ export { Sandbox, commandAnswer, edit_file, execute, fileTools, glob, grep, ls, maxAnswer, read_file, readTools, write_file, } from "./tools.js";
6
+ export { view } from "./view.js";
@@ -0,0 +1,8 @@
1
+ import type { SandboxBackend } from "./backend.ts";
2
+ /**
3
+ * A folder on the host as a backend: files and commands run as the host's own user. No wall: a path is checked
4
+ * against the root, but a symlink, or any command, reaches the whole host. For the host's own trusted work, and tests.
5
+ */
6
+ export declare function local({ root }: {
7
+ root: string;
8
+ }): SandboxBackend;
@@ -0,0 +1,20 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { run } from "./process.js";
3
+ import { shellBackend } from "./shell.js";
4
+ /**
5
+ * A folder on the host as a backend: files and commands run as the host's own user. No wall: a path is checked
6
+ * against the root, but a symlink, or any command, reaches the whole host. For the host's own trusted work, and tests.
7
+ */
8
+ export function local({ root }) {
9
+ if (!isAbsolute(root))
10
+ throw new Error(`local: the root must be absolute, not "${root}"`);
11
+ return shellBackend({
12
+ id: `local:${root}`,
13
+ root,
14
+ exec: ({ script, args = [], stdin, signal }) => run("sh", ["-c", script, "sh", ...args], {
15
+ cwd: root,
16
+ ...(stdin === undefined ? {} : { stdin }),
17
+ ...(signal === undefined ? {} : { signal }),
18
+ }),
19
+ });
20
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A path as a tool gives it, as a path from the root: "" for the root itself. undefined for a path the backend refuses:
3
+ * one that leaves the root (by `..`, or absolute and not under it), starts with `~`, or holds a control character,
4
+ * which a line-based listing cannot carry.
5
+ */
6
+ export declare function fromRoot(root: string, path: string): string | undefined;
7
+ /** A path from the root as a shell argument: "." for the root. */
8
+ export declare const asArg: (p: string) => string;
9
+ /** Whether `path` is `folder` or under it; every path is under the root (""). */
10
+ export declare const isUnder: (path: string, folder: string) => boolean;
@@ -0,0 +1,31 @@
1
+ import { posix } from "node:path";
2
+ /**
3
+ * A path as a tool gives it, as a path from the root: "" for the root itself. undefined for a path the backend refuses:
4
+ * one that leaves the root (by `..`, or absolute and not under it), starts with `~`, or holds a control character,
5
+ * which a line-based listing cannot carry.
6
+ */
7
+ export function fromRoot(root, path) {
8
+ const given = path.trim();
9
+ // A control character, such as a newline: no file of a backend is named with one.
10
+ // eslint-disable-next-line no-control-regex
11
+ if (/[\u0000-\u001f\u007f]/.test(given) || given.startsWith("~"))
12
+ return undefined;
13
+ let p = given === "" ? "." : given;
14
+ if (posix.isAbsolute(p)) {
15
+ const top = posix.normalize(root).replace(/\/+$/, "");
16
+ const at = posix.normalize(p).replace(/\/+$/, "");
17
+ if (at === top)
18
+ return "";
19
+ if (!at.startsWith(`${top}/`))
20
+ return undefined;
21
+ p = at.slice(top.length + 1);
22
+ }
23
+ const n = posix.normalize(p).replace(/\/+$/, "");
24
+ if (n === ".." || n.startsWith("../"))
25
+ return undefined;
26
+ return n === "." ? "" : n;
27
+ }
28
+ /** A path from the root as a shell argument: "." for the root. */
29
+ export const asArg = (p) => p || ".";
30
+ /** Whether `path` is `folder` or under it; every path is under the root (""). */
31
+ export const isUnder = (path, folder) => folder === "" || path === folder || path.startsWith(`${folder}/`);
@@ -0,0 +1,38 @@
1
+ import type { SandboxBackend } from "./backend.ts";
2
+ /** A folder of the host in the container. */
3
+ export type Mount = {
4
+ source: string;
5
+ target: string;
6
+ readOnly?: boolean;
7
+ };
8
+ /** One container, as the host sets it: what it runs, what it sees, and what bounds it. */
9
+ export type PodmanOptions = {
10
+ /** The container's name: one per sandbox, so a process that starts again finds it. */
11
+ name: string;
12
+ image: string;
13
+ /** The host folders it sees. Nothing else of the host is in it. */
14
+ mounts: readonly Mount[];
15
+ /** Its root: every path is under it. */
16
+ workdir?: string;
17
+ /** "none" by default: no network. */
18
+ network?: string;
19
+ /** The user commands run as, such as "1000:1000". */
20
+ user?: string;
21
+ /** Such as "4g". */
22
+ memory?: string;
23
+ /** Such as 3. */
24
+ cpus?: number;
25
+ /** Any other `podman run` options, such as `--userns=keep-id`. */
26
+ runArgs?: readonly string[];
27
+ };
28
+ /** A Podman container that outlives the process, as a backend. */
29
+ export type Podman = SandboxBackend & {
30
+ /** Remove the container. The next call makes it again. */
31
+ close(): Promise<void>;
32
+ };
33
+ /**
34
+ * A Podman container as a backend: every operation execs into it, so its mounts, its network and its limits are the
35
+ * 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
36
+ * resumes after a restart works with no host step. One made with other options is refused, never used.
37
+ */
38
+ export declare function podman(options: PodmanOptions): Podman;
@@ -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,208 @@
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
+ // By path, then line, as glob lists them: the same order on any file system.
161
+ files.sort();
162
+ // No line holds a newline, so a text with one is in no file.
163
+ if (pattern.includes("\n"))
164
+ return { matches: [] };
165
+ const matches = [];
166
+ for (let i = 0; i < files.length && matches.length <= maxMatches; i += batch) {
167
+ const some = files.slice(i, i + batch);
168
+ const ran = await exec({
169
+ script: `p="$1"; shift; grep -nHF -e "$p" -- "$@"`,
170
+ args: [pattern, ...some],
171
+ });
172
+ // 1: no match. 2: a file it could not read, such as one gone since the listing; the others still count.
173
+ if (ran.code !== 0 && ran.code !== 1 && ran.code !== 2)
174
+ failed("grep", ran);
175
+ // A line is "<file>:<n>:<text>", and a file's name can hold ":": the longest file of the batch that starts it.
176
+ const named = [...some].sort((a, b) => b.length - a.length);
177
+ for (const line of ran.stdout.split("\n")) {
178
+ const file = named.find((f) => line.startsWith(`${f}:`));
179
+ if (!file)
180
+ continue;
181
+ const rest = line.slice(file.length + 1);
182
+ const colon = rest.indexOf(":");
183
+ const n = Number(rest.slice(0, colon));
184
+ if (colon < 1 || !Number.isInteger(n))
185
+ continue;
186
+ matches.push({ path: file, line: n, text: rest.slice(colon + 1) });
187
+ }
188
+ }
189
+ const truncated = matches.length > maxMatches;
190
+ return {
191
+ matches: matches.slice(0, maxMatches),
192
+ ...(truncated ? { truncated } : {}),
193
+ };
194
+ },
195
+ async execute(command, options = {}) {
196
+ const ran = await exec({
197
+ script: `exec 2>&1; eval "$1"`,
198
+ args: [command],
199
+ ...(options.signal ? { signal: options.signal } : {}),
200
+ });
201
+ return {
202
+ output: ran.stdout + ran.stderr,
203
+ exitCode: ran.code,
204
+ truncated: ran.truncated,
205
+ };
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,85 @@
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 ExecuteResult, 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
+ /**
44
+ * What the model reads for a command that ran: the tail of its output, at most `maxAnswer` characters, where a failure
45
+ * is, then how it ended. `execute`'s answer, for a host tool that runs a command to answer the same way.
46
+ */
47
+ export declare function commandAnswer(result: ExecuteResult): string;
48
+ /** The tools that only read. */
49
+ export declare const readTools: readonly [import("../define/tool.ts").Tool<z.ZodObject<{
50
+ path: z.ZodDefault<z.ZodString>;
51
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
52
+ file_path: z.ZodString;
53
+ offset: z.ZodDefault<z.ZodNumber>;
54
+ limit: z.ZodDefault<z.ZodNumber>;
55
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
56
+ pattern: z.ZodString;
57
+ path: z.ZodDefault<z.ZodString>;
58
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
59
+ pattern: z.ZodString;
60
+ path: z.ZodDefault<z.ZodString>;
61
+ glob: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strip>, Json | undefined>];
63
+ /** The tools that read and write files. `execute` is given on its own. */
64
+ export declare const fileTools: readonly [import("../define/tool.ts").Tool<z.ZodObject<{
65
+ path: z.ZodDefault<z.ZodString>;
66
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
67
+ file_path: z.ZodString;
68
+ offset: z.ZodDefault<z.ZodNumber>;
69
+ limit: z.ZodDefault<z.ZodNumber>;
70
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
71
+ pattern: z.ZodString;
72
+ path: z.ZodDefault<z.ZodString>;
73
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
74
+ pattern: z.ZodString;
75
+ path: z.ZodDefault<z.ZodString>;
76
+ glob: z.ZodOptional<z.ZodString>;
77
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
78
+ file_path: z.ZodString;
79
+ content: z.ZodString;
80
+ }, z.core.$strip>, Json | undefined>, import("../define/tool.ts").Tool<z.ZodObject<{
81
+ file_path: z.ZodString;
82
+ old_string: z.ZodString;
83
+ new_string: z.ZodString;
84
+ replace_all: z.ZodDefault<z.ZodBoolean>;
85
+ }, z.core.$strip>, Json | undefined>];
@@ -0,0 +1,248 @@
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 { defineResource, defineTool, } from "../define/index.js";
8
+ import { canExecute, } from "./backend.js";
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 const Sandbox = defineResource("sandbox");
11
+ /** The most of a tool's answer the model gets (Deep Agents: 20,000 tokens, at four characters each). */
12
+ export const maxAnswer = 80_000;
13
+ const cutNote = "\n... [results truncated, try being more specific with your parameters]";
14
+ /** An answer cut to the most the model gets, at a whole line. */
15
+ function cut(text) {
16
+ if (text.length <= maxAnswer)
17
+ return text;
18
+ const head = text.slice(0, maxAnswer);
19
+ return head.slice(0, Math.max(head.lastIndexOf("\n"), 0)) + cutNote;
20
+ }
21
+ /** What the model reads for a result that did nothing. Deep Agents' strings. */
22
+ function failure(result, op, path, old = "") {
23
+ switch (result.error) {
24
+ case "file_not_found":
25
+ return op === "read" || op === "edit"
26
+ ? `Error: File '${path}' not found`
27
+ : `Error: Path '${path}' not found`;
28
+ case "is_directory":
29
+ return `Error: '${path}' is a directory, not a file`;
30
+ case "invalid_path":
31
+ return `Error: Path traversal not allowed: ${path}`;
32
+ case "permission_denied":
33
+ return `Error: permission denied for ${op} on ${path}${result.message ? `: ${result.message}` : ""}`;
34
+ case "string_not_found":
35
+ return `Error: String not found in file: '${old}'`;
36
+ case "string_not_unique":
37
+ return `Error: String '${old}' appears ${result.occurrences ?? "several"} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context.`;
38
+ default:
39
+ return undefined;
40
+ }
41
+ }
42
+ const sandbox = (runtime) => runtime.use(Sandbox);
43
+ /** A path the model gives. */
44
+ const path = () => z
45
+ .string()
46
+ .describe("A path in the sandbox: relative to its root, or absolute under it.");
47
+ /** A folder the model may leave out: the root. */
48
+ const folder = () => path().default(".");
49
+ export const ls = defineTool({
50
+ name: "ls",
51
+ description: "Lists the files and folders in a folder of the sandbox. A folder ends in a slash.",
52
+ schema: z.object({ path: folder() }),
53
+ uses: [Sandbox],
54
+ replay: "safe",
55
+ run: async ({ path }, runtime) => {
56
+ const result = await (await sandbox(runtime)).ls(path);
57
+ const failed = failure(result, "ls", path);
58
+ if (failed)
59
+ return failed;
60
+ const files = result.files ?? [];
61
+ if (files.length === 0)
62
+ return `No files found in '${path}'`;
63
+ return cut(files.map((f) => (f.isDir ? `${f.path}/` : f.path)).join("\n"));
64
+ },
65
+ });
66
+ export const read_file = defineTool({
67
+ name: "read_file",
68
+ description: "Reads a file from the sandbox. Assume any path the user provides is valid; reading a missing file returns an error. " +
69
+ "By default it reads up to 100 lines from the start; use offset and limit to page through a large file. " +
70
+ "A status header, `@@ lines A-B of T | next offset N @@`, sits above the file content, and every line after it is " +
71
+ "the file's content, verbatim. Never include the header when editing. Always read a file before editing it.",
72
+ schema: z.object({
73
+ file_path: path(),
74
+ offset: z
75
+ .number()
76
+ .int()
77
+ .min(0)
78
+ .default(0)
79
+ .describe("Line offset to start reading from (0-indexed)."),
80
+ limit: z
81
+ .number()
82
+ .int()
83
+ .min(1)
84
+ .default(100)
85
+ .describe("Maximum number of lines to read. Use for pagination of large files."),
86
+ }),
87
+ uses: [Sandbox],
88
+ replay: "safe",
89
+ run: async ({ file_path, offset, limit }, runtime) => {
90
+ const result = await (await sandbox(runtime)).read(file_path, offset, limit);
91
+ const failed = failure(result, "read", file_path);
92
+ if (failed)
93
+ return failed;
94
+ const total = result.totalLines ?? 0;
95
+ if (total === 0)
96
+ return "System reminder: File exists but has empty contents";
97
+ if (offset >= total)
98
+ return `Error: Line offset ${offset} exceeds file length (${total} lines)`;
99
+ let content = result.content ?? "";
100
+ let note = "";
101
+ if (content.length > maxAnswer) {
102
+ const head = content.slice(0, maxAnswer);
103
+ content = head.slice(0, head.lastIndexOf("\n") + 1);
104
+ note = " | truncated due to size";
105
+ }
106
+ const lines = content.split("\n").length - 1;
107
+ const end = offset + lines;
108
+ const next = end < total ? ` | next offset ${end}` : "";
109
+ return `@@ lines ${offset + 1}-${end} of ${total}${next}${note} @@\n${content}`;
110
+ },
111
+ });
112
+ export const write_file = defineTool({
113
+ name: "write_file",
114
+ description: "Writes content to a file in the sandbox. Creates the file, and its folder, if it does not exist; replaces it entirely if it does.",
115
+ schema: z.object({
116
+ file_path: path(),
117
+ content: z
118
+ .string()
119
+ .describe("The text content to write to the file. This parameter is required."),
120
+ }),
121
+ uses: [Sandbox],
122
+ // It writes the whole file, so a second run leaves the same file as one.
123
+ replay: "safe",
124
+ run: async ({ file_path, content }, runtime) => {
125
+ const result = await (await sandbox(runtime)).write(file_path, content);
126
+ return (failure(result, "write", file_path) ??
127
+ `Successfully wrote to '${file_path}'`);
128
+ },
129
+ });
130
+ export const edit_file = defineTool({
131
+ name: "edit_file",
132
+ description: "Performs exact string replacements in a file of the sandbox. old_string must occur exactly once, unless " +
133
+ "replace_all is true, which replaces each one. You must read the file before editing.",
134
+ schema: z.object({
135
+ file_path: path(),
136
+ old_string: z
137
+ .string()
138
+ .min(1)
139
+ .describe("The exact text to find and replace. Must be unique in the file unless replace_all is true."),
140
+ new_string: z.string().describe("The text to replace it with."),
141
+ replace_all: z
142
+ .boolean()
143
+ .default(false)
144
+ .describe("Replace every occurrence of old_string."),
145
+ }),
146
+ uses: [Sandbox],
147
+ // Not replay-safe: a second replace finds nothing, or replaces twice.
148
+ run: async ({ file_path, old_string, new_string, replace_all }, runtime) => {
149
+ const result = await (await sandbox(runtime)).edit(file_path, old_string, new_string, replace_all);
150
+ return (failure(result, "edit", file_path, old_string) ??
151
+ `Successfully replaced ${result.occurrences} occurrence(s) in '${file_path}'`);
152
+ },
153
+ });
154
+ export const glob = defineTool({
155
+ name: "glob",
156
+ description: "Finds the files of the sandbox whose path matches a glob pattern, under a folder: `*` matches within a folder, " +
157
+ "`**` across folders, as in `**/*.test.ts`. Paths are from the sandbox's root.",
158
+ schema: z.object({
159
+ pattern: z
160
+ .string()
161
+ .min(1)
162
+ .describe("The glob pattern, matched against the path from `path`."),
163
+ path: folder(),
164
+ }),
165
+ uses: [Sandbox],
166
+ replay: "safe",
167
+ run: async ({ pattern, path }, runtime) => {
168
+ const result = await (await sandbox(runtime)).glob(pattern, path);
169
+ const failed = failure(result, "glob", path);
170
+ if (failed)
171
+ return failed;
172
+ const files = result.files ?? [];
173
+ if (files.length === 0)
174
+ return `No files found matching pattern '${pattern}'`;
175
+ return cut(files.join("\n") + (result.truncated ? cutNote : ""));
176
+ },
177
+ });
178
+ export const grep = defineTool({
179
+ name: "grep",
180
+ description: "Search for a LITERAL text pattern across files (NOT regex), under a path, in files matching glob if given. " +
181
+ "Gives each matching file, then each matching line with its number.",
182
+ schema: z.object({
183
+ pattern: z.string().min(1).describe("The text to find, as is."),
184
+ path: folder(),
185
+ glob: z
186
+ .string()
187
+ .min(1)
188
+ .optional()
189
+ .describe("Only files whose path from `path` matches this glob, such as `*.ts`."),
190
+ }),
191
+ uses: [Sandbox],
192
+ replay: "safe",
193
+ run: async ({ pattern, path, glob }, runtime) => {
194
+ const result = await (await sandbox(runtime)).grep(pattern, path, glob);
195
+ const failed = failure(result, "grep", path);
196
+ if (failed)
197
+ return failed;
198
+ const matches = result.matches ?? [];
199
+ if (matches.length === 0)
200
+ return `No matches found for pattern '${pattern}'`;
201
+ const byFile = new Map();
202
+ for (const m of matches) {
203
+ const lines = byFile.get(m.path) ?? [];
204
+ lines.push(` ${m.line}: ${m.text}`);
205
+ byFile.set(m.path, lines);
206
+ }
207
+ const text = [...byFile]
208
+ .map(([f, lines]) => `${f}:\n${lines.join("\n")}`)
209
+ .join("\n");
210
+ return cut(text + (result.truncated ? cutNote : ""));
211
+ },
212
+ });
213
+ export const execute = defineTool({
214
+ name: "execute",
215
+ description: "Executes a shell command in the sandbox, in its root, and returns its combined stdout and stderr with the exit " +
216
+ "code (the tail, if very large).",
217
+ schema: z.object({
218
+ command: z.string().min(1).describe("The shell command to run."),
219
+ }),
220
+ uses: [Sandbox],
221
+ // Not replay-safe: a command can do anything.
222
+ run: async ({ command }, runtime) => {
223
+ const backend = await sandbox(runtime);
224
+ if (!canExecute(backend)) {
225
+ return "Error: Execution not available. This agent's backend does not support command execution.";
226
+ }
227
+ return commandAnswer(await backend.execute(command));
228
+ },
229
+ });
230
+ /**
231
+ * What the model reads for a command that ran: the tail of its output, at most `maxAnswer` characters, where a failure
232
+ * is, then how it ended. `execute`'s answer, for a host tool that runs a command to answer the same way.
233
+ */
234
+ export function commandAnswer(result) {
235
+ const cutTail = result.output.length > maxAnswer;
236
+ const output = cutTail ? result.output.slice(-maxAnswer) : result.output;
237
+ const status = result.exitCode === null
238
+ ? "[Command was killed]"
239
+ : `[Command ${result.exitCode === 0 ? "succeeded" : "failed"} with exit code ${result.exitCode}]`;
240
+ const truncated = result.truncated || cutTail
241
+ ? "\n[Output was truncated due to size limits]"
242
+ : "";
243
+ return `${output}${output.endsWith("\n") || output === "" ? "" : "\n"}${status}${truncated}`;
244
+ }
245
+ /** The tools that only read. */
246
+ export const readTools = [ls, read_file, glob, grep];
247
+ /** The tools that read and write files. `execute` is given on its own. */
248
+ export const fileTools = [...readTools, write_file, edit_file];
@@ -0,0 +1,17 @@
1
+ import type { FileBackend } from "./backend.ts";
2
+ /** The files a view holds: paths from the backend's root, or absolute under it. */
3
+ export type ViewPaths = {
4
+ /** The files it may read. */
5
+ read: readonly string[];
6
+ /** The files it may write: it may read them too. */
7
+ write: readonly string[];
8
+ };
9
+ /**
10
+ * A backend over a backend, limited to some files: to its tools, a path outside the view does not exist, and a write
11
+ * outside its write files is refused. It has no execute, since a shell reaches any path. It checks names, so it is a
12
+ * wall only over a backend that is one, such as a container whose mounts hold no more than the view's folder.
13
+ *
14
+ * Deep Agents' path rules answer a path outside them as denied; a view answers it as missing, so the model keeps to its
15
+ * task and does not probe.
16
+ */
17
+ export declare function view(backend: FileBackend, paths: ViewPaths): FileBackend;
@@ -0,0 +1,87 @@
1
+ import { fromRoot, isUnder } from "./paths.js";
2
+ /**
3
+ * A backend over a backend, limited to some files: to its tools, a path outside the view does not exist, and a write
4
+ * outside its write files is refused. It has no execute, since a shell reaches any path. It checks names, so it is a
5
+ * wall only over a backend that is one, such as a container whose mounts hold no more than the view's folder.
6
+ *
7
+ * Deep Agents' path rules answer a path outside them as denied; a view answers it as missing, so the model keeps to its
8
+ * task and does not probe.
9
+ */
10
+ export function view(backend, paths) {
11
+ const { root } = backend;
12
+ const checked = (list) => list.map((p) => {
13
+ const at = fromRoot(root, p);
14
+ if (!at)
15
+ throw new Error(`view: "${p}" is not a file under ${root}`);
16
+ return at;
17
+ });
18
+ const writes = new Set(checked(paths.write));
19
+ const files = [...new Set([...checked(paths.read), ...writes])];
20
+ const mine = new Set(files);
21
+ /** A path of the view: one of its files, or a folder on the way to one. */
22
+ const sees = (at) => files.some((f) => f === at || isUnder(f, at));
23
+ /** A path the view resolves, or undefined when it is outside the root. */
24
+ const at = (path) => fromRoot(root, path);
25
+ const missing = { error: "file_not_found" };
26
+ const invalid = { error: "invalid_path" };
27
+ /** A path the backend gave back that is one of the view's files. */
28
+ const own = (path) => mine.has(path);
29
+ const refused = (path) => ({
30
+ error: "permission_denied",
31
+ message: `${path} is not a file this view may write: it may write ${[...writes].join(", ") || "none"}`,
32
+ });
33
+ return {
34
+ root,
35
+ async ls(path) {
36
+ const p = at(path);
37
+ if (p === undefined)
38
+ return invalid;
39
+ if (!sees(p))
40
+ return missing;
41
+ const listed = await backend.ls(path);
42
+ return listed.files
43
+ ? { files: listed.files.filter((f) => sees(f.path)) }
44
+ : listed;
45
+ },
46
+ async read(path, offset, limit) {
47
+ const p = at(path);
48
+ if (p === undefined)
49
+ return invalid;
50
+ return sees(p) ? backend.read(path, offset, limit) : missing;
51
+ },
52
+ async write(path, content) {
53
+ const p = at(path);
54
+ if (p === undefined)
55
+ return invalid;
56
+ return writes.has(p) ? backend.write(path, content) : refused(path);
57
+ },
58
+ async edit(path, old, next, all) {
59
+ const p = at(path);
60
+ if (p === undefined)
61
+ return invalid;
62
+ return writes.has(p) ? backend.edit(path, old, next, all) : refused(path);
63
+ },
64
+ // A search runs once on the backend, and the view keeps its own files: Deep Agents filters its path rules' results
65
+ // the same way. The backend's limit counts files outside the view too, so a cut result says it is cut.
66
+ async glob(pattern, path) {
67
+ const p = at(path);
68
+ if (p === undefined)
69
+ return invalid;
70
+ if (!sees(p))
71
+ return missing;
72
+ const found = await backend.glob(pattern, path);
73
+ return found.files ? { ...found, files: found.files.filter(own) } : found;
74
+ },
75
+ async grep(pattern, path, glob) {
76
+ const p = at(path);
77
+ if (p === undefined)
78
+ return invalid;
79
+ if (!sees(p))
80
+ return missing;
81
+ const found = await backend.grep(pattern, path, glob);
82
+ return found.matches
83
+ ? { ...found, matches: found.matches.filter((m) => own(m.path)) }
84
+ : found;
85
+ },
86
+ };
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mku1tra/que-pasa",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "An agent harness on pi-durable: define agents and tools with define/, and run them durably and traced with loop/.",
5
5
  "license": "MIT",
6
6
  "repository": {