如果文章对你有帮助,请点个“关注”
一、edit 工具
1.1 工具概述
功能:精确编辑文件(替换指定文本)核心特性:
• 必须精确匹配原文本(包括空白字符) • 支持 9 个参数别名(path/oldText/newText 各 3 个别名) • 编辑失败恢复机制(判断是否部分成功) • 不匹配提示(显示当前文件内容)
1.2 工具创建链
createEditTool (外部库 @mariozechner/pi-coding-agent) ↓wrapEditToolWithRecovery (编辑恢复机制) ↓wrapToolParamNormalization (参数标准化) ↓wrapToolWorkspaceRootGuard (工作目录限制) ↓最终工具1.3 编辑恢复机制
位置:第 114462 行
functionwrapEditToolWithRecovery(base, options) {return { ...base,execute: async (toolCallId, params, signal, onUpdate) => {// 1. 读取参数const { pathParam, oldText, newText } = readEditToolParams(params);// 2. 解析绝对路径const absolutePath = typeof pathParam === "string" ? resolveEditPath(options.root, pathParam) : void0;// 3. 读取原始内容(用于恢复判断)let originalContent;if (absolutePath && newText !== void0) {try { originalContent = await options.readFile(absolutePath); } catch {} }// 4. 执行编辑try {returnawait base.execute(toolCallId, params, signal, onUpdate); } catch (err) {// 5. 编辑失败,尝试恢复判断if (!absolutePath) throw err;let currentContent;try { currentContent = await options.readFile(absolutePath); } catch {}// 6. 判断编辑是否已应用(部分成功)if (typeof currentContent === "string" && newText !== void0) {if (didEditLikelyApply({ originalContent, currentContent, oldText, newText })) {// 编辑已应用,返回成功returnbuildEditSuccessResult(pathParam ?? absolutePath); } }// 7. 添加不匹配提示if (typeof currentContent === "string" && err instanceofError && shouldAddMismatchHint(err)) {throwappendMismatchHint(err, currentContent); }throw err; } } };}1.4 参数标准化
位置:第 114541 行
// edit 工具参数分组constCLAUDE_PARAM_GROUPS = {edit: [ {keys: ["path", "file_path", "filePath", "file"],label: "path alias" }, {keys: ["oldText", "old_string", "old_text", "oldString"],label: "oldText alias" }, {keys: ["newText", "new_string", "new_text", "newString"],label: "newText alias",allowEmpty: true// newText 可以为空(删除文本) } ]};// edit 工具参数别名(9 个)constCLAUDE_PARAM_ALIASES = [// path 别名 { original: "path", alias: "file_path" }, { original: "path", alias: "filePath" }, { original: "path", alias: "file" },// oldText 别名 { original: "oldText", alias: "old_string" }, { original: "oldText", alias: "old_text" }, { original: "oldText", alias: "oldString" },// newText 别名 { original: "newText", alias: "new_string" }, { original: "newText", alias: "new_text" }, { original: "newText", alias: "newString" }];// 参数标准化函数functionnormalizeToolParams(params) {if (!params || typeof params !== "object") return;const normalized = { ...params };// 提取结构化文本(处理嵌套对象)for (const key of ["path", "oldText", "newText"]) {normalizeTextLikeParam(normalized, key); }// 转换别名为标准参数normalizeClaudeParamAliases(normalized);return normalized;}1.5 结构化文本提取
位置:第 114598 行
functionextractStructuredText(value, depth = 0) {if (depth > 6) return;// 1. 直接是字符串if (typeof value === "string") return value;// 2. 数组类型if (Array.isArray(value)) {const parts = value.map((entry) =>extractStructuredText(entry, depth + 1) ).filter((entry) =>typeof entry === "string");return parts.length > 0 ? parts.join("") : void0; }// 3. 对象类型if (!value || typeof value !== "object") return;const record = value;if (typeof record.text === "string") return record.text;if (typeof record.content === "string") return record.content;if (Array.isArray(record.content)) {returnextractStructuredText(record.content, depth + 1); }if (Array.isArray(record.parts)) {returnextractStructuredText(record.parts, depth + 1); }// 4. 带 type 字段的对象if (typeof record.value === "string" && record.value.length > 0) {const type = typeof record.type === "string" ? record.type.toLowerCase() : "";const kind = typeof record.kind === "string" ? record.kind.toLowerCase() : "";if (type.includes("text") || kind === "text") {return record.value; } }}1.6 编辑恢复判断
functiondidEditLikelyApply(params) {const { originalContent, currentContent, oldText, newText } = params;// 1. 如果没有原始内容,无法判断if (!originalContent) returnfalse;// 2. 如果 oldText 不再存在,且 newText 存在,可能已应用if (!currentContent.includes(oldText) && currentContent.includes(newText)) {returntrue; }// 3. 如果内容发生了变化,可能部分应用if (originalContent !== currentContent) {returntrue; }returnfalse;}functionbuildEditSuccessResult(filePath) {return {content: [{type: "text",text: `Successfully edited ${filePath}` }],details: {path: filePath,status: "success" } };}1.7 不匹配提示
functionappendMismatchHint(err, currentContent) {const maxContextChars = 2000;const context = currentContent.length > maxContextChars ? currentContent.slice(0, maxContextChars) + "\n...(truncated)..." : currentContent; err.message += `\n\nCurrent file content:\n\`\`\`\n${context}\n\`\`\``; err.message += `\n\nMake sure oldText exactly matches the content you want to replace.`;return err;}functionshouldAddMismatchHint(err) {return err.message.includes("oldText") || err.message.includes("not found") || err.message.includes("no match");}1.8 执行流程图
edit 工具调用 ↓1. 参数标准化 ├─ path/file_path/filePath/file → path ├─ oldText/old_string/old_text/oldString → oldText └─ newText/new_string/new_text/newString → newText ↓2. 验证必填参数(path, oldText) ↓3. 读取原始内容(用于恢复判断) ↓4. 执行编辑(精确替换) ├─ 查找 oldText(必须完全匹配) ├─ 替换为 newText └─ 保存文件 ↓5. 编辑失败处理 ├─ 读取当前内容 ├─ 判断编辑是否已部分应用 ├─ 是 → 返回成功 └─ 否 → 添加不匹配提示(显示当前内容) ↓6. 返回结果1.9 返回结果格式
成功:
{"content":[{"type":"text","text":"Successfully edited src/main.py"}],"details":{"path":"src/main.py","status":"success"}}失败(不匹配):
{"content":[{"type":"text","text":"Error: oldText not found in file.\n\nCurrent file content:\n```\ndef hello():\n print('Hello')\n```\n\nMake sure oldText exactly matches the content you want to replace."}],"details":{"error":"oldText_not_found","path":"src/main.py"}}二、process 工具
2.1 工具概述
功能:管理后台 exec 会话核心特性:
• 8 个 actions(list/poll/log/write/send-keys/paste/submit/kill) • 会话范围限制(scopeKey) • 支持交互式输入(stdin) • 支持按键发送(send-keys)
2.2 工具创建函数
位置:第 17108 行
functioncreateProcessTool(defaults) {if (defaults?.cleanupMs !== void0) setJobTtlMs(defaults.cleanupMs);const scopeKey = defaults?.scopeKey;const supervisor = getProcessSupervisor();constisInScope = (session) => !scopeKey || session?.scopeKey === scopeKey;return {name: "process",label: "process",description: "Manage running exec sessions: list, poll, log, write, send-keys, submit, paste, kill.",parameters: processSchema,execute: async (_toolCallId, args, _signal, _onUpdate) => {const params = args;// === action: list ===if (params.action === "list") {const running = listRunningSessions() .filter((s) =>isInScope(s)) .map((s) => ({sessionId: s.id,status: "running",pid: s.pid ?? void0,startedAt: s.startedAt,runtimeMs: Date.now() - s.startedAt,cwd: s.cwd,command: s.command,name: deriveSessionName(s.command),tail: s.tail,truncated: s.truncated }));const finished = listFinishedSessions() .filter((s) =>isInScope(s)) .map((s) => ({sessionId: s.id,status: s.status,startedAt: s.startedAt,endedAt: s.endedAt,runtimeMs: s.endedAt - s.startedAt,cwd: s.cwd,command: s.command,name: deriveSessionName(s.command),tail: s.tail,truncated: s.truncated,exitCode: s.exitCode ?? void0,exitSignal: s.exitSignal ?? void0 }));return {content: [{type: "text",text: [...running, ...finished] .toSorted((a, b) => b.startedAt - a.startedAt) .map((s) => {const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);return`${s.sessionId}${pad(s.status, 9)}${formatDurationCompact$1(s.runtimeMs) ?? "n/a"} :: ${label}`; }) .join("\n") || "No running or recent sessions." }],details: {status: "completed",sessions: [...running, ...finished] } }; }// === 其他 action 需要 sessionId ===if (!params.sessionId) {return {content: [{ type: "text", text: "sessionId is required for this action." }],details: { status: "failed" } }; }const session = getSession(params.sessionId);const finished = getFinishedSession(params.sessionId);const scopedSession = isInScope(session) ? session : void0;const scopedFinished = isInScope(finished) ? finished : void0;constfailedResult = (text) => ({content: [{ type: "text", text }],details: { status: "failed" } });constresolveBackgroundedWritableStdin = () => {if (!scopedSession) {return {ok: false,result: failedResult(`No active session found for ${params.sessionId}`) }; }if (!scopedSession.backgrounded) {return {ok: false,result: failedResult(`Session ${params.sessionId} is not backgrounded.`) }; }const stdin = scopedSession.stdin ?? scopedSession.child?.stdin;if (!stdin || stdin.destroyed) {return {ok: false,result: failedResult(`Session ${params.sessionId} stdin is not writable.`) }; }return { ok: true, session: scopedSession, stdin }; };constwriteToStdin = async (stdin, data) => {awaitnewPromise((resolve, reject) => { stdin.write(data, (err) => err ? reject(err) : resolve()); }); };// === action: poll ===if (params.action === "poll") {if (!scopedSession) {if (scopedFinished) {resetPollRetrySuggestion(params.sessionId);return {content: [{type: "text",text: (scopedFinished.tail || `(no output recorded${scopedFinished.truncated ? " — truncated to cap" : ""})`) + `\n\nProcess exited with ${scopedFinished.exitSignal ? `signal ${scopedFinished.exitSignal}` : `code ${scopedFinished.exitCode ?? 0}`}.` }],details: {status: scopedFinished.status === "completed" ? "completed" : "failed",sessionId: params.sessionId,exitCode: scopedFinished.exitCode ?? void0,aggregated: scopedFinished.aggregated,name: deriveSessionName(scopedFinished.command) } }; }resetPollRetrySuggestion(params.sessionId);returnfailText(`No session found for ${params.sessionId}`); }if (!scopedSession.backgrounded) {returnfailText(`Session ${params.sessionId} is not backgrounded.`); }const pollWaitMs = resolvePollWaitMs(params.timeout);if (pollWaitMs > 0 && !scopedSession.exited) {const deadline = Date.now() + pollWaitMs;while (!scopedSession.exited && Date.now() < deadline) {awaitnewPromise((resolve) =>setTimeout(resolve, Math.max(0, Math.min(250, deadline - Date.now()))) ); } }const { stdout, stderr } = drainSession(scopedSession);const exited = scopedSession.exited;const exitCode = scopedSession.exitCode ?? 0;const exitSignal = scopedSession.exitSignal ?? void0;if (exited) {markExited(scopedSession, exitCode ?? null, exitSignal ?? null, status); }const output = [stdout.trimEnd(), stderr.trimEnd()] .filter(Boolean) .join("\n") .trim();const hasNewOutput = output.length > 0;const retryInMs = exited ? void0 : recordPollRetrySuggestion(params.sessionId, hasNewOutput);if (exited) resetPollRetrySuggestion(params.sessionId);return {content: [{type: "text",text: (output || "(no new output)") + (exited ? `\n\nProcess exited with ${exitSignal ? `signal ${exitSignal}` : `code ${exitCode}`}.` : "\n\nProcess still running.") }],details: {status: exited ? "completed" : "running",sessionId: params.sessionId,exitCode: exited ? exitCode : void0,aggregated: scopedSession.aggregated,name: deriveSessionName(scopedSession.command), ...typeof retryInMs === "number" ? { retryInMs } : {} } }; }// === action: log ===if (params.action === "log") {if (!scopedSession || !scopedSession.backgrounded) {return {content: [{ type: "text", text: `Session ${params.sessionId} is not backgrounded.` }],details: { status: "failed" } }; }constwindow = resolveLogSliceWindow(params.offset, params.limit);const { slice, totalLines, totalChars } = sliceLogLines( scopedSession.aggregated, window.effectiveOffset, window.effectiveLimit );const logDefaultTailNote = defaultTailNote(totalLines, window.usingDefaultTail);return {content: [{ type: "text", text: (slice || "(no output yet)") + logDefaultTailNote }],details: {status: scopedSession.exited ? "completed" : "running",sessionId: params.sessionId,total: totalLines, totalLines, totalChars } }; }// === action: write ===if (params.action === "write") {const { ok, stdin, session } = resolveBackgroundedWritableStdin();if (!ok) return stdin.result;awaitwriteToStdin(stdin, params.data);return {content: [{ type: "text", text: `Wrote ${params.data.length} bytes to stdin.` }],details: {status: session.exited ? "completed" : "running",sessionId: params.sessionId } }; }// === action: send-keys ===if (params.action === "send-keys") {const { ok, stdin, session } = resolveBackgroundedWritableStdin();if (!ok) return stdin.result;const keys = params.keys || [];const hex = params.hex || [];const literal = params.literal;if (literal) {awaitwriteToStdin(stdin, literal); } elseif (hex.length > 0) {const buffer = Buffer.from(hex.join(""), "hex");awaitnewPromise((resolve, reject) => { stdin.write(buffer, (err) => err ? reject(err) : resolve()); }); } else {for (const key of keys) {const keyData = namedKeyMap.get(key.toLowerCase()) || key;awaitwriteToStdin(stdin, keyData); } }return {content: [{ type: "text", text: `Sent keys to session.` }],details: { status: "running", sessionId: params.sessionId } }; }// === action: paste ===if (params.action === "paste") {const { ok, stdin, session } = resolveBackgroundedWritableStdin();if (!ok) return stdin.result;if (params.bracketed) {awaitwriteToStdin(stdin, BRACKETED_PASTE_START); }awaitwriteToStdin(stdin, params.text);if (params.bracketed) {awaitwriteToStdin(stdin, BRACKETED_PASTE_END); }return {content: [{ type: "text", text: `Pasted ${params.text.length} characters.` }],details: { status: "running", sessionId: params.sessionId } }; }// === action: submit ===if (params.action === "submit") {const { ok, stdin, session } = resolveBackgroundedWritableStdin();if (!ok) return stdin.result; stdin.end();return {content: [{ type: "text", text: "Submitted EOF to stdin." }],details: { status: "running", sessionId: params.sessionId } }; }// === action: kill ===if (params.action === "kill") {if (!scopedSession) {return {content: [{ type: "text", text: `No active session found for ${params.sessionId}` }],details: { status: "failed" } }; }const cancelled = cancelManagedSession(params.sessionId);if (!cancelled) {terminateSessionFallback(scopedSession); }return {content: [{ type: "text", text: `Killed session ${params.sessionId}.` }],details: { status: "killed", sessionId: params.sessionId } }; }return {content: [{ type: "text", text: `Unknown action: ${params.action}` }],details: { status: "failed" } }; } };}2.3 支持的 Actions
list | |||
poll | |||
log | |||
write | |||
send-keys | |||
paste | |||
submit | |||
kill |
2.4 命名按键映射
const namedKeyMap = newMap([ ["enter", "\r"], ["return", "\r"], ["tab", "\t"], ["escape", "\x1B"], ["esc", "\x1B"], ["space", " "], ["backspace", "\x7F"], ["up", "\x1B[A"], ["down", "\x1B[B"], ["right", "\x1B[C"], ["left", "\x1B[D"], ["home", "\x1B[1~"], ["end", "\x1B[4~"], ["pageup", "\x1B[5~"], ["pagedown", "\x1B[6~"]]);2.5 括号粘贴模式
constBRACKETED_PASTE_START = "\x1B[200~";constBRACKETED_PASTE_END = "\x1B[201~";// 作用:避免粘贴的文本被解释为命令// 例如:粘贴 "rm -rf /" 不会被立即执行2.6 执行流程图
process 工具调用 ↓1. 检查 action 类型 ↓2. action=list ├─ 获取运行中会话 ├─ 获取已完成会话 └─ 返回格式化列表 ↓3. 其他 actions(需要 sessionId) ├─ 验证 sessionId ├─ 检查会话范围(isInScope) └─ 获取会话对象 ↓4. 根据 action 执行 ├─ poll → 轮询输出 ├─ log → 读取日志 ├─ write → 写入 stdin ├─ send-keys → 发送按键 ├─ paste → 粘贴文本 ├─ submit → 发送 EOF └─ kill → 终止会话 ↓5. 返回结果三、关键机制对比
3.1 参数别名数量
| edit | ||
| process |
3.2 错误恢复
| 恢复机制 | ||
| 不匹配提示 | ||
| 会话恢复 |
3.3 安全限制
| 工作目录限制 | ||
| 沙盒隔离 | ||
| 路径边界检查 |
四、使用示例
4.1 edit 工具调用
用户:把 main.py 中的 print('Hello') 改为 print('Hello, World!')
大模型返回:
{"tool_call":{"name":"edit","arguments":{"path":"main.py","oldText":"print('Hello')","newText":"print('Hello, World!')"}}}执行结果(成功):
{"content":[{"type":"text","text":"Successfully edited main.py"}],"details":{"path":"main.py","status":"success"}}执行结果(失败):
{"content":[{"type":"text","text":"Error: oldText not found in file.\n\nCurrent file content:\n```\ndef main():\n print('Hi')\n```\n\nMake sure oldText exactly matches the content you want to replace."}],"details":{"error":"oldText_not_found","path":"main.py"}}4.2 process 工具调用
用户:查看后台运行的命令输出
大模型返回:
{"tool_call":{"name":"process","arguments":{"action":"poll","sessionId":"abc123","timeout":5000}}}执行结果:
{"content":[{"type":"text","text":"Line 1\nLine 2\nLine 3\n\nProcess still running."}],"details":{"status":"running","sessionId":"abc123","aggregated":"Line 1\nLine 2\nLine 3\n"}}如果文章对你有帮助,请点个“关注”
夜雨聆风