Better FS Tools
Integrations

Pi agent core

Give the read, edit, and write tools to an Agent or agentLoop() from @earendil-works/pi-agent-core with createAgentCoreFsTools().

@earendil-works/pi-agent-core is the agent loop under the Pi coding agent. It has the Agent class and the lower agentLoop() function. Its tools are AgentTool objects. Use this page when your host runs the agent loop itself, without the coding agent's sessions, settings, and extensions.

createAgentCoreFsTools() from @better-fs-tools/pi/agent-core gives the Better FS Tools as AgentTool objects. The examples compile against pi-agent-core 1.2.0.

Install

npm install @better-fs-tools/pi @better-fs-tools/read @better-fs-tools/write @earendil-works/pi-agent-core @earendil-works/pi-coding-agent typebox

@better-fs-tools/pi has @earendil-works/pi-coding-agent (^1.0.0) and typebox as required peers, so npm installs the coding agent too. The agent-core module does not import it, or any Node module, at run time. Add a filesystem package for fs, for example @better-fs-tools/node.

Give createAgentCoreFsTools() any writable filesystem as fs. Every other option can keep its default:

import { createAgentCoreFsTools } from "@better-fs-tools/pi/agent-core";
import { memoryFileSystem } from "@better-fs-tools/fs";

createAgentCoreFsTools({ fs: memoryFileSystem() }); // any writable filesystem; no Node import

pi-agent-core has no tools, no file queue, and no working directory of its own, so the defaults of every integration fit it: sha256Digest(), memoryLocks(), no read store, and read returns images. Choose an integration has the full table.

Give the tools to the agent

tools has every tool that is on. Add it to agent.state.tools, or to the tools of an agentLoop() context:

import { Agent } from "@earendil-works/pi-agent-core";
import type { AgentContext } from "@earendil-works/pi-agent-core";
import { nodeFileSystem } from "@better-fs-tools/node";
import { createAgentCoreFsTools } from "@better-fs-tools/pi/agent-core";
import { memoryStore } from "@better-fs-tools/read";

// read, edit, and write over one filesystem, with one read store and one lock
// manager.
const fsTools = createAgentCoreFsTools({
  fs: nodeFileSystem({ cwd: "/work/repo", allowedRoots: ["/work/repo"] }),
  state: memoryStore(),
});

export function addFsTools(agent: Agent): void {
  agent.state.tools = [...agent.state.tools, ...fsTools.tools];
}

// For agentLoop(), put the tools in the context.
export const context: AgentContext = {
  messages: [],
  tools: [...fsTools.tools],
};

The result has one property for each tool: read, edit, write, applyPatch, and bash. applyPatch and bash are null when they are off. The result also has the shared state, digest, locks, and clock, and invalidate(path, call?), as createFsTools() does.

Why the Pi bundle does not fit

An AgentTool gets execute(toolCallId, params, signal, onUpdate). It gets no context object, so the host of each call is undefined. The tools from createPiFsTools() take Pi's ExtensionContext as the host, and piCwdFileSystem() needs its ctx.cwd. createAgentCoreFsTools() takes the filesystem when you make it, as createFsTools() does.

Options

The options are those of createFsTools(), with the same defaults, plus a signature in each tool's options. Set the shared options once, at the top level:

OptionDefaultMeaning
fsrequiredA writable filesystem, or a function that returns one for each call
statenullThe read store. null turns read before write off. memoryStore() turns it on
digestsha256Digest()The digest for read records. It is plain JavaScript, so it runs in a Worker
locksmemoryLocks()The lock manager of the write tools
clock() => new Date()The clock of the tools and the store
read, edit, write{}The options of one tool, and its signature. See the read and write guides
applyPatchofftrue, or an options object, adds apply_patch
bashoffAn options object with runner and env adds bash. See Add a bash tool

An unknown key, a shared option inside one tool's options, and true as the value of bash throw TypeError.

Each tool's options also take a signature, which sets the model-facing name, description, and JSON Schema. The defaults are the signatures of every integration:

ToolDefault signatureModel input
readdefaultReadSignature(){ path, offset?, limit? }
editdefaultEditSignature({ matchers }){ path, old_string, new_string, replace_all? }
writedefaultWriteSignature(){ path, content }
apply_patchdefaultPatchSignature(){ patch } as JSON
bashdefaultBashSignature({ runner, limits }){ command, timeout?, cwd? }, with timeout in milliseconds

A signature with a grammar, such as freeformPatchSignature(), sets constrainedSampling on the tool, so a model that supports grammar tools gets the grammar. multiEditSignature() gives the coding agent's edit input, { path, edits: [{ oldText, newText }] }. See Tool definitions to change a signature.

read returns images by default. Set read: { converters: [] } to turn images off.

Single tools

Each tool also has its own factory: createAgentCoreReadTool(), createAgentCoreEditTool(), createAgentCoreWriteTool(), createAgentCoreApplyPatchTool(), and createAgentCoreBashTool(). Each takes the options of its core factory, with the same defaults, plus signature. The options types are AgentCoreReadToolOptions and the four others. The tool types are AgentCoreReadTool, AgentCoreMutationTool, and AgentCoreBashTool.

A single factory shares nothing with another one. Use the bundle when you install more than one tool.

Alternatives

Add a bash tool

pi-agent-core has no tools of its own. To give the model a shell, pass a runner and an environment in bash. This tool runs commands on the local machine:

import { nodeCommandRunner, nodeFileSystem } from "@better-fs-tools/node";
import { createAgentCoreFsTools } from "@better-fs-tools/pi/agent-core";
import { shellEnv } from "@better-fs-tools/shell";

const cwd = "/work/repo";

// read, edit, write, and bash in /work/repo.
export const fsTools = createAgentCoreFsTools({
  fs: nodeFileSystem({ cwd, allowedRoots: [cwd] }),
  bash: { runner: nodeCommandRunner({ cwd }), env: shellEnv(() => process.env) },
});

true as the value of bash throws TypeError, because the module starts no process and does not read process.env. The allowed roots of fs do not limit what a command touches. See the bash guide for limits, policy, and runners.

Use pi-ai only

A host can also call @earendil-works/pi-ai directly and run the tools itself. Declare each tool in the request with its name, description, and parameters. For each tool call in the model's answer, call tool.execute(toolCall.id, toolCall.arguments). Then add a toolResult message with the tool's content and isError. pi-ai only sends and receives the messages. The loop, the parallel calls, and the hooks are yours.

See also

On this page