Agent クラスとライフサイクル
Agent は @mariozechner/pi-agent-core が外部に晒すステートフルな入り口だ。下層は素の runAgentLoop ループ、上層は AgentSession のようなオーケストレーション層 (orchestration layer)。Agent はその中間に位置して、トランスクリプト (transcript) とツール表を保持し、activeRun のライフサイクルを管理し、steering/followUp 2 つの待ち行列をメンテし、AgentOptions を AgentLoopConfig に変換し、runWithLifecycle で実際のループ呼び出しを包み、subscribe でイベントを外部 listener に送る。循環自体は状態を何も持たないので、「ループの上に乗る状態の殻」と捉えればいい。
責務
- 状態の保持:
AgentStateはmessages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessageを含み、構築時にcreateMutableAgentStateで初期化される。packages/agent/src/agent.ts:158-188参照。 - キュー管理:
steeringQueueとfollowUpQueueはそれぞれ 1 つのPendingMessageQueueで、mode は"one-at-a-time"か"all"を取る。packages/agent/src/agent.ts:113-144とpackages/agent/src/agent.ts:200-280参照。 - ライフサイクルフック (lifecycle hooks):
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 を返すだけで何も保存しない。現実のコーディングアシスタントは「ターンをまたぐトランスクリプト、ストリーミング中に打ち続ける入力のキューイング、abort の単一シグナル源、失敗時の errorMessage 書き込み」を必要とする。これを runLoop に押し込むとループが重くて再利用できなくなり、上層に置くと print/rpc/TUI の 3 モードで重複する。Agent を唯一の状態の殻として切り出し、ループは走るだけ、状態は溜めるだけに分ける。
キューが 1 本ではなく 2 本なのは、steering (本轮の差し込み、次の assistant 応答の前に注入したい) と followUp (本来終わるはずのところで次ターンを起こす) で意味が違うからだ。steering は内側の while で getSteeringMessages が引き、followUp は外側の while の末尾で getFollowUpMessages が引く。二重 while ループ 参照。PendingMessageQueue の "all"/"one-at-a-time" mode は、呼び出し側が全部一気に drain するか先頭 1 件だけ取るかを決める。
主要ファイル
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear。drainは"all"mode なら全部、そうでなければ先頭 1 件だけ。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 を非同期関数に閉じ込め、ループはこの 2 つのコールバックでメッセージを引く:
// 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に包んでトランスクリプトに追記し、UI がそのままレンダリングできる。packages/agent/src/agent.ts:463-478参照。 - reset の完全クリア:
resetは messages、ストリーミング状態、pendingToolCalls、errorMessage を消し、2 本のキューも空にする。packages/agent/src/agent.ts:301-310参照。 - abort の単一ソース:
abort()はactiveRun.abortController.abort()だけを呼ぶ。ループと stream は同じ signal を監視し、複数経路の協調は要らない。packages/agent/src/agent.ts:287-290参照。
小ねた
Agent は runAgentLoop をステートフルなオブジェクトに包む:トランスクリプトを保持し、2 本のキューを管理し、ライフサイクルフックを取り付け、abort シグナルを統一する。上を見れば AgentSession のようなオーケストレーション層、下を見れば 二重 while ループ。ライフサイクルフックがツール実行パスでどう消費されるかは ツール実行 sequential/parallel を、状態フィールドと AgentMessage/AgentEvent の形は 型契約 を参照。