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.
Recommended configuration
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 importpi-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:
| Option | Default | Meaning |
|---|---|---|
fs | required | A writable filesystem, or a function that returns one for each call |
state | null | The read store. null turns read before write off. memoryStore() turns it on |
digest | sha256Digest() | The digest for read records. It is plain JavaScript, so it runs in a Worker |
locks | memoryLocks() | 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 |
applyPatch | off | true, or an options object, adds apply_patch |
bash | off | An 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:
| Tool | Default signature | Model input |
|---|---|---|
read | defaultReadSignature() | { path, offset?, limit? } |
edit | defaultEditSignature({ matchers }) | { path, old_string, new_string, replace_all? } |
write | defaultWriteSignature() | { path, content } |
apply_patch | defaultPatchSignature() | { patch } as JSON |
bash | defaultBashSignature({ 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
- Pi: the coding agent extension and SDK
- Pi durable: the tools in the durable harness
- Tool definitions: signatures, names, and descriptions
- Choose an integration: the defaults of every integration