Choose an integration
Choose the integration for your host, and see the defaults and options that every integration shares.
Every integration has the same options and the same defaults. An integration only changes the format of the tools, so that your host can use them. This page helps you choose one, and lists what they all share.
Choose an integration
| Host | Package | Bundle | Page |
|---|---|---|---|
AI SDK 7 generateText or streamText | @better-fs-tools/ai-sdk | createAiSdkFsTools() | AI SDK |
Pi coding agent: the pi command or SDK | @better-fs-tools/pi | createPiFsTools() | Pi |
pi-agent-core or pi-ai | @better-fs-tools/pi/agent-core | createAgentCoreFsTools() | Pi agent core |
pi-durable, experimental | @better-fs-tools/pi-durable | createDurableFsTools() | Pi durable |
| A framework of your own | @better-fs-tools/write | fsToolDefinitions() | Tool definitions |
| Your own code, with no model framework | @better-fs-tools/write | createFsTools() | Get started |
Pi has more than one library. createPiFsTools() only fits the coding agent, because its tools get Pi's ExtensionContext on each call. pi-agent-core and pi-durable have their own bundles.
The Node, just-bash, Cloudflare Shell, and Cloudflare Computer packages are backends, not integrations. Pass their filesystem as fs, and a runner as the bash runner, to any factory. See Filesystems.
One interface in three layers
| Layer | Factories | Takes |
|---|---|---|
| Core | createReadTool(), createEditTool(), createWriteTool(), createApplyPatchTool(), createBashTool(), createFsTools() | XxxToolOptions and FsToolsOptions. fs is required. bash needs runner and env |
| Definitions | readToolDefinition(), editToolDefinition(), writeToolDefinition(), applyPatchToolDefinition(), bashToolDefinition(), fsToolDefinitions() in the ./signature subpaths | the core options, plus signature |
| Integration | the AI SDK, Pi, pi-agent-core, and pi-durable factories | the core options, plus signature, plus host display or registration options |
The core tools take the canonical input and return a result. A definition binds a core tool to its signature: the name, description, and JSON Schema to declare, a validate for model input, and an execute that maps model input and runs the tool. Every integration is an adapter over the definitions. Tool definitions shows how to use them in a framework of your own.
Each integration has one factory for each tool and one bundle:
| Integration | Single tools | Bundle |
|---|---|---|
| AI SDK 7 | createAiSdkReadTool() and the four others | createAiSdkFsTools() |
| Pi coding agent | createPiReadTool() and the four others | createPiFsTools() |
| Pi agent core | createAgentCoreReadTool() and the four others, from @better-fs-tools/pi/agent-core | createAgentCoreFsTools() |
| Pi durable harness | none: tools register inside an extension | createDurableFsTools(), which returns the extension |
A bundle creates read, edit, and write over one filesystem. When you ask for them, it also creates apply_patch and bash. The tools share one digest, one lock manager, and one clock. When you set state, they also share one read store, so edit and write know what the model has read.
The core bundle runs anywhere. This example uses an in-memory filesystem:
import { memoryFileSystem } from "@better-fs-tools/fs";
import { memoryStore, textOf } from "@better-fs-tools/read";
import { createFsTools } from "@better-fs-tools/write";
const fs = memoryFileSystem({ files: { "/src/index.ts": "const a = 1;\n" } });
const tools = createFsTools({ fs, state: memoryStore(), applyPatch: true });
await tools.read({ path: "/src/index.ts" });
const result = await tools.edit({
path: "/src/index.ts",
edits: [{ oldText: "a = 1", newText: "a = 2" }],
});
console.log(textOf(result));
// Edited /src/index.ts: 1 replacement at line 1.Defaults
Every core tool, bundle, and integration has these defaults:
| Option | Default |
|---|---|
fs | required: a backend, or a factory of the call |
state | null: no read before write. A store, such as memoryStore({ clock }), or a factory of the call turns it on |
digest | sha256Digest(), plain JavaScript. null turns observations off, and then state must be null |
locks | memoryLocks() |
clock | () => new Date() |
read.converters | [imageConverter()]: an image becomes a media part. [] turns images off |
applyPatch in a bundle | off. true or an options object adds apply_patch |
bash in a bundle | off. An options object with runner and env adds it. true throws |
bash runner, env | required |
bash cwd | the runner's cwd |
| New file and directory modes | the backend's |
symlinks, identity, buffering | the backend's |
invalidate of a bundle | (path, call?). Pass the call when fs or state is a factory |
sha256Digest() and nodeDigest() give the same digest, with the id sha256, so records made with one are valid for the other. nodeDigest() uses node:crypto and is faster. sha256Digest() runs in a Worker.
Rules for every bundle
- A shared key (
fs,state,digest,locks,clock) inside one tool's options throwsTypeError. Set it once at the top level. An unknown option key also throws. stateis a store, a factory of the call, ornull. A factory gives each call its own store.- The bash tool gets the bundle's digest and clock, so
ctx.digestandctx.clockin a bash hook match the file tools. - The allowed roots do not limit what a bash command touches.
- The bash tool does not update the read records. To make the model read a file again after a command, call
invalidate(path)from anafterRunhook. See Bash policy.
Single-tool factories
A single-tool factory shares nothing with another one. Each write tool makes its own lock manager, and there is no read store unless you pass one. To use single tools with read before write, give each tool the same state, and the same locks to the write tools. Use a bundle when a host installs more than one tool.
Tool signatures
A signature is the shape of a tool that the model sees. The core tools take the canonical input. Every definition and integration has the same default signatures:
| Tool | Canonical input (core) | Default signature |
|---|---|---|
read | { path, offset?, limit? } | defaultReadSignature(): read({ path, offset?, limit? }) |
edit | { path, edits: [{ oldText, newText, replaceAll? }] } | defaultEditSignature({ matchers }): edit({ path, old_string, new_string, replace_all? }) |
write | { path, content } | defaultWriteSignature(): write({ path, content }) |
apply_patch | { patch } | defaultPatchSignature(): apply_patch({ patch }) as JSON |
bash | { command, timeoutMs?, cwd? } | defaultBashSignature({ runner, limits }): bash({ command, timeout?, cwd? }), milliseconds |
To give a model other names, pass a default signature with options as the tool's signature. Every default signature takes name, description, describe, and names. names renames a parameter, keyed by the signature's own parameter names. Three presets give another shape:
multiEditSignature():edit({ path, edits: [{ oldText, newText }] }), for many replacements in one call.lineRangeSignature():read({ path, start?, end? }), with inclusive lines.freeformPatchSignature():apply_patch({ patch }), plus the Codex patch grammar. The Pi, pi-agent-core, and pi-durable integrations send that grammar asconstrainedSampling. The AI SDK sends only the JSON schema.
Tool definitions has the options and the default descriptions of each signature.
Host options
An integration adds options only for its host's display or registration. It changes no default.
| Integration | Options | Default | Why |
|---|---|---|---|
| AI SDK | contextSchema | none | The AI SDK validates the toolsContext entry of a tool with it |
| Pi | promptSnippet, promptGuidelines | the first sentence of the signature description, and [] | Pi lists a tool under "Available tools" only when it has a snippet. The text comes from the shared signature |
| pi-durable | name, outputLimits | "better-fs-tools", and 128 KiB and 4,000 lines | Durable extensions have names. The Harness cuts tool output again, and these limits are above the read view, so the output is not cut twice |
piToolRenderers() from @better-fs-tools/pi is a renderer resolver, not an option. Pi draws a tool named read, edit, write, or bash with its built-in renderer, which reads Pi's argument names. The resolver maps the shared arguments to them. See Renderers.
Recommended configurations
Each host has choices that suit it. They are options, not defaults, so each one is visible in your code. Each integration page starts with its recommended configuration:
| Integration | Recommended options |
|---|---|
| AI SDK | On Node or Bun: fs: nodeFileSystem(), digest: nodeDigest(), and bash: { runner: nodeCommandRunner(), env } |
| Pi | fs: piCwdFileSystem(), locks: piMutationQueueLocks(), digest: nodeDigest(), and for bash runner: piCwdCommandRunner() |
| Pi agent core | Any writable filesystem as fs |
| Pi durable | fs: durableEnvFileSystem(), state: documentReadStore(), and bash: { runner: durableEnvRunner(), env } |
Filesystems
The defaults above say "the backend's" where the backend decides. The Filesystems page lists what each filesystem does: buffering, maxBufferedBytes, symlinks, identity, write support, and new file modes. It also explains why a buffered backend fetches the whole file before authorize runs.