Tools: read/bash/edit/write/grep/find/ls
The tools/ directory is pi's built-in toolset. Each tool is a two-layer structure: a ToolDefinition (with schema, description, prompt snippet, guidelines) and an AgentTool (the execution function), converted via wrapToolDefinition. index.ts re-exports all tool factories and provides three preset combinations: createCodingTools / createReadOnlyTools / createAllTools. File-writing tools (edit, write) are serialized through withFileMutationQueue to avoid races from concurrent edits to the same file.
Responsibilities
- read: reads text/image files. Images support auto-resize (
autoResizeImages); text is truncated byDEFAULT_MAX_LINES/DEFAULT_MAX_BYTES. Seepackages/coding-agent/src/core/tools/read.ts:205-236. - bash: executes shell commands, tracks detached child processes, supports abort and timeout. See
packages/coding-agent/src/core/tools/bash.ts:264-280. - edit / write: file editing and creation, both serialized through
withFileMutationQueue. Seepackages/coding-agent/src/core/tools/edit.ts:288-316andpackages/coding-agent/src/core/tools/write.ts:181-210. - grep / find / ls: file search and listing, built on
fdandrg, respecting.gitignore. Seepackages/coding-agent/src/core/tools/grep.ts:122-135,packages/coding-agent/src/core/tools/find.ts:112-125, andpackages/coding-agent/src/core/tools/ls.ts:99-112. - Composition factories:
createToolDefinition/createTooldispatch byToolName.createCodingToolsis the default 4-tool set (read/bash/edit/write);createReadOnlyToolsis the 4-tool read-only set (read/grep/find/ls). Seepackages/coding-agent/src/core/tools/index.ts:96-136andpackages/coding-agent/src/core/tools/index.ts:138-184. - Serialized writes:
withFileMutationQueuemaintains a Promise chain keyed by file realpath. Writes to the same file queue in call order; different files run in parallel. Seepackages/coding-agent/src/core/tools/file-mutation-queue.ts:19-39.
Design Motivation
Why split tools into two layers? ToolDefinition is for the LLM: schema, description, prompt snippet, guidelines — these get injected into the system prompt. AgentTool is the execution body, only meaningful to the runtime. Splitting them lets one definition drive multiple execution bodies — e.g., the read tool definition can be paired with a different operations implementation in RPC mode (read unit tests can inject a mock filesystem).
Why serialize writes? An LLM can call edit/write on the same file multiple times in parallel within one turn. Without serialization, the later write would overwrite the earlier one and corrupt content. withFileMutationQueue uses realpathSync.native as the key, so symlinks and real paths map to the same queue. Different files still run in parallel, so throughput isn't lost.
Why does read default to promptGuidelines: ["Use read to examine files instead of cat or sed."]? The system prompt builder concatenates tool guidelines, and an LLM that sees "prefer read over bash running cat" avoids some inefficient paths.
Key Files
packages/coding-agent/src/core/tools/— tool directory: bash.ts / edit.ts / find.ts / grep.ts / ls.ts / read.ts / write.ts / truncate.ts / file-mutation-queue.ts.packages/coding-agent/src/core/tools/index.ts:1-69— re-exports of allcreate*Tool/create*ToolDefinition.packages/coding-agent/src/core/tools/index.ts:81-115—ToolNametype andcreateToolDefinitiondispatch.packages/coding-agent/src/core/tools/index.ts:138-196— three preset combos:createCodingTools/createReadOnlyTools/createAllTools.packages/coding-agent/src/core/tools/file-mutation-queue.ts:4-13—fileMutationQueuesMap andgetMutationQueueKey, realpath resolution.packages/coding-agent/src/core/tools/file-mutation-queue.ts:19-39—withFileMutationQueue, the Promise-chain queue.packages/coding-agent/src/core/tools/read.ts:205-236—createReadToolDefinition, image detection, resize, abort signal handling.packages/coding-agent/src/core/tools/read.ts:360-362—createReadTool, wrapped viawrapToolDefinition.
withFileMutationQueue is implemented as a promise chain — a new call attaches itself after the old promise's .then:
// packages/coding-agent/src/core/tools/file-mutation-queue.ts:19-39
export async function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
const key = getMutationQueueKey(filePath);
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
let releaseNext!: () => void;
const nextQueue = new Promise<void>((resolveQueue) => {
releaseNext = resolveQueue;
});
const chainedQueue = currentQueue.then(() => nextQueue);
fileMutationQueues.set(key, chainedQueue);
await currentQueue;
try {
return await fn();
} finally {
releaseNext();
if (fileMutationQueues.get(key) === chainedQueue) {
fileMutationQueues.delete(key);
}
}
}The read tool's execute takes signal: AbortSignal and rejects immediately on Ctrl+C:
// packages/coding-agent/src/core/tools/read.ts:225-237
const absolutePath = resolveReadPath(path, cwd);
return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
(resolve, reject) => {
if (signal?.aborted) {
reject(new Error("Operation aborted"));
return;
}
let aborted = false;
const onAbort = () => {
aborted = true;
reject(new Error("Operation aborted"));
};
signal?.addEventListener("abort", onAbort, { once: true });Data Flow
Tool selection and execution:
Edges and Failures
- Abort signal: read rejects on
signal.abortedand checks theabortedflag in async tasks to avoid race conditions. Seepackages/coding-agent/src/core/tools/read.ts:228-237. - realpath failure:
getMutationQueueKeyfalls back toresolve(filePath)whenrealpathSync.nativethrows, so even deleted files can still be queued. Seepackages/coding-agent/src/core/tools/file-mutation-queue.ts:6-13. - Image resize failure: when
resizedreturns null, it's replaced with the text placeholder "Image omitted: could not be resized below the inline image size limit" rather than throwing. Seepackages/coding-agent/src/core/tools/read.ts:255-258. - Non-vision models: read checks whether
ctx?.modelsupports vision; if not, it appends a note viagetNonVisionImageNoteso the LLM knows the image is not actually visible. - Unknown tool name: the
defaultbranch ofcreateToolDefinition's switch throwsUnknown tool nameinstead of silently returning undefined. Seepackages/coding-agent/src/core/tools/index.ts:112-114.
Summary
tools/ is pi's built-in toolbox, with a two-layer structure (ToolDefinition for the LLM, AgentTool for the runtime) and writes serialized through withFileMutationQueue to avoid concurrent races. Three preset combos cover coding, read-only, and full-set scenarios. For wiring these tools into a session, see createAgentSession Wiring; for pre/post tool call hooks, see AgentSession Orchestration Layer.