ZH

 


权限闸门: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。