Skip to content

Tools: read/bash/edit/write/grep/find/ls

源码版本v0.73.1

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

  1. read: reads text/image files. Images support auto-resize (autoResizeImages); text is truncated by DEFAULT_MAX_LINES / DEFAULT_MAX_BYTES. See packages/coding-agent/src/core/tools/read.ts:205-236.
  2. bash: executes shell commands, tracks detached child processes, supports abort and timeout. See packages/coding-agent/src/core/tools/bash.ts:264-280.
  3. edit / write: file editing and creation, both serialized through withFileMutationQueue. See packages/coding-agent/src/core/tools/edit.ts:288-316 and packages/coding-agent/src/core/tools/write.ts:181-210.
  4. grep / find / ls: file search and listing, built on fd and rg, respecting .gitignore. See packages/coding-agent/src/core/tools/grep.ts:122-135, packages/coding-agent/src/core/tools/find.ts:112-125, and packages/coding-agent/src/core/tools/ls.ts:99-112.
  5. Composition factories: createToolDefinition / createTool dispatch by ToolName. createCodingTools is the default 4-tool set (read/bash/edit/write); createReadOnlyTools is the 4-tool read-only set (read/grep/find/ls). See packages/coding-agent/src/core/tools/index.ts:96-136 and packages/coding-agent/src/core/tools/index.ts:138-184.
  6. Serialized writes: withFileMutationQueue maintains a Promise chain keyed by file realpath. Writes to the same file queue in call order; different files run in parallel. See packages/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

withFileMutationQueue is implemented as a promise chain — a new call attaches itself after the old promise's .then:

typescript
// 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:

typescript
// 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

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.