Skip to content

Agent 类与生命周期

源码版本v0.73.1

Agent@mariozechner/pi-agent-core 对外的有状态入口。下层是裸的 runAgentLoop 循环,上层是 AgentSession 那样的编排层。Agent 在中间负责:持有 transcript 和工具表、管 activeRun 生命周期、维护 steering/followUp 两条待办队列、把 AgentOptions 翻译成 AgentLoopConfig、用 runWithLifecycle 包住真正的循环调用,并通过 subscribe 把事件发给外部 listener。可以把它理解成「循环之上的状态壳」,循环本身不存任何状态。

职责

  1. 持有状态:AgentState 包含 messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage,构造时由 createMutableAgentState 初始化。见 packages/agent/src/agent.ts:158-188
  2. 队列管理:steeringQueuefollowUpQueue 各自一个 PendingMessageQueue,mode 取 "one-at-a-time""all"。见 packages/agent/src/agent.ts:113-144packages/agent/src/agent.ts:200-280
  3. 生命周期钩子:beforeToolCall/afterToolCall 在构造时存下,装进 createLoopConfig 传给底层循环。见 packages/agent/src/agent.ts:410-436
  4. 运行封装:runWithLifecycle 负责创建 AbortController、设 isStreaming、catch 异常走 handleRunFailurefinallyfinishRun。见 packages/agent/src/agent.ts:438-486
  5. 入口三件套:prompt 收新输入、continue 从末尾接续、steer/followUp 排队。见 packages/agent/src/agent.ts:312-353packages/agent/src/agent.ts:252-259

设计动机

为什么不直接让上层调 runAgentLoop?因为循环本身是纯函数式的——传进去什么 context,它就跑什么,跑完返回 newMessages,不存任何东西。真实编码助手需要「跨轮的 transcript、流式期间继续打字要排队、abort 要有唯一信号源、失败要把 errorMessage 写进 state」。把这些塞进 runLoop 会让循环变重且不可复用,放到上层又会在 print/rpc/TUI 三种模式重复。Agent 抽出来当唯一状态壳,循环只管跑,状态只管存。

队列分两条而不是一条,是因为 steering(本轮插话,要在下一轮 assistant 回复前注入)和 followUp(本该结束时再起一轮)语义不同——steering 在内层 while 里 getSteeringMessages 拉,followUp 在外层 while 末尾 getFollowUpMessages 拉,见 双层 while 主循环PendingMessageQueue"all"/"one-at-a-time" mode 让调用方决定一次排空还是只取一条。

关键文件

构造器把可替换件全装上,streamFn 缺省指向 streamSimple:

typescript
// packages/agent/src/agent.ts:190-207
constructor(options: AgentOptions = {}) {
  this._state = createMutableAgentState(options.initialState);
  this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
  this.transformContext = options.transformContext;
  this.streamFn = options.streamFn ?? streamSimple;
  // ... getApiKey / onPayload / onResponse / beforeToolCall / afterToolCall ...
  this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
  this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
  this.transport = options.transport ?? "auto";
  this.toolExecution = options.toolExecution ?? "parallel";
}

createLoopConfig 把队列的 drain 闭包成异步函数,循环通过这两个回调拉消息:

typescript
// packages/agent/src/agent.ts:427-434
getSteeringMessages: async () => {
  if (skipInitialSteeringPoll) {
    skipInitialSteeringPoll = false;
    return [];
  }
  return this.steeringQueue.drain();
},
getFollowUpMessages: async () => this.followUpQueue.drain(),

runWithLifecycle 是所有运行入口的公共壳,出错走 handleRunFailure 把错误写进 state 再发 agent_end:

typescript
// packages/agent/src/agent.ts:454-461
try {
  await executor(abortController.signal);
} catch (error) {
  await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
  this.finishRun();
}

数据流

prompt(text) 的生命周期:

边界与失败

  • 并发 prompt 拒绝:prompt/continueactiveRun 已存在时直接抛错,不静默排队。调用方应改走 steer/followUp,见 packages/agent/src/agent.ts:316-320
  • continue 角色校验:末条消息若是 assistant,优先消费 steering/followUp 队列;两者都空时抛 "Cannot continue from message role: assistant",见 packages/agent/src/agent.ts:336-350
  • 失败不丢消息:handleRunFailure 把错误包成一条 stopReason: "aborted"|"error" 的 assistant AgentMessage 追加到 transcript,UI 能直接渲染,见 packages/agent/src/agent.ts:463-478
  • reset 清干净:reset 清 messages、流式状态、pendingToolCalls、errorMessage,也清两条队列,见 packages/agent/src/agent.ts:301-310
  • abort 单一源:abort() 只调 activeRun.abortController.abort(),循环和 stream 都监听同一个 signal,无需多路径协调,见 packages/agent/src/agent.ts:287-290

小结

AgentrunAgentLoop 包成有状态对象:持有 transcript、管两条队列、装生命周期钩子、统一 abort 信号。往上看是 AgentSession 那种编排层,往下看是 双层 while 主循环。生命周期钩子如何被工具执行路径消费,看 工具执行 sequential/parallel;状态字段和 AgentMessage/AgentEvent 形状,看 类型契约