Skip to content

Agent クラスとライフサイクル

源码版本v0.73.1

Agent@mariozechner/pi-agent-core が外部に晒すステートフルな入り口だ。下層は素の runAgentLoop ループ、上層は AgentSession のようなオーケストレーション層 (orchestration layer)。Agent はその中間に位置して、トランスクリプト (transcript) とツール表を保持し、activeRun のライフサイクルを管理し、steering/followUp 2 つの待ち行列をメンテし、AgentOptionsAgentLoopConfig に変換し、runWithLifecycle で実際のループ呼び出しを包み、subscribe でイベントを外部 listener に送る。循環自体は状態を何も持たないので、「ループの上に乗る状態の殻」と捉えればいい。

責務

  1. 状態の保持:AgentStatemessages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage を含み、構築時に createMutableAgentState で初期化される。packages/agent/src/agent.ts:158-188 参照。
  2. キュー管理:steeringQueuefollowUpQueue はそれぞれ 1 つの PendingMessageQueue で、mode は "one-at-a-time""all" を取る。packages/agent/src/agent.ts:113-144packages/agent/src/agent.ts:200-280 参照。
  3. ライフサイクルフック (lifecycle hooks):beforeToolCall/afterToolCall を構築時に保存し、createLoopConfig に詰めて下層のループに渡す。packages/agent/src/agent.ts:410-436 参照。
  4. 実行のラップ:runWithLifecycleAbortController を作り、isStreaming を立て、例外を catch して handleRunFailure に回し、finallyfinishRun する。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 を返すだけで何も保存しない。現実のコーディングアシスタントは「ターンをまたぐトランスクリプト、ストリーミング中に打ち続ける入力のキューイング、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 件だけ取るかを決める。

主要ファイル

コンストラクタは交換可能パーツを全部取り付ける。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 を非同期関数に閉じ込め、ループはこの 2 つのコールバックでメッセージを引く:

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 に包んでトランスクリプトに追記し、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 参照。

小ねた

AgentrunAgentLoop をステートフルなオブジェクトに包む:トランスクリプトを保持し、2 本のキューを管理し、ライフサイクルフックを取り付け、abort シグナルを統一する。上を見れば AgentSession のようなオーケストレーション層、下を見れば 二重 while ループ。ライフサイクルフックがツール実行パスでどう消費されるかは ツール実行 sequential/parallel を、状態フィールドと AgentMessage/AgentEvent の形は 型契約 を参照。