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 本来要停止时才处理,适合“等你做完再问”的消息。

源码对应:

  • getSteeringMessages
  • getFollowUpMessages
  • prepareNextTurn
  • shouldStopAfterTurn

AgentMessage 与 LLM Message 的边界

循环内部一直使用 AgentMessage[]。只有在调用模型前,才执行:

messages = await config.transformContext(messages, signal)
llmMessages = await config.convertToLlm(messages)

这很关键。AgentMessage 可以包含 coding-agent 自定义消息,如:

  • bashExecution
  • custom
  • branchSummary
  • compactionSummary

但 provider 只需要看到标准 MessageuserassistanttoolResult。转换边界在模型调用前,避免低层 loop 被产品层消息类型污染。

流式 assistant 响应

streamAssistantResponse() 的流程:

  1. 可选 transformContext
  2. convertToLlm
  3. 构造 ContextsystemPrompt + messages + tools
  4. 解析 API key。
  5. 调用 streamFn || streamSimple
  6. 监听 provider 事件:
  7. start
  8. text_start/delta/end
  9. thinking_start/delta/end
  10. toolcall_start/delta/end
  11. done/error
  12. 每次 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 能及时更新。
  • toolResult message 按原始 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 队列给用户并发输入提供了明确语义。