print と rpc モード
pi の非インタラクティブモードには二種類ある:print(単発、テキストまたは JSON 出力)と rpc(長接続、JSON-RPC over stdin/stdout)。print モードは pi -p "fix this bug" のようなスクリプト呼び出しに使い、rpc モードは pi を他の GUI アプリ(VS Code 拡張、web フロントエンド)に組み込むのに使う。両者は AgentSessionRuntime を共有するが、イベント購読、出力フォーマット、拡張 UI 統合のやり方は完全に異なる。
責務
- print モード:
runPrintModeはmode: "text" | "json"を受け取る。text モードは最終 assistant メッセージのテキストだけを出力し、json モードはすべてのAgentSessionEventをストリーミング出力する。packages/coding-agent/src/modes/print-mode.ts:32-66参照。 - rpc モード:
runRpcModeはattachJsonlLineReaderで stdin から JSON コマンドを読み、handleCommandで 29 種類のコマンド型にディスパッチし、イベントとレスポンスを stdout に書く。packages/coding-agent/src/modes/rpc/rpc-mode.ts:48-70、packages/coding-agent/src/modes/rpc/rpc-mode.ts:371-625参照。 - signal 処理:両モードは SIGTERM / SIGHUP ハンドラを登録し、まず
killTrackedDetachedChildrenしてからdisposeRuntimeで bash 子プロセスを残さない。packages/coding-agent/src/modes/print-mode.ts:47-63、packages/coding-agent/src/modes/rpc/rpc-mode.ts:351-365参照。 - 拡張 UI ブリッジ:rpc モードは
createExtensionUIContextを実装し、拡張の dialog / widget リクエストをextension_ui_requestイベントでクライアントに送り、クライアントはextension_ui_responseで返す。packages/coding-agent/src/modes/rpc/rpc-mode.ts:83-130参照。 - session 差し替えフック:両モードは
runtimeHost.setRebindSessionで rebind コールバックを登録し、session 切り替え後に再度bindExtensionsしてイベント再購読する。packages/coding-agent/src/modes/print-mode.ts:67-108、packages/coding-agent/src/modes/rpc/rpc-mode.ts:306-349参照。
設計動機
なぜ print と rpc でコードを共有しないのか?出力契約が違うからだ。print モードの stdout は片方向(テキストか JSON 行のいずれか)で、rpc モードの stdin/stdout は双方向(コマンド入り、イベント+レスポンス出)だ。rpc はさらに拡張 UI リクエスト用の pending Promise map(クライアントの応答を待つ)を処理する必要がある。この仕組みは print モードには不要で、共有すると不必要な複雑さが入る。だから二つのファイルはそれぞれ rebindSession と handleEvent を実装している。
rpc モードの 29 個のコマンド型(prompt、steer、follow_up、abort、new_session、get_state、set_model、cycle_model、bash、fork、clone、export_html など)は AgentSession パブリック API の完全な写像だ。クライアントは RPC 経由で AgentSession が晒す全メソッドを呼べる。これで pi は JSON-RPC を喋れる限り、どんな言語で書かれた GUI にも組み込める。
print モードの mode: "text" パスは最後の assistant メッセージだけを見る。成功なら content.text を stdout に書き、失敗(stopReason === "error" || "aborted")なら stderr に書いて exitCode 1 を返す。シンプルで、pi -p "..." | jq のようなシェルスクリプト用法に向く。
主要ファイル
packages/coding-agent/src/modes/print-mode.ts:14-26—PrintModeOptions:mode、messages、initialMessage、initialImages。packages/coding-agent/src/modes/print-mode.ts:32-70—runPrintMode関数シグネチャ、disposeRuntime、registerSignalHandlers、rebindSession。packages/coding-agent/src/modes/print-mode.ts:110-145— 主フロー:initialMessage 送信、messages ループ送信、text モードで最後の assistant メッセージを出力。packages/coding-agent/src/modes/rpc/rpc-mode.ts:1-13— ファイルヘッダコメント。rpc プロトコル(コマンド、レスポンス、イベント、拡張 UI)を記述。packages/coding-agent/src/modes/rpc/rpc-mode.ts:48-70—runRpcMode関数シグネチャ、output/success/errorヘルパー。packages/coding-agent/src/modes/rpc/rpc-mode.ts:83-130—createDialogPromise。拡張 UI リクエストの Promise + timeout + abort 管理。packages/coding-agent/src/modes/rpc/rpc-mode.ts:306-349—rebindSession:拡張再バインド、session.subscribeでイベントを output する。packages/coding-agent/src/modes/rpc/rpc-mode.ts:371-625—handleCommandswitch、29 個のコマンド分岐。packages/coding-agent/src/modes/rpc/rpc-types.ts— RPC プロトコル型定義(RpcCommand、RpcResponse、RpcExtensionUIRequestなど)。
print モードの text パスは最後の assistant メッセージの text content だけ出力する:
// packages/coding-agent/src/modes/print-mode.ts:128-145
if (mode === "text") {
const state = session.state;
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage?.role === "assistant") {
const assistantMsg = lastMessage as AssistantMessage;
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
console.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
exitCode = 1;
} else {
for (const content of assistantMsg.content) {
if (content.type === "text") {
writeRawStdout(`${content.text}\n`);
}
}
}
}
}rpc モードの prompt コマンドは preflightResult を使い、prompt 受諾後にただちに response を返す:
// packages/coding-agent/src/modes/rpc/rpc-mode.ts:379-401
case "prompt": {
let preflightSucceeded = false;
void session
.prompt(command.message, {
images: command.images,
streamingBehavior: command.streamingBehavior,
source: "rpc",
preflightResult: (didSucceed) => {
if (didSucceed) {
preflightSucceeded = true;
output(success(id, "prompt"));
}
},
})
.catch((e) => {
if (!preflightSucceeded) {
output(error(id, "prompt", e.message));
}
});データフロー
両モードの入出力の対照:
境界と失敗
- rpc は @file 引数をサポートしない:
main.tsはディスパッチ前にparsed.mode === "rpc" && parsed.fileArgs.length > 0を検査してエラー終了する。packages/coding-agent/src/main.ts:475-478参照。 - rpc はテーマ切替をサポートしない:
setThemeは{ success: false, error: "Theme switching not supported in RPC mode" }を返す。packages/coding-agent/src/modes/rpc/rpc-mode.ts:291-294参照。 - rpc はツール展開をサポートしない:
getToolsExpandedは常に false を返す。TUI の概念は rpc に存在しないからだ。packages/coding-agent/src/modes/rpc/rpc-mode.ts:296-303参照。 - 拡張 UI timeout / abort:
createDialogPromiseはopts.timeoutとopts.signalをサポートし、タイムアウトや abort 時はデフォルト値を resolve し、reject しない。これで拡張ロジックを続行できる。packages/coding-agent/src/modes/rpc/rpc-mode.ts:90-130参照。 - print モードの stdout 接管:
main.tsは非インタラクティブモードでtakeOverStdout()を呼び、すべてのconsole.logを内部バッファにリダイレクトし、最後にflushRawStdoutで書き出す。TUI エスケープが混入するのを防ぐ。packages/coding-agent/src/modes/print-mode.ts:155-157参照。 - shutdown シグナル:
rpc-modeのshutdownRequestedフラグは拡張shutdownHandlerがトリガし、メインループが検出したら整理して終了する。packages/coding-agent/src/modes/rpc/rpc-mode.ts:79-81、packages/coding-agent/src/modes/rpc/rpc-mode.ts:337-339参照。
まとめ
print と rpc は pi の非インタラクティブモードで、AgentSessionRuntime を共有するが出力契約が異なる。print はテキストか JSON イベントストリームを片方向に出力し、rpc は双方向 JSON-RPC プロトコルで AgentSession の全メソッドを外部クライアントに晒し、拡張 UI リクエストもブリッジする。装配入口は CLI 入口とディスパッチ、runtime 差し替えは セッション switch/fork/import 参照。