Pi Coding Agent RPC ガイド:JSON プロトコル、Provider、カスタム Web UI

Pi Coding Agent の RPC クライアントを構築し、プロセス管理、JSON メッセージ、イベント、セッション、Provider、Web UI 分離、セキュリティ、復旧を解説します。

Pi コーディング エージェントは、端末内で操作できるだけではありません。 その RPC モードは、エージェントを子プロセスとして実行し、標準入力を介して JSON コマンドを受信し、標準出力から応答とイベントを継続的に返します。 このパスは、デスクトップ シェル、ブラウザ コンソール、内部作業指示システム、または自動テストに適していますが、端末出力を Web ページに適合させるほど単純ではありません。 本当に対処する必要があるのは、プロセスの有効期間、リクエスト数、ストリーミング イベント、セッション ディレクトリ、アクセス許可、および例外の回復です。

RPC モードではリモート ネットワーク呼び出しが解決されない

ここでの RPC はネイティブ プロセス プロトコルです。 ホスト プログラムは pi --mode rpc を開始し、行ごとに JSON オブジェクトを書き込み、Pi の標準出力を行ごとに読み取ります。 HTTP ポートを自動的にリッスンすることはなく、ユーザー認証、TLS、またはクロスマシン アクセスも行いません。 Web UI を提供するには、Pi 子プロセスを独自のバックエンドで保持し、ブラウザーはこのバックエンドにのみ接続する必要があります。 公開 Web ページでネイティブ コマンドを直接生成したり、作業ディレクトリにアクセスしたりしないでください。

まず、コマンドとバージョンが同じインストール

からのものであることを確認します。 Pi をインストールした後、まずサービスを実行する予定のアカウントと同じアカウントで確認してください:

1
2
3
pi --version
pi --help
pi --mode rpc --help

Windows サービス、WSL、およびプレーン PowerShell は、それぞれ異なる実行可能ファイルに解決される場合があります。 PowerShell で次のコマンドを使用してパスを確認します:

1
Get-Command pi | Select-Object Source, Version

アップグレード前にバージョンをログに記録するには、RPC クライアントをデフォルトですべてのイベント フィールドを永久に変更するのではなく、明示的なバージョンに対してテストする必要があります。

最小限のコマンドでプロトコルを観察

空のテスト ディレクトリから開始します:

1
2
3
mkdir pi-rpc-lab
cd pi-rpc-lab
pi --mode rpc --no-session

プロセスが開始されると、従来の対話型インターフェイスは表示されませんが、標準入力の JSON 行を待ちます。 リクエストを送信するときは、改行で終了する必要があります。改行なしで JSON を記述するだけでは、パーサーは待機し続ける可能性があります。 手動実験は最初の応答を確認するのに適していますが、正式な統合ではプログラムが stdout と stderr の両方を読み取る必要があります。

stdout をプロトコル チャネル

として扱う 標準出力の各行は、最初に完全な JSON メッセージとして解析される必要があります。 デバッグ ヒント、独自のログ プレフィックス、またはカラー コントロールを同じパイプに書き込まないでください。 ホスト プログラムの診断ログは、stderr または別のファイルに書き込まれます。 解析できない行を受信した場合は、元のテキスト、バージョン、前後のメッセージ番号を記録しますが、キーが含まれる可能性のある行全体を公開ログに送信しないでください。

堅牢な読み取りサイクルは 4 つのステップで構成されます。

  1. バイト ストリームを改行で分割します。
  2. JSON 解析を 1 行で実行します。
  3. メッセージ タイプに基づいて応答またはイベントを配布します。
  4. 不明なタイプは、プロセス全体をクラッシュさせるのではなく、互換性ブランチに入ります。

リクエスト ID は同時相関

の核心です 前のタスクがまだ出力をストリーミングしている間に、クライアントは制御コマンドを送信することがあります。 したがって、「次のレスポンスは前のリクエストに属する」という位置仮定は使えません。 コマンドごとに一意の ID を生成し、pending マッピングを維持します。 アンマッピングは、最終応答が到着した後に行われます。中間イベントはセッションまたは現在のターン状態によって処理されます。

1
2
3
4
5
6
7
type Pending = {
  resolve: (value: unknown) => void;
  reject: (reason: Error) => void;
  timer: ReturnType<typeof setTimeout>;
};

const pending = new Map<string, Pending>();

タイムアウトは、ホストが時間内に最終応答を取得できなかったことを意味するだけで、Pi 子プロセスが停止したことを意味するわけではありません。 タイムアウト後は、送信を中止するか、受信を続行するか、子プロセス全体を終了するかを決定する必要もあります。

子プロセスを起動するための Node.js の最小限のスケルトン

次のスケルトンは、特定のイベント フィールドを意図的にバインドせず、信頼性の高い分岐とプロセスの終了のみを担当します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { spawn } from "node:child_process";
import readline from "node:readline";

const child = spawn("pi", ["--mode", "rpc", "--no-session"], {
  cwd: process.cwd(),
  stdio: ["pipe", "pipe", "pipe"],
  shell: false,
  env: { ...process.env },
});

const lines = readline.createInterface({ input: child.stdout });

lines.on("line", (line) => {
  try {
    const message = JSON.parse(line);
    routeMessage(message);
  } catch (error) {
    console.error("invalid RPC JSON", { line, error });
  }
});

child.stderr.on("data", (chunk) => {
  process.stderr.write(`[pi] ${chunk}`);
});

child.on("exit", (code, signal) => {
  rejectAllPending(new Error(`pi exited: ${code}/${signal}`));
});

shell: false は重要です。プロジェクト パスやユーザー テキストがシェルによって再度解釈されるのを避けるために、パラメーターは配列として渡されます。 Windows が pi を見つけられない場合は、コマンド文字列を連結するのではなく、起動フェーズ中に絶対パスを解決する必要があります。

ユニファイドカプセル化送信機能

すべての書き込みは、メッセージ サイズを制限し、プロセス ステータスを確認し、末尾の改行を保証するためにエントリを経由します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function send(message: Record<string, unknown>) {
  if (!child.stdin.writable) {
    throw new Error("Pi RPC stdin is not writable");
  }

  const payload = JSON.stringify(message);
  if (Buffer.byteLength(payload, "utf8") > 256 * 1024) {
    throw new Error("RPC message is too large");
  }

  child.stdin.write(payload + "\n");
}

大きなファイルに JSON 文字列を埋め込まないでください。 ファイルを制御された作業ディレクトリに置き、エージェントにツールを通じてファイルを読み取らせ、実際のパスがまだディレクトリ内にあるかどうかをバックエンドで確認します。

イベント フローには明示的なステート マシンが必要

プロンプトは、キュー、開始、テキスト増分、ツール呼び出し、ツール結果、終了、またはエラーを通過する場合があります。 フロントエンドで継続的に追加される文字列を維持するだけでなく、ツール イベントや再試行が簡単に繰り返し発生する可能性があります。 ターンごとに次のステータスを保存することをお勧めします:

  • queued: リクエストは書き込まれましたが、まだ開始が確認されていません。
  • running: モデルまたはツールのイベントを受信しています。
  • aborting: ユーザー要求は中止され、最終ステータスを待っています。
  • completed: 最終応答が完了しました。
  • failed: プロトコル エラー、モデル エラー、またはプロセス終了。

イベントが異常な順序で到着した場合、元のシーケンス番号が保持され、UI は成功したふりをする代わりに「不完全ステータス」を表示することができます。

最初にバックプレッシャーに対処し、次にストリーミング エクスペリエンスについて話す

stdin.write() false が返されると、バッファーに圧力がかかっていることを示します。 このとき、送信は中断され、drain イベントを待って続行されます。 ブラウザ側も WebSocket 保留キューを制限する必要があり、遅いクライアントがサーバー メモリを無制限に占有することはできません。 テキスト デルタは 30 ~ 80 ミリ秒ごとにマージしてプッシュできるため、DOM の更新とネットワーク パケットが削減されます。 ツールの結果は通常、全体がイベントとして送信されるため、キャラクター レベルのスライスには適していません。

プロバイダとモデルは起動パラメータ

公式 RPC ドキュメントには、--provider および --model 起動パラメータが記載されています。 たとえば、サービスはさまざまな目的に応じてさまざまなワーカーを開始できます:

1
pi --mode rpc --provider anthropic --model MODEL_NAME --no-session

通常のフロントエンド ユーザーが任意のプロバイダー名やモデル パラメーターを送信できるようにしないでください。 バックエンドは許可リストを維持し、「高速」、「高品質」、「ローカル」をレビュー済みの組み合わせにマッピングします。 プロバイダーを切り替える前に、認証情報、コンテキストの制約、ツールの機能を確認してください。同じ名前のモデルでも、出力や請求動作が異なる場合があります。

キーは子プロセス環境

にのみ入ります API キーは、サーバー側のシークレット管理ツールまたは制限された環境変数に保存されます。 RPC JSON、ブラウザーの localStorage、セッション ヘッダー、またはエラー ポストバックにキーを書き込まないでください。 子プロセスを起動する際に、サービスプロセスのすべての変数を無条件に継承するのではなく、最小限の環境を構築できます。

1
2
3
4
const childEnv = {
  PATH: process.env.PATH,
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
};

実際の変数名は、選択したプロバイダーによって異なります。これが欠落している場合は、リクエストが途中で失敗することを避けるために、開始前にエラーが報告されます。

セッション ディレクトリが回復可能性を決定します

--no-session は、ワンタイムタスク、CI、プロトコルテストに適しています。プロセス終了後の履歴セッションには依存しません。 セッションを再開する必要がある場合は、Pi のセッション機能を使用し、--session-dir を介してデータを明示的な場所に置きます。

1
pi --mode rpc --session-dir ./runtime/sessions

サーバーは、アプリケーションのユーザー ID を内部のランダムなディレクトリ名にマップする必要があります。 ユーザーは、../、ドライブ文字、ネットワーク共有パスを直接入力することを禁止されています。 セッションをバックアップする前に、セッションにプロンプト、コード スニペット、ツール出力、またはビジネス データが含まれているかどうかを確認し、保存期間を設定します。

--name は、制御されたインスタンス

を区別するために使用されます。 長時間実行インスタンスの識別名を設定すると、ログの関連付けに役立ちます:

1
pi --mode rpc --name support-agent --session-dir ./runtime/support

名前は展開構成によって生成される必要があります。電子メール アドレス、顧客名、または作業指示書の全文を直接使用しないでください。 ログにはアプリケーション インスタンス ID、Pi プロセス PID、および起動バージョンも記録されるため、例外がどのサブプロセスに属しているかを復元できます。

Web UI は独自のバックエンド

によって分離される必要があります 推奨リンクは次のとおりです:

1
2
3
4
5
6
Browser
  -> HTTPS / WebSocket
Application backend
  -> stdin/stdout JSON lines
Pi RPC child process
  -> model provider and local tools

バックエンドは、ログイン、レート制限、作業ディレクトリ、プロバイダーのホワイトリスト登録、および監査を担当します。 ブラウザーは表示に必要なフィールドのみを受け取り、ネイティブの絶対パス、環境変数、マスクされていないツールの結果は表示されません。 WebSocket が切断された場合、バックエンドでターンを続行し、イベント シーケンス番号に従ってクライアントを補充できます。製品ルールに従ってアクティブに終了することもできます。

1 人のユーザー、1 つのプロセスが常に正しいとは限りません

常駐の子プロセスはすぐに回復しますが、メモリを占有し、より多くのコンテキストが保存されます。 リクエストごとに、より明確に分離された新しいプロセスが作成されますが、起動コストとセッション回復コストが増加します。 一般的な妥協策は、ワークスペースに基づいてワーカーを作成し、一定期間アイドル状態になった後に終了し、同時リクエストを各ワーカーのシリアル キューに入れることです。 2 つのリクエストで同じ Git ワークスペースを同時に変更しないでください。 並列処理が必要な場合は、独立したワークツリーまたはタスクの一時コピーを作成します。

ツールの権限はモデルの選択より重要です

Pi 子プロセスは、実行中のアカウントがアクセスできるファイルとコマンドを継承します。 Web UI を展開するときは、少なくとも次の分離を実行してください:

  • 管理者以外の専用アカウントを使用してください。
  • 作業ディレクトリは許可リストを使用します。
  • SSH キー、ブラウザ プロファイル、運用環境設定へのアクセスを拒否します。
  • まず、外部リポジトリの読み取り専用コピーまたは一時コピーを作成します。
  • 監査イベントは、ファイルの書き込み、コマンドの実行、およびネットワーク アクセスに対して保持されます。

コンテナーはファイル システムの範囲を減らすことができますが、それでもマウント、ネットワーク、およびホスト ソケットを制限する必要があります。 Docker ソケットをエージェントにハングすることは、高度なホスト制御機能を付与することと同じです。

一時停止とシャットダウンは分離する必要があります

「現在の回答を停止する」は、「Pi プロセスを強制終了する」ことと同じではありません。 現在のターンが認識可能な終了ステータスを受け取るように、プロトコルによって提供される中止コマンドの使用を優先します。 子プロセスは、プロトコルが応答しなくなった場合、標準出力が閉じられた場合、または強制終了期間を超えた場合にのみ終了します。 サービスが終了すると、まず新しいリクエストの受信を停止し、短い猶予期間を待ってから、標準入力を閉じてプロセスをリサイクルします。 Windows と Linux の信号セマンティクスは異なるため、終了テストは個別に実行する必要があります。

クラッシュ リカバリでは書き込み操作を自動的に再生できません

ツール呼び出し後に Pi がクラッシュした場合、ホストはファイルの書き込みが完了したかどうかを必ずしも知りません。 最後のプロンプトを無条件に再実行しないでください。再実行しないと、リクエストの再送信、再送信、またはファイルの上書きが発生する可能性があります。 回復インターフェイスには最後の確認イベントが表示され、ワークスペースを確認するか、会話を続行するか、新しいセッションを作成するかをユーザーに選択できるようにする必要があります。 読み取り専用クエリはべき等な再試行を使用して設計できます。書き込み操作には操作 ID または実行後の検証が必要です。

ログは、プロトコル、操作、監査の 3 つのカテゴリに分類されます

プロトコル ログには、メッセージ タイプ、リクエスト ID、イベント シーケンス番号、消費時間が記録されますが、デフォルトでは完全なテキストは保存されません。 実行ログ記録の PID、バージョン、終了コード、メモリ、および標準エラー出力の概要。 監査ログには、誰がどのワークスペースを開いたのか、どのツール機能が許可されたのか、どのような変更が行われたのかが記録されます。 3種類のログにはそれぞれアクセス権限と保存期間が設定されています。 マスキング ルールは、少なくとも API キー、認証ヘッダー、電子メール、絶対ユーザー パス、リポジトリ シークレットをカバーします。

を手動でクリックする代わりに、最初にプロトコル テストを作成します。 テスト クライアントは、pi --mode rpc --no-session を開始し、固定リクエストを送信して、

  1. を検証できます。 各 stdout 行は JSON として解析できます。
  2. リクエスト ID は最終レスポンスに関連付けることができます。
  3. テキストイベントとツールイベントは 2 回決済されません。
  4. 保留中の Promise はタイムアウト後にクリアされます。
  5. 子プロセスが異常終了すると、すべてのウェイターが失敗を受け取ります。

中国語、バックスラッシュ、改行、および長すぎるテキストを含む入力を追加して、UTF-8 と JSON エスケープを確認します。 CI では高価な実際のモデルを呼び出さないでください。サブプロセスを、同じプロトコルに従って固定イベントを出力する偽の実装に置き換えます。

オンラインにする前に 4 つの障害ドリルを実行する

まず、操作中にブラウザを切断し、バックエンドがイベントを無期限にキャッシュしないことを確認します。 次に、プロバイダーが電流制限または認証失敗を返して、エラーによってキーが公開されないことを確認します。 3 番目に、ツールの実行中に Pi を強制終了して、システムが書き込みを自動的に再実行しないことを確認します。 4 番目に、不明なメッセージ タイプを標準出力に表示し、クライアント レコードを確認して、後続の互換性のあるメッセージの処理を続行します。 これらの結果は、「ページが最初のテキストを受信する」よりも、統合が維持可能であることのより良い証拠となります。

RPC を選択するかライブラリを直接使用する

RPC の利点は、言語に依存しないこと、プロセスが明確に分離されていること、および公式の CLI 起動設定を再利用できることです。 トレードオフは、サブプロセス、JSON ブランチ、ステート マシン、およびバージョンの互換性を維持することです。 ホスト自体が TypeScript で、エージェントのライフサイクルを詳細に制御する必要がある場合は、対応するライブラリ インターフェイスを直接統合して評価できます。 ホストが Python、Go、デスクトップ プログラムである場合、またはエージェントが独立したワーカーである必要がある場合は、通常、RPC の方が境界を引くのが簡単です。

最小納品バージョン

は何ですか 信頼できる最初のバージョンには、少なくとも次の機能が必要です: 修正された Pi バージョン、単一ワークスペースのシリアル キュー、プロバイダーのホワイトリスト、バックエンド認証、イベント シーケンス番号、タイムアウトと中止、標準エラー ログ、セッション ディレクトリの検証、および例外終了クリーンアップ。 第 2 段階では、マルチワークスペースのワーカー プール、切断の再開、ツールの承認、使用状況の統計が追加されます。 最初に豪華なチャットバブルを作成し、その後オンラインになるまでファイルのアクセス許可とクラッシュの回復を残さないでください。

結論

Pi コーディング エージェント RPC の価値は、成熟したエージェント実行ループを明確なプロセス境界の背後に置くことです。 統合の品質は、ホストが JSON 行、リクエスト ID、イベント ステータス、セッション ディレクトリ、ツールの権限を正しく管理しているかどうかに依存します。 まず --no-session を使用して単一リクエスト プロトコル テストを完了し、次に永続セッションと Web UI を追加し、最後に切断、電流制限、およびクラッシュ ドリルを通じて回復戦略を検証します。 この方法で得られるのは、「チャットできるターミナル フォワーダー」ではなく、監査、分離、継続的なアップグレードが可能な一連のエージェント サービスです。

Pi RPC ドキュメント エントリ