权限闸门:beforeToolCall / afterToolCall
工具能改文件、跑命令,所以不能让模型「想调就调」。pi 在每个工具调用前后各设一道关卡:beforeToolCall 决定放行 / 拒绝 / 改参数,afterToolCall 可以截断 / 重写结果。
toolCall │ ▼ beforeToolCall ── 拒绝 ─▶ 返回拒绝结果,不执行 │ 放行 / 改参数 ▼ 执行工具(parallel | sequential) │ ▼ afterToolCall ── 重写 / 截断 ─▶ 干净的 toolResult 回灌 context
并行 vs 串行
同一轮里模型可能一次性发起多个工具调用。pi 默认全部并行跑——快。引擎也允许某个工具把自己标记成 executionMode: "sequential";只要批次里有一个这样的工具,整批就退化为顺序执行:
packages/agent/src/agent-loop.ts —— 决定并行还是串行
const hasSequentialToolCall = toolCalls.some(
(tc) => tools.find((t) => t.name === tc.name)
?.executionMode === "sequential",
)
if (config.toolExecution === "sequential" || hasSequentialToolCall)
return executeToolCallsSequential(...)
return executeToolCallsParallel(...)但 pi 的内置工具都没有标记 sequential——它们照样并行。那「同时改同一个文件」的竞态怎么办?答案不是串行整批,而是一把按文件路径加锁的队列:edit / write 都包在 withFileMutationQueue(filePath, fn) 里,同一文件的写操作排队进行,不同文件依旧并行。
coding-agent/.../file-mutation-queue.ts —— 按文件序列化写操作
// 改同一个文件 → 排队;改不同文件 → 并行
export async function withFileMutationQueue(filePath, fn) {
const key = realpath(filePath)
const prev = queues.get(key) ?? Promise.resolve()
const next = prev.then(() => fn()) // 接在上一个操作之后
queues.set(key, next.catch(() => {}))
return next
}
afterToolCall的一个经典用途:把超长命令输出换成「…省略 N 行」,既保住关键信息,又不撑爆 Context。