@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,242 @@
|
|
|
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
|
+
const result = await backend.execute(command);
|
|
228
|
+
const cutTail = result.output.length > maxAnswer;
|
|
229
|
+
const output = cutTail ? result.output.slice(-maxAnswer) : result.output;
|
|
230
|
+
const status = result.exitCode === null
|
|
231
|
+
? "[Command was killed]"
|
|
232
|
+
: `[Command ${result.exitCode === 0 ? "succeeded" : "failed"} with exit code ${result.exitCode}]`;
|
|
233
|
+
const truncated = result.truncated || cutTail
|
|
234
|
+
? "\n[Output was truncated due to size limits]"
|
|
235
|
+
: "";
|
|
236
|
+
return `${output}${output.endsWith("\n") || output === "" ? "" : "\n"}${status}${truncated}`;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
/** The tools that only read. */
|
|
240
|
+
export const readTools = [ls, read_file, glob, grep];
|
|
241
|
+
/** The tools that read and write files. `execute` is given on its own. */
|
|
242
|
+
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,105 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { fromRoot, isUnder } from "./paths.js";
|
|
3
|
+
/**
|
|
4
|
+
* A backend over a backend, limited to some files: to its tools, a path outside the view does not exist, and a write
|
|
5
|
+
* outside its write files is refused. It has no execute, since a shell reaches any path. It checks names, so it is a
|
|
6
|
+
* wall only over a backend that is one, such as a container whose mounts hold no more than the view's folder.
|
|
7
|
+
*
|
|
8
|
+
* Deep Agents' path rules answer a path outside them as denied; a view answers it as missing, so the model keeps to its
|
|
9
|
+
* task and does not probe.
|
|
10
|
+
*/
|
|
11
|
+
export function view(backend, paths) {
|
|
12
|
+
const { root } = backend;
|
|
13
|
+
const checked = (list) => list.map((p) => {
|
|
14
|
+
const at = fromRoot(root, p);
|
|
15
|
+
if (!at)
|
|
16
|
+
throw new Error(`view: "${p}" is not a file under ${root}`);
|
|
17
|
+
return at;
|
|
18
|
+
});
|
|
19
|
+
const writes = new Set(checked(paths.write));
|
|
20
|
+
const files = [...new Set([...checked(paths.read), ...writes])];
|
|
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
|
+
/** The view's files under a folder whose path from it matches a glob: its own list, never the folder's. */
|
|
28
|
+
const under = (folder, glob) => files.filter((f) => {
|
|
29
|
+
// A search leaves out .git, as a backend's does.
|
|
30
|
+
if (!isUnder(f, folder) || f.split("/").includes(".git"))
|
|
31
|
+
return false;
|
|
32
|
+
const from = f === folder
|
|
33
|
+
? posix.basename(f)
|
|
34
|
+
: folder
|
|
35
|
+
? f.slice(folder.length + 1)
|
|
36
|
+
: f;
|
|
37
|
+
return glob === undefined || posix.matchesGlob(from, glob);
|
|
38
|
+
});
|
|
39
|
+
/** Which of the files are there: a write file can be one not made yet. */
|
|
40
|
+
const present = async (list) => {
|
|
41
|
+
const there = await Promise.all(list.map(async (f) => {
|
|
42
|
+
const listed = await backend.ls(f);
|
|
43
|
+
return listed.files?.some((e) => e.path === f && !e.isDir) ?? false;
|
|
44
|
+
}));
|
|
45
|
+
return list.filter((_, i) => there[i]);
|
|
46
|
+
};
|
|
47
|
+
const refused = (path) => ({
|
|
48
|
+
error: "permission_denied",
|
|
49
|
+
message: `${path} is not a file this view may write: it may write ${[...writes].join(", ") || "none"}`,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
root,
|
|
53
|
+
async ls(path) {
|
|
54
|
+
const p = at(path);
|
|
55
|
+
if (p === undefined)
|
|
56
|
+
return invalid;
|
|
57
|
+
if (!sees(p))
|
|
58
|
+
return missing;
|
|
59
|
+
const listed = await backend.ls(path);
|
|
60
|
+
return listed.files
|
|
61
|
+
? { files: listed.files.filter((f) => sees(f.path)) }
|
|
62
|
+
: listed;
|
|
63
|
+
},
|
|
64
|
+
async read(path, offset, limit) {
|
|
65
|
+
const p = at(path);
|
|
66
|
+
if (p === undefined)
|
|
67
|
+
return invalid;
|
|
68
|
+
return sees(p) ? backend.read(path, offset, limit) : missing;
|
|
69
|
+
},
|
|
70
|
+
async write(path, content) {
|
|
71
|
+
const p = at(path);
|
|
72
|
+
if (p === undefined)
|
|
73
|
+
return invalid;
|
|
74
|
+
return writes.has(p) ? backend.write(path, content) : refused(path);
|
|
75
|
+
},
|
|
76
|
+
async edit(path, old, next, all) {
|
|
77
|
+
const p = at(path);
|
|
78
|
+
if (p === undefined)
|
|
79
|
+
return invalid;
|
|
80
|
+
return writes.has(p) ? backend.edit(path, old, next, all) : refused(path);
|
|
81
|
+
},
|
|
82
|
+
async glob(pattern, path) {
|
|
83
|
+
const p = at(path);
|
|
84
|
+
if (p === undefined)
|
|
85
|
+
return invalid;
|
|
86
|
+
if (!sees(p))
|
|
87
|
+
return missing;
|
|
88
|
+
return { files: (await present(under(p, pattern))).sort() };
|
|
89
|
+
},
|
|
90
|
+
async grep(pattern, path, glob) {
|
|
91
|
+
const p = at(path);
|
|
92
|
+
if (p === undefined)
|
|
93
|
+
return invalid;
|
|
94
|
+
if (!sees(p))
|
|
95
|
+
return missing;
|
|
96
|
+
// Each of its files on its own, so files outside the view are never read.
|
|
97
|
+
const matches = [];
|
|
98
|
+
for (const f of (await present(under(p, glob))).sort()) {
|
|
99
|
+
const found = await backend.grep(pattern, f);
|
|
100
|
+
matches.push(...(found.matches ?? []));
|
|
101
|
+
}
|
|
102
|
+
return { matches };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mku1tra/que-pasa",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"./loop": {
|
|
21
21
|
"types": "./dist/loop/index.d.ts",
|
|
22
22
|
"default": "./dist/loop/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./sandbox": {
|
|
25
|
+
"types": "./dist/sandbox/index.d.ts",
|
|
26
|
+
"default": "./dist/sandbox/index.js"
|
|
23
27
|
}
|
|
24
28
|
},
|
|
25
29
|
"files": [
|