@mku1tra/que-pasa 0.2.1 → 0.2.3

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 CHANGED
@@ -51,7 +51,7 @@ Install it from npm: `npm install @mku1tra/que-pasa zod`. zod is a peer dependen
51
51
 
52
52
  ## Release
53
53
 
54
- Set the version in `package.json`, commit, land, and push its tag, `v<version>`. `.github/workflows/publish.yml` runs the check set and publishes on npm by trusted publishing (no token, with provenance). A ruleset lets only an admin of the repo make a version tag. A version npm has is not published again. A published version cannot change: npm unpublishes only within 72 hours.
54
+ A land is a version when its commits ask for one: crate reads their types (Conventional Commits) on the server, tags main `v<version>`, and copies the tag to GitHub ([crate](https://github.com/matta-kelly/crate#release)). While que-pasa is at 0.x, a breaking change bumps minor, and `feat`, `fix` and `perf` bump patch. `.github/workflows/publish.yml` runs the check set, sets `package.json`'s version from the tag, and publishes on npm by trusted publishing (no token, with provenance). `package.json`'s version in git is not kept: the tag is the version. A ruleset lets only crate's deploy key, or an admin of the repo, make a version tag. A version npm has is not published again. A published version cannot change: npm unpublishes only within 72 hours.
55
55
 
56
56
  ## Ship
57
57
 
@@ -66,7 +66,7 @@ export interface FileBackend {
66
66
  edit(path: string, old: string, next: string, all: boolean): Promise<EditResult>;
67
67
  /** The files under a folder whose path from that folder matches a glob. */
68
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. */
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
70
  grep(pattern: string, path: string, glob?: string): Promise<GrepResult>;
71
71
  }
72
72
  /** Files, and commands: a backend that can run a shell command. */
@@ -3,5 +3,5 @@ export { local } from "./local.ts";
3
3
  export { podman, type Mount, type Podman, type PodmanOptions, } from "./podman.ts";
4
4
  export { shellBackend, type Exec } from "./shell.ts";
5
5
  export type { Ran, Script } from "./process.ts";
6
- export { Sandbox, edit_file, execute, fileTools, glob, grep, ls, read_file, readTools, write_file, } from "./tools.ts";
6
+ export { Sandbox, commandAnswer, edit_file, execute, fileTools, glob, grep, ls, maxAnswer, read_file, readTools, write_file, } from "./tools.ts";
7
7
  export { view, type ViewPaths } from "./view.ts";
@@ -2,5 +2,5 @@ export { canExecute, } from "./backend.js";
2
2
  export { local } from "./local.js";
3
3
  export { podman, } from "./podman.js";
4
4
  export { shellBackend } from "./shell.js";
5
- export { Sandbox, edit_file, execute, fileTools, glob, grep, ls, read_file, readTools, write_file, } from "./tools.js";
5
+ export { Sandbox, commandAnswer, edit_file, execute, fileTools, glob, grep, ls, maxAnswer, read_file, readTools, write_file, } from "./tools.js";
6
6
  export { view } from "./view.js";
@@ -157,6 +157,8 @@ else exit ${missing}; fi`,
157
157
  const files = glob ? await matching(at, glob) : await filesUnder(at);
158
158
  if (!files)
159
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();
160
162
  // No line holds a newline, so a text with one is in no file.
161
163
  if (pattern.includes("\n"))
162
164
  return { matches: [] };
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { z } from "zod";
7
7
  import { type Json } from "../define/index.ts";
8
- import { type FileBackend } from "./backend.ts";
8
+ import { type ExecuteResult, type FileBackend } from "./backend.ts";
9
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
10
  export declare const Sandbox: import("../define/resource.ts").Resource<FileBackend>;
11
11
  /** The most of a tool's answer the model gets (Deep Agents: 20,000 tokens, at four characters each). */
@@ -40,6 +40,11 @@ export declare const grep: import("../define/tool.ts").Tool<z.ZodObject<{
40
40
  export declare const execute: import("../define/tool.ts").Tool<z.ZodObject<{
41
41
  command: z.ZodString;
42
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;
43
48
  /** The tools that only read. */
44
49
  export declare const readTools: readonly [import("../define/tool.ts").Tool<z.ZodObject<{
45
50
  path: z.ZodDefault<z.ZodString>;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { z } from "zod";
7
7
  import { defineResource, defineTool, } from "../define/index.js";
8
- import { canExecute } from "./backend.js";
8
+ import { canExecute, } from "./backend.js";
9
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
10
  export const Sandbox = defineResource("sandbox");
11
11
  /** The most of a tool's answer the model gets (Deep Agents: 20,000 tokens, at four characters each). */
@@ -224,18 +224,24 @@ export const execute = defineTool({
224
224
  if (!canExecute(backend)) {
225
225
  return "Error: Execution not available. This agent's backend does not support command execution.";
226
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}`;
227
+ return commandAnswer(await backend.execute(command));
237
228
  },
238
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
+ }
239
245
  /** The tools that only read. */
240
246
  export const readTools = [ls, read_file, glob, grep];
241
247
  /** The tools that read and write files. `execute` is given on its own. */
@@ -1,4 +1,3 @@
1
- import { posix } from "node:path";
2
1
  import { fromRoot, isUnder } from "./paths.js";
3
2
  /**
4
3
  * A backend over a backend, limited to some files: to its tools, a path outside the view does not exist, and a write
@@ -18,32 +17,15 @@ export function view(backend, paths) {
18
17
  });
19
18
  const writes = new Set(checked(paths.write));
20
19
  const files = [...new Set([...checked(paths.read), ...writes])];
20
+ const mine = new Set(files);
21
21
  /** A path of the view: one of its files, or a folder on the way to one. */
22
22
  const sees = (at) => files.some((f) => f === at || isUnder(f, at));
23
23
  /** A path the view resolves, or undefined when it is outside the root. */
24
24
  const at = (path) => fromRoot(root, path);
25
25
  const missing = { error: "file_not_found" };
26
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
- };
27
+ /** A path the backend gave back that is one of the view's files. */
28
+ const own = (path) => mine.has(path);
47
29
  const refused = (path) => ({
48
30
  error: "permission_denied",
49
31
  message: `${path} is not a file this view may write: it may write ${[...writes].join(", ") || "none"}`,
@@ -79,13 +61,16 @@ export function view(backend, paths) {
79
61
  return invalid;
80
62
  return writes.has(p) ? backend.edit(path, old, next, all) : refused(path);
81
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.
82
66
  async glob(pattern, path) {
83
67
  const p = at(path);
84
68
  if (p === undefined)
85
69
  return invalid;
86
70
  if (!sees(p))
87
71
  return missing;
88
- return { files: (await present(under(p, pattern))).sort() };
72
+ const found = await backend.glob(pattern, path);
73
+ return found.files ? { ...found, files: found.files.filter(own) } : found;
89
74
  },
90
75
  async grep(pattern, path, glob) {
91
76
  const p = at(path);
@@ -93,13 +78,10 @@ export function view(backend, paths) {
93
78
  return invalid;
94
79
  if (!sees(p))
95
80
  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 };
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;
103
85
  },
104
86
  };
105
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mku1tra/que-pasa",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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": {