Agent Loop¶
packages/agent/src/agent-loop.ts 是 Pi 最核心也最干净的一段代码。它把 agent 的执行定义成一组消息、一个上下文、一个模型流函数、若干工具和一串事件。
本章涉及的具体函数行号见源码索引。
核心职责¶
Agent loop 负责:
- 把新 prompt 加入 context。
- 调用模型生成 assistant message。
- 流式转发 assistant 的文本、thinking、tool call 增量。
- 执行 assistant 请求的工具。
- 把 tool result 写回 context。
- 处理 steering 与 follow-up 队列。
- 在每个生命周期点发出
AgentEvent。
Agent loop 不负责:
- 从文件系统加载上下文。
- 把消息写入 session。
- 显示 UI。
- 管理 API key。
- 发现扩展。
- 决定何时压缩历史。
双层循环¶
runLoop() 有外层和内层两级循环:
外层: agent 可能停止时,再检查 follow-up 队列
内层: 只要还有 tool call 或 steering 消息,就继续下一 turn
1. 注入 pending steering
2. stream assistant response
3. 执行 tool calls
4. prepareNextTurn
5. shouldStopAfterTurn
这让两类用户消息语义不同:
- steering:agent 工作中插入,当前 assistant 的工具不会被跳过,但会在下一次 LLM call 前注入。
- follow-up:agent 本来要停止时才处理,适合“等你做完再问”的消息。
源码对应:
getSteeringMessagesgetFollowUpMessagesprepareNextTurnshouldStopAfterTurn
AgentMessage 与 LLM Message 的边界¶
循环内部一直使用 AgentMessage[]。只有在调用模型前,才执行:
messages = await config.transformContext(messages, signal)
llmMessages = await config.convertToLlm(messages)
这很关键。AgentMessage 可以包含 coding-agent 自定义消息,如:
bashExecutioncustombranchSummarycompactionSummary
但 provider 只需要看到标准 Message:user、assistant、toolResult。转换边界在模型调用前,避免低层 loop 被产品层消息类型污染。
流式 assistant 响应¶
streamAssistantResponse() 的流程:
- 可选
transformContext。 convertToLlm。- 构造
Context:systemPrompt + messages + tools。 - 解析 API key。
- 调用
streamFn || streamSimple。 - 监听 provider 事件:
starttext_start/delta/endthinking_start/delta/endtoolcall_start/delta/enddone/error- 每次 partial 更新都替换 context 里的最后一条 assistant message,并发出
message_update。
这样 UI 可以实时显示 partial message,同时最终 context 中保留的是完整 assistant message。
工具执行策略¶
Pi 的工具执行不是简单 Promise.all(toolCalls.map(...))。
Preflight¶
每个 tool call 都会先经过 prepareToolCall():
- 找不到工具则生成 error tool result。
- 如果工具提供
prepareArguments,先做参数兼容修正。 - 用
validateToolArguments()进行 schema 校验。 - 调用
beforeToolCall,允许外层阻止执行。
Sequential 与 Parallel¶
执行策略由两层决定:
- 全局
toolExecution:"parallel"或"sequential"。 - 单工具
executionMode:某些工具可以强制 sequential。
只要批次里有 sequential 工具,整批就走顺序执行。这避免写文件、编辑文件等工具在并行时互相踩状态。
结果顺序¶
并行模式下,Pi 区分两个顺序:
tool_execution_end按工具完成顺序发出,UI 能及时更新。toolResultmessage 按原始 tool call 顺序写回,LLM 上下文稳定可重放。
这是一个小但很重要的工程取舍。
early terminate 语义¶
AgentToolResult 可带 terminate?: boolean。但 Pi 只有在当前批次所有 finalized tool result 都要求 terminate 时,才终止工具批次后的继续循环。这样单个工具不能随意截断其他工具的结果,避免并行批次里出现不完整上下文。
事件协议¶
AgentEvent 是 loop 对外唯一可观察接口:
| 事件 | 含义 |
|---|---|
agent_start / agent_end |
一次 agent run 开始和结束 |
turn_start / turn_end |
一个 assistant response 加工具执行的周期 |
message_start / message_update / message_end |
消息生命周期 |
tool_execution_start/update/end |
工具执行生命周期 |
产品层通过这些事件实现 UI、持久化、扩展、自动压缩,而 loop 本身不需要知道这些功能存在。
设计启发¶
- 把 agent loop 做成“消息状态机 + 事件流”,比把 CLI/会话/UI 混在一起更容易测试和复用。
- 自定义消息类型可以存在,但要把它们隔离在
convertToLlm边界。 - 工具执行要同时考虑执行效率、UI 反馈和上下文确定性。
- steering/follow-up 队列给用户并发输入提供了明确语义。