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. 佇列管理:steeringQueue 和 followUpQueue 各自一個 PendingMessageQueue,mode 取 "one-at-a-time" 或 "all"。見 packages/agent/src/agent.ts:113-144 與 packages/agent/src/agent.ts:200-280。
  3. 生命週期鉤子:beforeToolCall/afterToolCall 在建構時存下,裝進 createLoopConfig 傳給底層迴圈。見 packages/agent/src/agent.ts:410-436。
  4. 執行封裝:runWithLifecycle 負責建立 AbortController、設 isStreaming、catch 例外走 handleRunFailure、finally 裡 finishRun。見 packages/agent/src/agent.ts:438-486。
  5. 入口三件套:prompt 收新輸入、continue 從末尾接續、steer/followUp 排隊。見 packages/agent/src/agent.ts:312-353 和 packages/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/continue 在 activeRun 已存在時直接拋錯,不靜默排隊。呼叫方應改走 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。

小結 ​

Agent 把 runAgentLoop 包成有狀態物件:持有 transcript、管兩條佇列、裝生命週期鉤子、統一 abort 訊號。往上看是 AgentSession 那種編排層,往下看是 雙層 while 主迴圈。生命週期鉤子如何被工具執行路徑消費,看 工具執行 sequential/parallel;狀態欄位和 AgentMessage/AgentEvent 形狀,看 型別契約。