Runtime:循环如何真正运行
逐段跟进 runLoop、streamAssistantResponse 与 tool batch,理解停止、打断和继续。
5bc1c2c0核心不是 while(true),而是退出条件
Agent 会一直工作,并不是因为它永不停止,而是每轮结束时都检查:还有工具要跑吗?用户插入了新方向吗?排队任务还在吗?都没有才结束。
runLoop 有内外双循环。内层消费 tool calls 与 steering;外层在自然停止点拉取 follow-up。error/aborted、shouldStopAfterTurn 和空队列形成三类退出路径。
runLoop() 在 155–275 行;streamAssistantResponse() 在 281–371 行;executeToolCalls() 根据全局与 per-tool executionMode 选择顺序或并行。
消息进入队列
内外两层循环
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 就宣称“安全”。