@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 +172 -47
- package/dist/agent-core.d.ts +141 -0
- package/dist/agent-core.js +121 -0
- package/dist/bash-tool.d.ts +16 -24
- package/dist/bash-tool.js +23 -58
- package/dist/details.d.ts +7 -1
- package/dist/details.js +18 -0
- package/dist/extension.d.ts +12 -0
- package/dist/extension.js +10 -8
- package/dist/fs-tools.d.ts +70 -36
- package/dist/fs-tools.js +33 -88
- package/dist/index.d.ts +9 -4
- package/dist/index.js +2 -0
- package/dist/locks.d.ts +16 -0
- package/dist/locks.js +68 -0
- package/dist/mutation-tools.d.ts +29 -42
- package/dist/mutation-tools.js +42 -93
- package/dist/node.d.ts +23 -0
- package/dist/node.js +76 -0
- package/dist/parts.js +11 -6
- package/dist/prompt.d.ts +30 -0
- package/dist/prompt.js +35 -0
- package/dist/renderers.d.ts +27 -0
- package/dist/renderers.js +72 -0
- package/dist/results.d.ts +13 -0
- package/dist/results.js +18 -0
- package/dist/tool.d.ts +17 -43
- package/dist/tool.js +26 -75
- package/package.json +25 -8
- package/dist/roots.d.ts +0 -20
- package/dist/roots.js +0 -58
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)
|
|
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.
|
|
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()
|
|
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
|
-
##
|
|
77
|
+
## Use the Pi SDK
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
109
|
+
Every Pi factory takes the options of the matching core factory, with the same defaults, plus `signature`, `promptSnippet`, and `promptGuidelines`:
|
|
79
110
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
86
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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 {
|
|
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
|
|
114
|
-
// the next edit of package.json needs a read, since the command may
|
|
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({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
-
|
|
140
|
-
- [`@better-fs-tools/read`](https://www.npmjs.com/package/@better-fs-tools/read): every read option, the signature
|
|
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
|
|
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
|
+
}
|