Agent 類別與生命週期
Agent 是 @mariozechner/pi-agent-core 對外的有狀態入口。下層是裸的 runAgentLoop 迴圈,上層是 AgentSession 那樣的編排層。Agent 在中間負責:持有 transcript 和工具表、管 activeRun 生命週期、維護 steering/followUp 兩條待辦佇列、把 AgentOptions 翻譯成 AgentLoopConfig、用 runWithLifecycle 包住真正的迴圈呼叫,並透過 subscribe 把事件發給外部 listener。可以把它理解成「迴圈之上的狀態殼」,迴圈本身不存任何狀態。
職責
- 持有狀態:
AgentState包含messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage,建構時由createMutableAgentState初始化。見packages/agent/src/agent.ts:158-188。 - 佇列管理:
steeringQueue和followUpQueue各自一個PendingMessageQueue,mode 取"one-at-a-time"或"all"。見packages/agent/src/agent.ts:113-144與packages/agent/src/agent.ts:200-280。 - 生命週期鉤子:
beforeToolCall/afterToolCall在建構時存下,裝進createLoopConfig傳給底層迴圈。見packages/agent/src/agent.ts:410-436。 - 執行封裝:
runWithLifecycle負責建立AbortController、設isStreaming、catch 例外走handleRunFailure、finally裡finishRun。見packages/agent/src/agent.ts:438-486。 - 入口三件套:
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 讓呼叫方決定一次排空還是只取一條。
關鍵檔案
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear,drain在"all"模式全取,否則只取首條。packages/agent/src/agent.ts:158-207—class Agent欄位與建構子,預設streamFn指向streamSimple,toolExecution預設"parallel"。packages/agent/src/agent.ts:312-323—prompt多載:支援AgentMessage、AgentMessage[]、string + images,內部走normalizePromptInput→runPromptMessages。packages/agent/src/agent.ts:355-372—normalizePromptInput:字串+圖片轉成帶timestamp的 userAgentMessage。packages/agent/src/agent.ts:374-400—runPromptMessages/runContinuation分別呼叫runAgentLoop/runAgentLoopContinue,都包在runWithLifecycle裡。packages/agent/src/agent.ts:410-436—createLoopConfig:把實例欄位拼成AgentLoopConfig,getSteeringMessages/getFollowUpMessages閉包到佇列的drain。packages/agent/src/agent.ts:438-486—runWithLifecycle+handleRunFailure+finishRun。
建構子把可替換件全裝上,streamFn 預設指向 streamSimple:
// 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 閉包成非同步函式,迴圈透過這兩個回呼拉訊息:
// 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:
// 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"的 assistantAgentMessage附加到 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 形狀,看 型別契約。