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 形狀,看 型別契約