Better FS Tools
Integrations

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

HostPackageBundlePage
AI SDK 7 generateText or streamText@better-fs-tools/ai-sdkcreateAiSdkFsTools()AI SDK
Pi coding agent: the pi command or SDK@better-fs-tools/picreatePiFsTools()Pi
pi-agent-core or pi-ai@better-fs-tools/pi/agent-corecreateAgentCoreFsTools()Pi agent core
pi-durable, experimental@better-fs-tools/pi-durablecreateDurableFsTools()Pi durable
A framework of your own@better-fs-tools/writefsToolDefinitions()Tool definitions
Your own code, with no model framework@better-fs-tools/writecreateFsTools()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

LayerFactoriesTakes
CorecreateReadTool(), createEditTool(), createWriteTool(), createApplyPatchTool(), createBashTool(), createFsTools()XxxToolOptions and FsToolsOptions. fs is required. bash needs runner and env
DefinitionsreadToolDefinition(), editToolDefinition(), writeToolDefinition(), applyPatchToolDefinition(), bashToolDefinition(), fsToolDefinitions() in the ./signature subpathsthe core options, plus signature
Integrationthe AI SDK, Pi, pi-agent-core, and pi-durable factoriesthe 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:

IntegrationSingle toolsBundle
AI SDK 7createAiSdkReadTool() and the four otherscreateAiSdkFsTools()
Pi coding agentcreatePiReadTool() and the four otherscreatePiFsTools()
Pi agent corecreateAgentCoreReadTool() and the four others, from @better-fs-tools/pi/agent-corecreateAgentCoreFsTools()
Pi durable harnessnone: tools register inside an extensioncreateDurableFsTools(), 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:

OptionDefault
fsrequired: a backend, or a factory of the call
statenull: no read before write. A store, such as memoryStore({ clock }), or a factory of the call turns it on
digestsha256Digest(), plain JavaScript. null turns observations off, and then state must be null
locksmemoryLocks()
clock() => new Date()
read.converters[imageConverter()]: an image becomes a media part. [] turns images off
applyPatch in a bundleoff. true or an options object adds apply_patch
bash in a bundleoff. An options object with runner and env adds it. true throws
bash runner, envrequired
bash cwdthe runner's cwd
New file and directory modesthe backend's
symlinks, identity, bufferingthe 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 throws TypeError. Set it once at the top level. An unknown option key also throws.
  • state is a store, a factory of the call, or null. A factory gives each call its own store.
  • The bash tool gets the bundle's digest and clock, so ctx.digest and ctx.clock in 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 an afterRun hook. 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:

ToolCanonical 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 as constrainedSampling. 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.

IntegrationOptionsDefaultWhy
AI SDKcontextSchemanoneThe AI SDK validates the toolsContext entry of a tool with it
PipromptSnippet, promptGuidelinesthe 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-durablename, outputLimits"better-fs-tools", and 128 KiB and 4,000 linesDurable 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.

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:

IntegrationRecommended options
AI SDKOn Node or Bun: fs: nodeFileSystem(), digest: nodeDigest(), and bash: { runner: nodeCommandRunner(), env }
Pifs: piCwdFileSystem(), locks: piMutationQueueLocks(), digest: nodeDigest(), and for bash runner: piCwdCommandRunner()
Pi agent coreAny writable filesystem as fs
Pi durablefs: 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.

On this page