Runtime:循环如何真正运行

逐段跟进 runLoop、streamAssistantResponse 与 tool batch,理解停止、打断和继续。

ANALYSIS SNAPSHOTPi v0.82.15bc1c2c0

核心不是 while(true),而是退出条件

Agent 会一直工作,并不是因为它永不停止,而是每轮结束时都检查:还有工具要跑吗?用户插入了新方向吗?排队任务还在吗?都没有才结束。

INTERACTIVE TRACE一次 turn 如何流动
ACTIVE STAGEPrompt

消息进入队列

点击节点查看阶段;这不是定时演示,而是一张可键盘操作的调用链。

内外两层循环

while (true) {                         // follow-up boundary
  while (hasMoreToolCalls || pendingMessages.length) {
    // inject steering → stream model → execute tools → turn_end
  }
  const followUps = await getFollowUpMessages();
  if (followUps.length) continue;
  break;
}

这段伪代码省略了错误、事件和 hooks,但保留了 Pi 最重要的控制语义:steering 在 run 中改变方向,follow-up 在本应结束后开启下一轮。

packages/agent/src/agent-loop.ts:155-275runLoop()

Streaming 不是最后再切字符串

Provider stream 产生 start、文本/思考/toolcall 的 start-delta-end,以及 done/error。Pi 把 partial assistant message 放进 context,并在每个 delta 到来时替换最后一条消息,再发出 message_update

这让 UI 能稳定看到“同一条消息逐步完成”,而不是把每个 token 当成独立消息。

Tool batch:并行但保持可重放顺序

默认模式是 parallel。Pi 先按源码顺序做参数准备和 before hook,然后并发执行允许的工具;tool_execution_end 按真实完成顺序出现,但最终 tool-result messages 按 assistant 原始 call 顺序写回。

packages/agent/src/agent-loop.ts:489-554executeToolCallsParallel()

如果任一 tool 标为 executionMode: "sequential",整个 batch 走顺序路径。这是一种保守语义:避免一个需要交互或会写共享状态的工具与其他调用并发。

错误与截断

  • provider error / aborted:结束当前 turn 与 run。
  • 输出因 token limit 截断:所有 tool calls 都不执行,返回错误结果让模型重发。
  • schema 校验失败或 before hook block:转成 error tool result,而不是让 loop 崩溃。
  • tool 抛错:最终化为可观察的失败事件与结果。
packages/agent/src/agent-loop.ts:374-405failToolCallsFromTruncatedMessage()

Human-in-the-loop 在哪里

Pi core 没有强制“每个危险命令都确认”。人类控制来自可组合机制:abort signal、steering queue、beforeToolCall、顺序工具、coding-agent extension UI,以及宿主的外部 sandbox。

Trade-off

这种设计不替应用做政策,因此嵌入简单、扩展自由;代价是安全策略必须由产品层明确实现,不能因为存在 hook 就宣称“安全”。