源码
packages/coding-agent/src/core/tools/index.ts
import { createHash } from "node:crypto";
import { realpath, writeFile } from "node:fs/promises";
import type { AgentTool, AgentToolResult } from "@zpi-dev/agent";
import type { JsonValue, Model } from "@zpi-dev/ai";
import type { ToolContext, ToolDefinition } from "../types.ts";
import { beforeFile, type FileChange, recordFileChange } from "./file-change.ts";
import { type BashToolOptions, createBashToolDefinition } from "./pi/bash.ts";
import { createEditToolDefinition, type EditToolOptions } from "./pi/edit.ts";
import { FileReadState } from "./pi/file-read-state.ts";
import { createReadToolDefinition, type ReadToolOptions } from "./pi/read.ts";
import { wrapToolDefinition } from "./pi/tool-definition-wrapper.ts";
import { readRegularFile } from "./pi/utils/read-file.ts";
import { createWriteToolDefinition, type WriteToolOptions } from "./pi/write.ts";
export { createBashToolDefinition, createLocalBashOperations } from "./pi/bash.ts";
export { createEditToolDefinition } from "./pi/edit.ts";
export { FileReadState } from "./pi/file-read-state.ts";
export { createReadToolDefinition } from "./pi/read.ts";
export { createWriteToolDefinition } from "./pi/write.ts";
export type { BashToolOptions, EditToolOptions, ReadToolOptions, WriteToolOptions };
export type ToolContextFactory = () => Partial<ToolContext> | undefined;
export function createReadTool(
cwd: string,
options?: ReadToolOptions | (() => Model),
context?: ToolContextFactory,
): AgentTool {
const getModel = typeof options === "function" ? options : undefined;
const definition = createReadToolDefinition(cwd, typeof options === "function" ? undefined : options);
return wrapToolDefinition(definition, context ?? (() => ({ cwd, model: getModel?.() })));
}
export function createBashTool(
cwd: string,
options?: BashToolOptions,
context?: ToolContextFactory,
): AgentTool {
return wrapToolDefinition(createBashToolDefinition(cwd, options), context ?? (() => undefined));
}
// File-change observation is outside the Pi tool semantics, but runs inside its mutation queue.
// It adds persisted UI facts without altering matching, filesystem operations, or model-facing success text.
function observeMutation(
cwd: string,
tool: "write" | "edit",
options: WriteToolOptions | EditToolOptions | undefined,
outputDir: string | undefined,
context: ToolContextFactory | undefined,
): AgentTool {
const base =
tool === "write"
? createWriteToolDefinition(cwd, options as WriteToolOptions)
: createEditToolDefinition(cwd, options as EditToolOptions);
const wrapped = wrapToolDefinition(
base as ToolDefinition<typeof base.parameters, unknown, Partial<ToolContext> | undefined>,
context ?? (() => undefined),
);
return {
...wrapped,
execute: async (id, params, signal, update) => {
if (options?.operations) return wrapped.execute(id, params as never, signal, update);
let change: FileChange | undefined;
let diagnostic = "";
const observeWrite = async (path: string, content: string) => {
const before = await beforeFile(path);
let failure: unknown;
try {
await writeFile(path, content, "utf8");
} catch (error) {
failure = error;
}
try {
change = await recordFileChange(
await realpath(path).catch(() => path),
id,
before,
outputDir,
Boolean(failure),
);
} catch (error) {
if (failure) diagnostic = `文件最终状态不可读,可能已修改;记录不可用: ${String(error)}`;
else
change = {
path,
operation: before === null ? "created" : "modified",
toolCallId: id,
beforeHash: before === null ? null : createHash("sha256").update(before).digest("hex"),
afterHash: "unknown",
reason: "文件已修改,但记录不可用",
};
}
if (failure) throw failure;
};
const definition =
tool === "write"
? createWriteToolDefinition(cwd, {
...options,
operations: {
writeFile: observeWrite,
mkdir: async (path) => {
const { mkdir } = await import("node:fs/promises");
await mkdir(path, { recursive: true });
},
},
})
: createEditToolDefinition(cwd, {
...options,
operations: {
writeFile: observeWrite,
readFile: readRegularFile,
access: async (path) => {
const { access, constants } = await import("node:fs/promises");
await access(path, constants.R_OK | constants.W_OK);
},
},
});
let result: AgentToolResult;
try {
result = await wrapToolDefinition(
definition as ToolDefinition<
typeof definition.parameters,
unknown,
Partial<ToolContext> | undefined
>,
context ?? (() => undefined),
).execute(id, params as never, signal, update);
} catch (error) {
if (!change && !diagnostic) throw error;
result = {
isError: true,
content: [
{
type: "text",
text: `${String(error)}${diagnostic ? `\n${diagnostic}` : "; file has changed."}`,
},
],
details: undefined,
};
}
if (change)
result.details = {
...(result.details as Record<string, JsonValue> | undefined),
fileChange: { ...change, ...(result.isError ? { failed: true } : {}) },
};
return result;
},
};
}
export function createWriteTool(
cwd: string,
options?: WriteToolOptions | string,
context?: ToolContextFactory,
): AgentTool {
return observeMutation(
cwd,
"write",
typeof options === "string" ? undefined : options,
typeof options === "string" ? options : undefined,
context,
);
}
export function createEditTool(
cwd: string,
options?: EditToolOptions | string,
context?: ToolContextFactory,
): AgentTool {
return observeMutation(
cwd,
"edit",
typeof options === "string" ? undefined : options,
typeof options === "string" ? options : undefined,
context,
);
}
export function createCodingTools(
cwd: string,
outputDir?: string,
getModel?: () => Model,
context?: ToolContextFactory,
fileReadState = new FileReadState(),
): AgentTool[] {
return [
createReadTool(cwd, { fileReadState }, context ?? (() => ({ cwd, model: getModel?.() }))),
createBashTool(cwd, undefined, context),
observeMutation(cwd, "edit", { fileReadState }, outputDir, context),
observeMutation(cwd, "write", { fileReadState }, outputDir, context),
];
}