@better-fs-tools/pi 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @better-fs-tools/pi
2
2
 
3
- Better FS Tools for the [Pi coding agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent). It replaces Pi's built-in `read`, `edit`, and `write` tools, and can add `apply_patch`. Every call is confined to the working directory of that call. A read store, so that `edit` and `write` need a read first, is opt-in.
3
+ Better FS Tools for the [Pi coding agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) and for `@earendil-works/pi-agent-core`. It replaces Pi's built-in `read`, `edit`, and `write` tools, and can add `apply_patch` and `bash`. The tools take the options of every Better FS Tools integration, with the same defaults, plus Pi's prompt options. `piCwdFileSystem()` from the `./node` subpath confines each call to its working directory.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,9 +8,9 @@ Better FS Tools for the [Pi coding agent](https://www.npmjs.com/package/@earendi
8
8
  npm install @better-fs-tools/pi @better-fs-tools/read @better-fs-tools/write @earendil-works/pi-coding-agent typebox
9
9
  ```
10
10
 
11
- `@earendil-works/pi-coding-agent` (`^0.84.2`) and `typebox` (`^1.3.16`) are required peers. The package needs Node 24 or later.
11
+ `@earendil-works/pi-coding-agent` (`^1.0.0`) and `typebox` (`^1.3.16`) are required peers. Pi versions before 1.0 are not supported. The package needs Node 24 or later.
12
12
 
13
- The package has a `pi.extensions` entry. When Pi loads the package, the entry registers `read`, `edit`, and `write` from `createPiFsTools()`, as Pi's own tools are. `read` returns images, as Pi's own `read` does. There is no read store, so `edit` and `write` do not check for a read first. Writes stay inside `ctx.cwd`. The entry writes no settings or session file.
13
+ The package has a `pi.extensions` entry. When Pi loads the package, the entry registers `read`, `edit`, and `write` from `createPiFsTools()`. They replace Pi's own tools of those names. The entry uses the recommended Pi configuration: `fs: piCwdFileSystem()`, so each call reads and writes inside `ctx.cwd`; `locks: piMutationQueueLocks()`, so Pi's own `edit` and `write` wait for these tools; and `digest: nodeDigest()`. Every other option has the shared default: `read` returns images, and there is no read store, so `edit` and `write` do not check for a read first. The entry registers `piToolRenderers()` so that Pi's built-in renderers draw the tools. It writes no settings or session file.
14
14
 
15
15
  ## Example
16
16
 
@@ -18,15 +18,20 @@ To choose your own options, register the tools from your own extension:
18
18
 
19
19
  ```ts
20
20
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
- import { createPiFsTools } from "@better-fs-tools/pi";
21
+ import { createPiFsTools, piMutationQueueLocks, piToolRenderers } from "@better-fs-tools/pi";
22
+ import { piCwdFileSystem } from "@better-fs-tools/pi/node";
23
+ import { nodeDigest } from "@better-fs-tools/node";
22
24
  import { denyPaths, memoryStore, unicodeRepair } from "@better-fs-tools/read";
23
25
  import { hashlineFormat } from "@better-fs-tools/read/formats";
24
26
  import { protectPaths } from "@better-fs-tools/write";
25
27
 
26
28
  export default function fsToolsExtension(pi: ExtensionAPI): void {
27
- // Four tools with one read store, so edit and write need a read first. The
28
- // store and apply_patch are opt-in.
29
29
  const tools = createPiFsTools({
30
+ // The recommended Pi configuration, as in the package's extension entry.
31
+ fs: piCwdFileSystem(), // a Node filesystem rooted at ctx.cwd on each call
32
+ locks: piMutationQueueLocks(), // the per-file queue of Pi's own edit and write
33
+ digest: nodeDigest(), // faster on Node than the default sha256Digest()
34
+ // Opt-ins: one read store, so edit and write need a read first, and apply_patch.
30
35
  state: memoryStore(),
31
36
  read: {
32
37
  resolve: unicodeRepair({ note: false }),
@@ -41,6 +46,8 @@ export default function fsToolsExtension(pi: ExtensionAPI): void {
41
46
  pi.registerTool(tools.edit);
42
47
  pi.registerTool(tools.write);
43
48
  pi.registerTool(tools.applyPatch);
49
+ // Pi's built-in read, edit, and write renderers draw the shared signatures.
50
+ pi.registerToolRenderer(piToolRenderers([tools.read, tools.edit, tools.write]));
44
51
  }
45
52
  ```
46
53
 
@@ -49,6 +56,7 @@ To register only the read tool:
49
56
  ```ts
50
57
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
51
58
  import { createPiReadTool } from "@better-fs-tools/pi";
59
+ import { piCwdFileSystem } from "@better-fs-tools/pi/node";
52
60
  import { denyPaths, unicodeRepair } from "@better-fs-tools/read";
53
61
  import { hashlineFormat } from "@better-fs-tools/read/formats";
54
62
  import { defaultReadSignature } from "@better-fs-tools/read/signature";
@@ -56,6 +64,7 @@ import { defaultReadSignature } from "@better-fs-tools/read/signature";
56
64
  export default function readExtension(pi: ExtensionAPI): void {
57
65
  pi.registerTool(
58
66
  createPiReadTool({
67
+ fs: piCwdFileSystem(),
59
68
  signature: defaultReadSignature({ name: "read", names: { path: "file_path" } }),
60
69
  resolve: unicodeRepair({ note: false }),
61
70
  authorize: denyPaths(["**/.env", "**/.env.*"]),
@@ -65,78 +74,194 @@ export default function readExtension(pi: ExtensionAPI): void {
65
74
  }
66
75
  ```
67
76
 
68
- ## What the read tool does
77
+ ## Use the Pi SDK
69
78
 
70
- - `createPiReadTool(options?)` takes every `createReadTool()` option except `fs`. It also takes `signature`, `promptSnippet`, `promptGuidelines`, `denyRoots`, and `symlinks`.
71
- - `fs`, `cwd`, and `allowedRoots` throw `TypeError`. Each call reads through `nodeFileSystem` with Pi's `ctx.cwd` as the only allowed root. When the working directory changes between calls, the root changes too. The tool keeps the filesystems for the last 8 working directories.
72
- - Pi's `ExtensionContext` is the host type. Host functions get it as `ctx.call.host`, for example `ctx.call.host.ui.confirm(...)` in `askUser`, or `ctx.call.host.sessionManager.getSessionId()` in a `state` factory.
73
- - The default signature is `defaultReadSignature({ name: "read" })`. Its schema has integer `offset` and `limit`, so a model that sends `2.5` gets Pi's validation error. `digest` defaults to `nodeDigest()`.
74
- - `converters` defaults to `[imageConverter()]`, as Pi's own `read` returns images. This holds for `createPiReadTool()`, `createPiFsTools()`, and the extension entry. A list you pass replaces the default, so `converters: []` turns images off, and a list without `imageConverter()` refuses images. In `createPiFsTools()` the option goes under `read`, for example `read: { converters: [] }`.
75
- - Image parts become Pi `image` parts. Other media becomes a text part that says what was left out.
76
- - `details.truncation` follows Pi's own read tool for line and byte stops. Its `content` comes from the formatter in `"view"` mode. For other results, or a formatter that returns parts, `details` is `{}`. `toPiReadDetails()` builds the details for your own tool.
79
+ The tools also work with `createAgentSession()` from the Pi SDK. Give them to `customTools`. A custom tool with the name of a built-in tool replaces it:
80
+
81
+ ```ts
82
+ import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
83
+ import { createPiFsTools } from "@better-fs-tools/pi";
84
+ import { piCwdFileSystem } from "@better-fs-tools/pi/node";
85
+ import { memoryStore } from "@better-fs-tools/read";
86
+
87
+ const cwd = "/work/repo";
88
+ const tools = createPiFsTools({ fs: piCwdFileSystem(), state: memoryStore(), applyPatch: true });
89
+
90
+ // read, edit, and write have the names of Pi's built-in tools, so they replace
91
+ // them. Pi's own bash stays.
92
+ const { session } = await createAgentSession({
93
+ cwd,
94
+ sessionManager: SessionManager.inMemory(cwd),
95
+ customTools: [tools.read, tools.edit, tools.write, tools.applyPatch],
96
+ });
97
+
98
+ try {
99
+ await session.prompt("Rename greet() to welcome() in src/greet.ts.");
100
+ } finally {
101
+ session.dispose();
102
+ }
103
+ ```
104
+
105
+ The session's `cwd` is the root of every call. For `tools`, `noTools`, and inline extensions with hooks and guards, see [Use Pi from code (SDK)](https://better-fs-tools.gandrew.com/integrations/pi#use-pi-from-code-sdk). For `pi-agent-core`, use `createAgentCoreFsTools()`: see [Pi agent core](#pi-agent-core) below. For the experimental `pi-durable` harness, use [`@better-fs-tools/pi-durable`](https://www.npmjs.com/package/@better-fs-tools/pi-durable).
106
+
107
+ ## Options
77
108
 
78
- ## What the write tools do
109
+ Every Pi factory takes the options of the matching core factory, with the same defaults, plus `signature`, `promptSnippet`, and `promptGuidelines`:
79
110
 
80
- - `createPiFsTools(options?)` wraps `createFsTools()` from `@better-fs-tools/write`. It builds `read`, `edit`, and `write` with one `nodeDigest()`, one lock manager, one clock, and one cache of filesystems over `ctx.cwd`. `applyPatch: true`, or an options object, adds `apply_patch`. `state` defaults to `null`, so read-before-write is off; `state: memoryStore()` turns it on. `digest` (default `nodeDigest()`), `locks`, `clock`, and the root options (`denyRoots`, `symlinks`, `hardLinks`, `newFileMode`, `newDirectoryMode`) are set once for all the tools. Each tool's other options go under `read`, `edit`, `write`, and `applyPatch`. The result also has `applyPatch` (`null` when off), `state`, `digest`, `locks`, `clock`, and `invalidate(path, call?)`, which returns an `InvalidateOutcome`. Pass the call, so the path resolves under its `ctx.cwd`: in a bash `afterRun` hook it is `ctx.call`. Without it the outcome is `{ ok: false }` with reason `unsupported`. An unknown top-level key, or `state`, `digest`, `locks`, `clock`, or a root option inside a tool's options, throws `TypeError`.
81
- - `createPiEditTool()`, `createPiWriteTool()`, and `createPiApplyPatchTool()` build one tool each. They default to `state: null`, so read-before-write is off. Pass the same `state` as your read tool, or use `createPiFsTools()`.
82
- - `fs`, `cwd`, and `allowedRoots` throw `TypeError`, as for the read tool. Writes go through `nodeFileSystem` with `ctx.cwd` as the only allowed root, and `hardLinks: "in-place"` by default: a file with more than one hard link is written through the link, not atomically. `hardLinks: "refuse"` fails that write with `DENIED`. `newFileMode` and `newDirectoryMode` set the modes of new files and directories exactly. Without them, a new file is `0o666` and a new directory `0o777`, less the process umask.
83
- - The default signatures follow Pi's own tools: `edit` takes `path` and `edits: [{ oldText, newText }]` (`multiEditSignature({ name: "edit" })`), and `write` takes `path` and `content`. Their prompt snippets and guidelines are Pi's own, so the system prompt stays the same. `apply_patch` uses `freeformPatchSignature()`: models with grammar-tool support get the Codex patch grammar, and other models get the JSON schema.
111
+ | Factory | Options |
112
+ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
113
+ | `createPiReadTool(options)` | `createReadTool()`: `fs` is required |
114
+ | `createPiEditTool()`, `createPiWriteTool()`, `createPiApplyPatchTool()` | `createEditTool()`, `createWriteTool()`, `createApplyPatchTool()`: `fs` is required |
115
+ | `createPiBashTool(options)` | `createBashTool()`: `runner` and `env` are required |
116
+ | `createPiFsTools(options)` | `createFsTools()`: `fs` is required. Each tool's options go under `read`, `edit`, `write`, `applyPatch`, and `bash` |
117
+
118
+ - `promptSnippet` and `promptGuidelines` are Pi fields. The snippet defaults to the first sentence of the signature description, because Pi lists a tool under "Available tools" only when it has a snippet. The guidelines default to `[]`.
119
+ - Pi's `ExtensionContext` is the host type. Host functions get it as `ctx.call.host`, for example `ctx.call.host.ui.confirm(...)` in `askUser`, or `ctx.call.host.sessionManager.getSessionId()` in a `state` factory. A factory `fs`, `state`, or `runner` gets the call, so it can use `ctx.cwd`.
120
+ - The defaults are those of every integration: `digest` is `sha256Digest()`, `locks` is `memoryLocks()`, `state` is `null`, `read.converters` is `[imageConverter()]`, and the default signatures are `defaultReadSignature()`, `defaultEditSignature({ matchers })`, `defaultWriteSignature()`, `defaultPatchSignature()`, and `defaultBashSignature({ runner, limits })`. `docs/hosts.md` in `@better-fs-tools/write` has the full table.
121
+ - `createPiFsTools()` returns `read`, `edit`, `write`, `applyPatch` and `bash` (each `null` when off), `state`, `digest`, `locks`, `clock`, and `invalidate(path, call?)`. Pass the call to `invalidate` when `fs` or `state` is a factory: in a bash `afterRun` hook it is `ctx.call`.
122
+
123
+ ## The Node helpers
124
+
125
+ `@better-fs-tools/pi/node` has the Node backends of a Pi extension. The main entry imports no Node module.
126
+
127
+ | Helper | Gives |
128
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | `piCwdFileSystem(options?)` | A per-call `fs`: `nodeFileSystem()` with `cwd` and `allowedRoots` set to `ctx.cwd`. It keeps the filesystems of the last 8 roots. It takes every other `nodeFileSystem()` option, such as `denyRoots`, `symlinks`, `hardLinks`, `newFileMode`, and `newDirectoryMode` |
130
+ | `piCwdCommandRunner(options?)` | A per-call bash `runner`: `nodeCommandRunner()` at `ctx.cwd`, which must be absolute. It keeps the runners of the last 8 directories |
131
+
132
+ A call without a usable `ctx.cwd` gives an `extension-failed` error result.
133
+
134
+ ## Recommended configuration
135
+
136
+ These choices suit Pi. They are options, not defaults:
137
+
138
+ - `fs: piCwdFileSystem()`: each call is confined to its working directory.
139
+ - `locks: piMutationQueueLocks()`: Pi's own per-file queue, `withFileMutationQueue()`. A Better FS Tools `edit` and Pi's own `edit` or `write` on the same file then wait for each other. `piMutationQueueLocks({ timeoutMs })` waits 30 seconds by default, then fails the change with `LOCK_TIMEOUT`. In a Pi extension, Pi's loader resolves the package's import of `@earendil-works/pi-coding-agent` to Pi's own module, so the queue is Pi's. In an SDK host, install one copy of Pi, so that the package and the host share the queue.
140
+ - `digest: nodeDigest()`: the same values as `sha256Digest()`, faster on Node.
141
+ - For bash: `runner: piCwdCommandRunner()` and `env: shellEnv(() => process.env)`.
142
+
143
+ ## What the tools return
144
+
145
+ - Image parts become Pi `image` parts. Other media becomes a text part that says what was left out.
146
+ - `read` gives `details.truncation` as Pi's own read tool does for line and byte stops. Its `content` comes from the formatter in `"view"` mode. For other results, or a formatter that returns parts, `details` is `{}`. `toPiReadDetails()` builds the details for your own tool.
84
147
  - `edit` and `apply_patch` return `details` in the shape of Pi's `EditToolDetails` (`diff`, `patch`, `firstChangedLine`), so Pi's edit renderer draws the diff. `toPiMutationDetails()` builds them for your own tool. `write` returns no details, as Pi's own `write` does.
85
- - A tool error becomes Pi content text. The tool does not throw for a tool error.
86
- - Pi's `edit` guideline says the old text must match exactly. The tool also accepts close matches, and the result says when it used one.
148
+ - A signature with a `grammar` sets `constrainedSampling` for models with grammar-tool support. For the Codex patch grammar, pass `applyPatch: { signature: freeformPatchSignature() }`. To give a model Pi's own edit input, `edits: [{ oldText, newText }]`, pass `edit: { signature: multiEditSignature() }`. Both presets come from `@better-fs-tools/write/signature`.
149
+ - A tool error becomes Pi content text with `isError: true`. The tool does not throw for a tool error. Pi shows the text as an error, and `tool_result` handlers see `isError: true`. Bad model input rejects with `TypeError`.
150
+
151
+ ## Renderers
152
+
153
+ Pi draws a tool named `read`, `edit`, `write`, or `bash` with its built-in renderer of that name, unless the tool has its own. Those renderers read Pi's own argument names: `edits[].oldText` for the edit preview, and `timeout` in seconds for bash. `piToolRenderers(tools)` returns a `ToolRendererResolver` that gives each renderer the arguments in Pi's names, for any signature. Register it with `pi.registerToolRenderer()`. The extension entry does this for its own tools. Without it, the tools still work, but Pi draws no edit preview before the change and shows a bash timeout in milliseconds as seconds. A replace-all edit gets no preview, and Pi shows the diff of the result.
154
+
155
+ ## Results and hints
156
+
157
+ Each tool sets these Pi 1.x tool fields:
158
+
159
+ | Tool | `isError` | `annotations` |
160
+ | --------------------- | ---------------------------------------------- | ------------------------------------------------------------------------ |
161
+ | `read` | `true` for status `error` | `readOnlyHint: true` |
162
+ | `edit`, `apply_patch` | `true` for status `error` | `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: false` |
163
+ | `write` | `true` for status `error` | As `edit`, with `idempotentHint: true` |
164
+ | `bash` | `true` for a nonzero exit, a timeout, an error | None |
165
+
166
+ No tool declares an `outputSchema` or returns `structuredContent`, as in every integration. A codemode script that calls a tool gets its text. A result with `isError: true` makes the call reject with that text.
167
+
168
+ Permission extensions read `annotations` from `pi.getAllTools()`. The hints are not checked.
87
169
 
88
170
  ## Bash tool
89
171
 
90
- `createPiBashTool()` is a `bash` tool from [`@better-fs-tools/shell`](https://www.npmjs.com/package/@better-fs-tools/shell) with Pi's own shape: `{ command, timeout }` with the timeout in seconds, and Pi's prompt snippet. It runs in `ctx.cwd` on every call, also with a `runner` you pass: the runner gets `ctx.cwd` as the request's `cwd`, not its own. A relative `ctx.cwd` throws `TypeError`. The extension entry does not register it, so Pi's own `bash` stays unless you register this one:
172
+ `createPiBashTool()` is a `bash` tool from [`@better-fs-tools/shell`](https://www.npmjs.com/package/@better-fs-tools/shell) with the shared default signature, `defaultBashSignature()`: `{ command, timeout?, cwd? }` with the timeout in milliseconds. `runner` and `env` are required. With `piCwdCommandRunner()`, commands run in `ctx.cwd`, and a `cwd` from the model is relative to it. For a runner with its own directory, the `cwd` option sets another one, for example `cwd: (call) => call.host.cwd`. The extension entry does not register this tool, so Pi's own `bash` stays unless you register this one:
91
173
 
92
174
  ```ts
93
175
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
94
- import { createPiBashTool } from "@better-fs-tools/pi";
176
+ import { createPiBashTool, piToolRenderers } from "@better-fs-tools/pi";
177
+ import { piCwdCommandRunner } from "@better-fs-tools/pi/node";
178
+ import { shellEnv } from "@better-fs-tools/shell";
95
179
 
96
- // Replaces Pi's own bash: same name, { command, timeout } in seconds, run in ctx.cwd.
180
+ // Replaces Pi's own bash with the shared signature: { command, timeout?, cwd? },
181
+ // the timeout in milliseconds, run in ctx.cwd. Pi's bash renderer still draws it.
97
182
  export default function bashExtension(pi: ExtensionAPI): void {
98
- pi.registerTool(createPiBashTool());
183
+ const bash = createPiBashTool({
184
+ runner: piCwdCommandRunner(), // a Node runner at ctx.cwd on each call
185
+ env: shellEnv(() => process.env),
186
+ });
187
+ pi.registerTool(bash);
188
+ pi.registerToolRenderer(piToolRenderers([bash]));
99
189
  }
100
190
  ```
101
191
 
102
- The options take every `createBashTool()` dependency except `cwd`, plus `runner`, `signature`, `promptSnippet`, and `promptGuidelines`. `runner` and `env` are optional here. The default description names the timeouts and output limits from `limits`, and the id of a `runner` object when you pass one.
192
+ The default description names the timeouts and output limits from `limits`, and the id of a `runner` object. A runner factory, such as `piCwdCommandRunner()`, has no id to name.
103
193
 
104
- Pi's own `bash` also sets `PI_*` session variables. This tool does not. Add them with the `env` option when you need them. `env` defaults to `shellEnv(() => process.env)`.
194
+ Pi's own `bash` also sets `PI_*` session variables. This tool does not. Add them with the `env` option when you need them.
105
195
 
106
- With the file tools, a bash `afterRun` hook can make the next edit of a file need a read:
196
+ The bundle takes `bash` too. With a read store, a bash `afterRun` hook can make the next edit of a file need a read:
107
197
 
108
198
  ```ts
109
199
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
110
- import { createPiBashTool, createPiFsTools } from "@better-fs-tools/pi";
200
+ import { createPiFsTools, piToolRenderers } from "@better-fs-tools/pi";
201
+ import { piCwdCommandRunner, piCwdFileSystem } from "@better-fs-tools/pi/node";
111
202
  import { memoryStore } from "@better-fs-tools/read";
203
+ import { shellEnv } from "@better-fs-tools/shell";
112
204
 
113
- // The file tools with a read store, and Pi-shaped bash. After each command,
114
- // the next edit of package.json needs a read, since the command may have
115
- // changed it.
205
+ // The file tools with a read store, and bash in the same bundle. After each
206
+ // command, the next edit of package.json needs a read, since the command may
207
+ // have changed it.
116
208
  export default function fsAndBashExtension(pi: ExtensionAPI): void {
117
- const tools = createPiFsTools({ state: memoryStore() });
118
- const bash = createPiBashTool({
119
- afterRun: [
120
- {
121
- id: "invalidate-package-json",
122
- afterRun: async (_outcome, ctx) => {
123
- // ctx.call carries Pi's ctx, so the path resolves under its cwd.
124
- await tools.invalidate("package.json", ctx.call);
125
- return {};
209
+ const tools = createPiFsTools({
210
+ fs: piCwdFileSystem(),
211
+ state: memoryStore(),
212
+ bash: {
213
+ runner: piCwdCommandRunner(),
214
+ env: shellEnv(() => process.env),
215
+ afterRun: [
216
+ {
217
+ id: "invalidate-package-json",
218
+ afterRun: async (_outcome, ctx) => {
219
+ // ctx.call carries Pi's ctx, so the path resolves under its cwd.
220
+ await tools.invalidate("package.json", ctx.call);
221
+ return {};
222
+ },
126
223
  },
127
- },
128
- ],
224
+ ],
225
+ },
129
226
  });
130
227
  pi.registerTool(tools.read);
131
228
  pi.registerTool(tools.edit);
132
229
  pi.registerTool(tools.write);
133
- pi.registerTool(bash);
230
+ pi.registerTool(tools.bash);
231
+ pi.registerToolRenderer(piToolRenderers([tools.read, tools.edit, tools.write, tools.bash]));
232
+ }
233
+ ```
234
+
235
+ ## Pi agent core
236
+
237
+ `@better-fs-tools/pi/agent-core` gives the tools as `AgentTool` objects for `@earendil-works/pi-agent-core`: for `agent.state.tools`, or the `tools` of an `agentLoop()` context. It has `createAgentCoreFsTools()` and the single-tool factories `createAgentCoreReadTool()`, `createAgentCoreEditTool()`, `createAgentCoreWriteTool()`, `createAgentCoreApplyPatchTool()`, and `createAgentCoreBashTool()`. An `AgentTool` gets no context object, so the host type is `undefined`:
238
+
239
+ ```ts
240
+ import { Agent } from "@earendil-works/pi-agent-core";
241
+ import { createAgentCoreFsTools } from "@better-fs-tools/pi/agent-core";
242
+ import { nodeFileSystem } from "@better-fs-tools/node";
243
+ import { memoryStore } from "@better-fs-tools/read";
244
+
245
+ const fsTools = createAgentCoreFsTools({
246
+ fs: nodeFileSystem({ cwd: "/work/repo", allowedRoots: ["/work/repo"] }),
247
+ state: memoryStore(),
248
+ });
249
+
250
+ export function addFsTools(agent: Agent): void {
251
+ agent.state.tools = [...agent.state.tools, ...fsTools.tools];
134
252
  }
135
253
  ```
136
254
 
255
+ - The options are those of the core factories and `createFsTools()`, with the same defaults, plus a `signature` for each tool. No tool declares an output schema.
256
+ - `fs` can be any writable filesystem. The module imports no Node module and no Pi package at run time, so it runs in a browser or a Worker, for example with `memoryFileSystem()` or a Cloudflare filesystem.
257
+ - pi-agent-core has no tools of its own. `bash: { runner, env }` adds a `bash` tool with `defaultBashSignature()`: `{ command, timeout?, cwd? }` with the timeout in milliseconds.
258
+ - A tool error is a result with `isError: true`. Bad model input rejects with `TypeError`, which the agent reports as an error result.
259
+ - `read` sets `replay: "safe"`. `edit` and `apply_patch` give `toPiMutationDetails()` as `details`.
260
+ - The tools use `memoryLocks()` by default, as every integration does. pi-agent-core has no file queue of its own. Two calls in one batch that change one file wait for each other.
261
+
137
262
  ## Links
138
263
 
139
- - `docs/hosts.md` in [`@better-fs-tools/write`](https://www.npmjs.com/package/@better-fs-tools/write): the defaults of every host and bundle, and what each backend can do
140
- - [`@better-fs-tools/read`](https://www.npmjs.com/package/@better-fs-tools/read): every read option, the signature builders, and the format presets. Its README has a full Pi host example.
264
+ - [Choose an integration](https://better-fs-tools.gandrew.com/integrations) and [Filesystems](https://better-fs-tools.gandrew.com/filesystems) on the docs site: the defaults of every host and bundle, and what each backend can do. `docs/hosts.md` in [`@better-fs-tools/write`](https://www.npmjs.com/package/@better-fs-tools/write) has the same tables
265
+ - [`@better-fs-tools/read`](https://www.npmjs.com/package/@better-fs-tools/read): every read option, the default signature, `readToolDefinition()`, and the format presets. Its README has a full Pi host example.
141
266
  - [`@better-fs-tools/write`](https://www.npmjs.com/package/@better-fs-tools/write): every write option, the guards, and the authorizers
142
- - [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node): the filesystem that each call uses
267
+ - [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node): the filesystem and runner that the `./node` helpers build on
@@ -0,0 +1,141 @@
1
+ import type { TSchema } from "typebox";
2
+ import type { Clock, Digest, ToolCallContext } from "@better-fs-tools/read";
3
+ import type { ReadSignature, ReadToolDefinitionOptions } from "@better-fs-tools/read/signature";
4
+ import type { BashSignature, BashToolDefinitionOptions } from "@better-fs-tools/shell/signature";
5
+ import type { FsToolsApplyPatchOptions, FsToolsBashOptions, FsToolsEditOptions, FsToolsOptions, FsToolsReadOptions, FsToolsState, FsToolsWriteOptions, InvalidateOutcome, LockManager } from "@better-fs-tools/write";
6
+ import type { ApplyPatchToolDefinitionOptions, EditSignature, EditToolDefinitionOptions, PatchSignature, WriteSignature, WriteToolDefinitionOptions } from "@better-fs-tools/write/signature";
7
+ import type { PiReadDetails } from "./details.ts";
8
+ import type { PiMutationDetails } from "./mutation-details.ts";
9
+ import type { PiContentPart } from "./parts.ts";
10
+ /** An AgentTool host has no context object, so each call's host is undefined. */
11
+ type Host = undefined;
12
+ /** The result of an agent core tool. Assignable to AgentToolResult<TDetails>. */
13
+ export interface AgentCoreToolResult<TDetails> {
14
+ content: PiContentPart[];
15
+ details: TDetails;
16
+ /** true for a tool error. bash also sets it for a nonzero exit and a timeout. */
17
+ isError: boolean;
18
+ }
19
+ /** Assignable to AgentTool<TSchema, TDetails> from @earendil-works/pi-agent-core 1.x. */
20
+ export interface AgentCoreTool<TDetails> {
21
+ readonly name: string;
22
+ readonly label: string;
23
+ readonly description: string;
24
+ /** The JSON Schema of the tool input. */
25
+ readonly parameters: TSchema;
26
+ /** Set when the signature has a grammar. */
27
+ readonly constrainedSampling?: {
28
+ type: "grammar";
29
+ variants: {
30
+ openai_lark: string;
31
+ };
32
+ };
33
+ /** "safe" on read: a rerun after a crash changes nothing. */
34
+ readonly replay?: "never" | "safe";
35
+ /** Model input is checked with the signature: bad input throws TypeError. */
36
+ readonly execute: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown) => Promise<AgentCoreToolResult<TDetails>>;
37
+ }
38
+ export type AgentCoreReadTool = AgentCoreTool<PiReadDetails>;
39
+ export type AgentCoreMutationTool = AgentCoreTool<PiMutationDetails | undefined>;
40
+ export type AgentCoreBashTool = AgentCoreTool<{
41
+ fullOutputPath?: string;
42
+ } | undefined>;
43
+ /** Options for createAgentCoreReadTool: the options of readToolDefinition(). `fs` is required. */
44
+ export type AgentCoreReadToolOptions = ReadToolDefinitionOptions<Host>;
45
+ /** Options for createAgentCoreEditTool: the options of editToolDefinition(). */
46
+ export type AgentCoreEditToolOptions = EditToolDefinitionOptions<Host>;
47
+ /** Options for createAgentCoreWriteTool: the options of writeToolDefinition(). */
48
+ export type AgentCoreWriteToolOptions = WriteToolDefinitionOptions<Host>;
49
+ /** Options for createAgentCoreApplyPatchTool: the options of applyPatchToolDefinition(). */
50
+ export type AgentCoreApplyPatchToolOptions = ApplyPatchToolDefinitionOptions<Host>;
51
+ /** Options for createAgentCoreBashTool: the options of bashToolDefinition(). `runner` and `env` are required. */
52
+ export type AgentCoreBashToolOptions = BashToolDefinitionOptions<Host>;
53
+ /** Read options of the bundle. Shared keys are set once at the top level. */
54
+ export type AgentCoreFsToolsReadOptions = FsToolsReadOptions<Host> & {
55
+ /** Default defaultReadSignature(). */
56
+ readonly signature?: ReadSignature;
57
+ };
58
+ export type AgentCoreFsToolsEditOptions = FsToolsEditOptions<Host> & {
59
+ /** Default defaultEditSignature({ matchers }). */
60
+ readonly signature?: EditSignature;
61
+ };
62
+ export type AgentCoreFsToolsWriteOptions = FsToolsWriteOptions<Host> & {
63
+ /** Default defaultWriteSignature(). */
64
+ readonly signature?: WriteSignature;
65
+ };
66
+ export type AgentCoreFsToolsApplyPatchOptions = FsToolsApplyPatchOptions<Host> & {
67
+ /** Default defaultPatchSignature(). */
68
+ readonly signature?: PatchSignature;
69
+ };
70
+ /** bash options of the bundle. `runner` and `env` are required. */
71
+ export type AgentCoreFsToolsBashOptions = FsToolsBashOptions<Host> & {
72
+ /** Default defaultBashSignature() for the runner's id and the limits. */
73
+ readonly signature?: BashSignature;
74
+ };
75
+ /** The options of createFsTools(), with a `signature` in each tool's options. */
76
+ export interface AgentCoreFsToolsOptions extends Omit<FsToolsOptions<Host>, "read" | "edit" | "write" | "applyPatch" | "bash"> {
77
+ readonly read?: AgentCoreFsToolsReadOptions;
78
+ readonly edit?: AgentCoreFsToolsEditOptions;
79
+ readonly write?: AgentCoreFsToolsWriteOptions;
80
+ /** Off by default. `true` or an options object adds an apply_patch tool. */
81
+ readonly applyPatch?: boolean | AgentCoreFsToolsApplyPatchOptions;
82
+ /** Off by default. pi-agent-core has no tools of its own: give a runner and an env for bash. */
83
+ readonly bash?: false | AgentCoreFsToolsBashOptions;
84
+ }
85
+ export interface AgentCoreFsTools {
86
+ readonly read: AgentCoreReadTool;
87
+ readonly edit: AgentCoreMutationTool;
88
+ readonly write: AgentCoreMutationTool;
89
+ /** null unless options.applyPatch is true or an object. */
90
+ readonly applyPatch: AgentCoreMutationTool | null;
91
+ /** null unless options.bash is set. */
92
+ readonly bash: AgentCoreBashTool | null;
93
+ /** Every tool of the bundle, for agent.state.tools or an agentLoop() context. */
94
+ readonly tools: readonly (AgentCoreReadTool | AgentCoreMutationTool | AgentCoreBashTool)[];
95
+ readonly state: FsToolsState<Host>;
96
+ readonly digest: Digest;
97
+ readonly locks: LockManager;
98
+ readonly clock: Clock;
99
+ /** As createFsTools().invalidate. When fs or state is a factory, pass the call context. */
100
+ invalidate(path: string, call?: ToolCallContext<Host>): Promise<InvalidateOutcome>;
101
+ }
102
+ /** The createAgentCoreFsTools result when `applyPatch` is true or an object: it is never null. */
103
+ export interface AgentCoreFsToolsWithApplyPatch extends AgentCoreFsTools {
104
+ readonly applyPatch: AgentCoreMutationTool;
105
+ }
106
+ /** The createAgentCoreFsTools result when `bash` is set: it is never null. */
107
+ export interface AgentCoreFsToolsWithBash extends AgentCoreFsTools {
108
+ readonly bash: AgentCoreBashTool;
109
+ }
110
+ /** Creates a read AgentTool with the options and defaults of createReadTool(). read is replay-safe. */
111
+ export declare function createAgentCoreReadTool(options: AgentCoreReadToolOptions): AgentCoreReadTool;
112
+ /** Creates an edit AgentTool with the options and defaults of createEditTool(). */
113
+ export declare function createAgentCoreEditTool(options: AgentCoreEditToolOptions): AgentCoreMutationTool;
114
+ /** Creates a write AgentTool with the options and defaults of createWriteTool(). */
115
+ export declare function createAgentCoreWriteTool(options: AgentCoreWriteToolOptions): AgentCoreMutationTool;
116
+ /** Creates an apply_patch AgentTool with the options and defaults of createApplyPatchTool(). */
117
+ export declare function createAgentCoreApplyPatchTool(options: AgentCoreApplyPatchToolOptions): AgentCoreMutationTool;
118
+ /** Creates a bash AgentTool with the options and defaults of createBashTool(). */
119
+ export declare function createAgentCoreBashTool(options: AgentCoreBashToolOptions): AgentCoreBashTool;
120
+ /**
121
+ * Creates read, edit, and write AgentTools over one writable filesystem, with
122
+ * one digest, lock manager, and clock, and the read state when `state` is
123
+ * given. With `applyPatch`, also apply_patch. With `bash`, also bash. Every
124
+ * option and default is the one of createFsTools().
125
+ *
126
+ * A tool error is a result with `isError: true`, not a throw. Bad model input
127
+ * throws TypeError. Throws TypeError where createFsTools() does, with
128
+ * createAgentCoreFsTools in the message.
129
+ */
130
+ export declare function createAgentCoreFsTools(options: AgentCoreFsToolsOptions & {
131
+ readonly applyPatch: true | AgentCoreFsToolsApplyPatchOptions;
132
+ readonly bash: AgentCoreFsToolsBashOptions;
133
+ }): AgentCoreFsToolsWithApplyPatch & AgentCoreFsToolsWithBash;
134
+ export declare function createAgentCoreFsTools(options: AgentCoreFsToolsOptions & {
135
+ readonly bash: AgentCoreFsToolsBashOptions;
136
+ }): AgentCoreFsToolsWithBash;
137
+ export declare function createAgentCoreFsTools(options: AgentCoreFsToolsOptions & {
138
+ readonly applyPatch: true | AgentCoreFsToolsApplyPatchOptions;
139
+ }): AgentCoreFsToolsWithApplyPatch;
140
+ export declare function createAgentCoreFsTools(options: AgentCoreFsToolsOptions): AgentCoreFsTools;
141
+ export {};
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The Better FS Tools as AgentTool objects for @earendil-works/pi-agent-core:
3
+ * an Agent's state.tools, or the tools of an agentLoop() context. This module
4
+ * imports neither Node nor Pi at run time, so it runs in a browser or a
5
+ * Worker with any writable filesystem.
6
+ */
7
+ import { Type } from "typebox";
8
+ import { readToolDefinition } from "@better-fs-tools/read/signature";
9
+ import { bashToolDefinition } from "@better-fs-tools/shell/signature";
10
+ import { applyPatchToolDefinition, editToolDefinition, fsToolDefinitions, writeToolDefinition, } from "@better-fs-tools/write/signature";
11
+ import { toPiReadDetails, viewOf } from "./details.js";
12
+ import { toPiMutationDetails } from "./mutation-details.js";
13
+ import { toPiPart } from "./parts.js";
14
+ import { checkPiOptions } from "./prompt.js";
15
+ /** Creates a read AgentTool with the options and defaults of createReadTool(). read is replay-safe. */
16
+ export function createAgentCoreReadTool(options) {
17
+ checkPiOptions(options, "agent core read tool");
18
+ return adaptReadTool(readToolDefinition(options));
19
+ }
20
+ /** Creates an edit AgentTool with the options and defaults of createEditTool(). */
21
+ export function createAgentCoreEditTool(options) {
22
+ checkPiOptions(options, "agent core edit tool");
23
+ return adaptMutationTool("edit", editToolDefinition(options));
24
+ }
25
+ /** Creates a write AgentTool with the options and defaults of createWriteTool(). */
26
+ export function createAgentCoreWriteTool(options) {
27
+ checkPiOptions(options, "agent core write tool");
28
+ return adaptMutationTool("write", writeToolDefinition(options));
29
+ }
30
+ /** Creates an apply_patch AgentTool with the options and defaults of createApplyPatchTool(). */
31
+ export function createAgentCoreApplyPatchTool(options) {
32
+ checkPiOptions(options, "agent core apply_patch tool");
33
+ return adaptMutationTool("apply_patch", applyPatchToolDefinition(options));
34
+ }
35
+ /** Creates a bash AgentTool with the options and defaults of createBashTool(). */
36
+ export function createAgentCoreBashTool(options) {
37
+ checkPiOptions(options, "agent core bash tool");
38
+ return adaptBashTool(bashToolDefinition(options));
39
+ }
40
+ export function createAgentCoreFsTools(options) {
41
+ checkPiOptions(options, "createAgentCoreFsTools");
42
+ const defs = fsToolDefinitions(options, "createAgentCoreFsTools");
43
+ const adapted = {
44
+ read: adaptReadTool(defs.read),
45
+ edit: adaptMutationTool("edit", defs.edit),
46
+ write: adaptMutationTool("write", defs.write),
47
+ applyPatch: defs.applyPatch === null ? null : adaptMutationTool("apply_patch", defs.applyPatch),
48
+ bash: defs.bash === null ? null : adaptBashTool(defs.bash),
49
+ };
50
+ return Object.freeze({
51
+ ...adapted,
52
+ tools: Object.freeze(Object.values(adapted).filter((tool) => tool !== null)),
53
+ state: defs.state,
54
+ digest: defs.digest,
55
+ locks: defs.locks,
56
+ clock: defs.clock,
57
+ invalidate: defs.invalidate,
58
+ });
59
+ }
60
+ function declaration(definition) {
61
+ return {
62
+ name: definition.name,
63
+ label: definition.name,
64
+ description: definition.description,
65
+ parameters: Type.Unsafe(definition.inputSchema),
66
+ ...(definition.grammar === undefined
67
+ ? {}
68
+ : {
69
+ constrainedSampling: {
70
+ type: "grammar",
71
+ variants: { openai_lark: definition.grammar.lark },
72
+ },
73
+ }),
74
+ };
75
+ }
76
+ function callOf(toolCallId, signal) {
77
+ return { ...(signal === undefined ? {} : { signal }), callId: toolCallId, host: undefined };
78
+ }
79
+ function adaptReadTool(definition) {
80
+ const { limits, formatter, digest } = definition;
81
+ return Object.freeze({
82
+ ...declaration(definition),
83
+ replay: "safe",
84
+ async execute(toolCallId, params, signal) {
85
+ const call = callOf(toolCallId, signal);
86
+ const result = await definition.execute(params, call);
87
+ const content = result.content.map((part) => toPiPart(part, "read"));
88
+ const view = viewOf(formatter, result, { digest, limits, mode: "view", call });
89
+ const details = toPiReadDetails(result, view, limits.maxViewBytes);
90
+ return { content, details, isError: result.status === "error" };
91
+ },
92
+ });
93
+ }
94
+ function adaptMutationTool(kind, definition) {
95
+ return Object.freeze({
96
+ ...declaration(definition),
97
+ async execute(toolCallId, params, signal) {
98
+ const result = await definition.execute(params, callOf(toolCallId, signal));
99
+ return {
100
+ content: result.content.map((part) => toPiPart(part, kind)),
101
+ // The coding agent's own write has no details.
102
+ details: kind === "write" ? undefined : toPiMutationDetails(result),
103
+ isError: result.status === "error",
104
+ };
105
+ },
106
+ });
107
+ }
108
+ function adaptBashTool(definition) {
109
+ return Object.freeze({
110
+ ...declaration(definition),
111
+ async execute(toolCallId, params, signal) {
112
+ const result = await definition.execute(params, callOf(toolCallId, signal));
113
+ const spill = result.output?.spill ?? null;
114
+ return {
115
+ content: result.content.map((part) => toPiPart(part, "bash")),
116
+ details: spill === null ? undefined : { fullOutputPath: spill },
117
+ isError: result.status !== "ok",
118
+ };
119
+ },
120
+ });
121
+ }