AI News HubLIVE
サイト内リライト6 分で読了

ターミナルアプリにはDOMが必要

agent-tui はオープンソースのツールで、ターミナルアプリにブラウザのDOMのような構造化されたクエリ可能なインターフェースを提供し、AIエージェントが安定した参照や状態待機を利用してターミナル画面と対話できるようにします。

ソースHacker News AI著者: philips

C1のソフトウェアファクトリーSquireを構築していたとき、チームは少し馬鹿げた問題に直面しました。AIツールも人間向けに作られていたのです。Squireはエージェントに作業を割り当てることができますが、Claude Code、Codex、Pi、および類似のAIフレームワークは、まずターミナルアプリとして現れます。それらのライブインターフェースは人間向けのTUIです。プロンプト、ストリーミング応答、承認画面、ファイル変更ペイン、そして次の指示を待つカーソル。別のエージェントはそのインターフェースに入力できますが、応答が完了したか、承認画面が表示されたか、カーソルがプロンプトに戻ったかを知る必要があります。

それがagent-tuiが解決する問題です。PTY上で対象プログラムを実行し、デーモン内でターミナル状態を維持し、レンダリングされた画面をテキストまたは安定したrefを持つ構造化アウトラインとして公開し、クライアントがスナップショットを撮り、キーを押し、名前付き画面状態を待つことを可能にします。ターミナルアプリに、ブラウザ自動化を有用にしたのと同じクエリ可能な表面を提供します。

agent-tuiはオープンソースであり、Apache-2.0ライセンスで公開されています。その設計はagent-browserでの経験に基づいています。エージェントにピクセルの山ではなく、クエリ可能なものを与えるというアイデアをターミナルアプリに適用したものです。

一般的なSquireパターンはオーケストレーションエージェントです。コーディングフレームワークがタスクを受け取り、別のフレームワークを駆動して作業を実行します。このデモでは、OpenAIのCodexがagent-tuiを使用してPiフレームワークを実際のターミナルセッションで駆動します。agent-tuiは外部のCodex TUIを起動し、@codex.inputを待ち、タスクを入力してEnterキーを押します。Codexは以下のコマンドシーケンスを実行し、Piの周りに2番目のagent-tuiデーモンを起動します。

Pi側は単なる別のagent-tuiセッションです。結果は2つのライブ画面です。1つはCodexがタスクを受け取り、もう1つはPiがネストされたセッション内で回答します。

VercelのAI SDKはフレームワークエージェントCLIをプロバイダ固有の表面として扱い、汎用ラッパーではありません。agent-tuiはターミナル画面に対して同じアプローチを取ります。各アプリの形状を維持し、エージェントがクエリできる部分を公開します。

なぜターミナルアプリに構造が必要か?ほとんどの有用なターミナルプログラムは人間向けに作られており、機械向けではありません。htop、vim、lazygit、psql、言語REPL、そして新しいエージェントCLI(Claude CodeやCodex)は異なる仕事を持っています。それらは1つの自動化問題を共有しています。ライブインターフェースがターミナルセッションであることです。PTYを所有し、セルグリッドを再描画し、人間が何が変わったかを推測することを期待します。一部のツールはバッチモードを公開しますが、多くは公開しません。バッチモードが存在しても、それは観察、割り込み、または誘導する必要がある人間セッションとは異なるインターフェースであることがよくあります。

エージェントは簡単にバイトをターミナルに書き込めます。難しいのは、それらのバイトが着地した後に何が起こったかを知ることです。全画面プログラムはその場で再描画したり、カーソルを移動したり、代替画面に入ったり、1つのフィールドを更新したり、"準備完了"というきれいな行を決して印刷しなかったりします。

通常の選択肢はターミナルとの悪い契約になります。エスケープシーケンス解析はバイトストリームをAPIとして扱います。レンダリングされたテキストのスクレイピングは状態を捨てます。キーストローク間のスリープは問題をスケジューラに押し付け、スクリプトはCIが遅いか、プロンプトが一致しない状態になるまで動作します。

ターミナルにDOMを与えましょう。Vimは良いストレステストです。全画面エディタであり、行を印刷するコマンドではありません。ここでagent-tuiはスリープではなくrefを通じて実際のVimセッションを駆動します。録画では、左ペインがagent-tuiコマンドを発行し、右ペインがVim PTYです。ドライバはバッファを待ち、モードを読み取り、挿入モードに入り、hello worldを書き込み、hello-world.txtを保存し、ファイル内容をチェックします。

生のターミナルテキストはこの仕事に適していません。agent-tuiはアウトラインを公開します。ロールと安定したrefを持つスクリーン領域のツリーです。refは画面上の何かの名前です。エージェントが「Vimモードインジケータ」と言えるようにし、「24行目1列目」と言う必要をなくします。@vim.modeは永続的です。値がnormal、insert、または他のものでも、画面の同じ部分を命名します。refは小さなセレクタ言語でクエリできます。組み込みのvimおよびシェルアダプタは、@vim.mode@shell.promptなどの名前付きrefを発行します。TOMLマニフェストは、Rustコードを追加せずにagent-tuiに別のアプリの領域を教えることができます。アダプタがなくても、汎用アダプタは画面を粗い領域にグループ化し、refを与えます。

チームはブラウザ自動化のパターンを参考にしました。そこではagent-browserがエージェントのためにうまく機能しています。ブラウザエージェントはピクセル(412, 308)をクリックするべきではなく、ボタンをクリックするべきです。ターミナルエージェントは固定行に依存すべきではなく、agent-tuiがモード、プロンプト、フォーカスされたペイン、またはテーブルを命名できます。

スクリーン状態の待機。スナップショットはいつ撮るかを知っていて初めて役立ちます。sleep 0.2はスケジューリング、ターミナル再描画、およびテスト対象プログラムについての推測です。高速マシンでは長すぎ、負荷の高いランナーでは短すぎます。さらに悪いことに、気にかけている状態に結びついていません。waitサブコマンドはスクリーン状態に結びついています。refの出現、refの消失、セレクタ値、正規表現、イベントシーケンス、または子プロセスの終了を待つことができます。まだ名前付きrefがない画面の場合、クライアントはスナップショットを撮り、その画面ハッシュを保持し、入力を送信し、レンダリングされたグリッドが変わるまで待つことができます。

以下はスリープなしの完全なvim編集です:

  1. spawnでvimを起動
  2. waitでバッファが存在するのを待つ
  3. press iで挿入モードに入る
  4. waitでモードがinsertになるのを待つ
  5. typeでテキストを入力
  6. press <Esc>で挿入を離れる
  7. waitでモードがnormalに戻るのを待つ
  8. press :wqで保存して終了

各コマンドは次の観測可能な遷移を待ちます。press iの後、スクリプトはvimがテキストを受け入れる準備ができているとは仮定しません。解析されたモードがinsertになるまで待ちます。<Esc>の後、モードが再びnormalになるまで待ちます。

refは一般的な誤検知を避けます。正規表現はバッファ内のリテラル単語"insert"に一致する可能性があります。@vim.mode[value=insert]の待機はVimの解析されたモードフィールドを監視します。任意の画面テキストを見ているわけではありません。

不在を待つこともできます。wait --ref '@vim.cmdline[focused]' --goneはコマンドプロンプトが閉じるまでブロックします。ターミナルテストでは、これは通常「おそらく完了」と「UI状態が変わった」の違いです。

レンダリングされたテキストへのフォールバック。すべてのアプリにアダプタや有用な状態信号があるわけではありません。htopは良い例です。JSONモードがなく、有用な出力は多くの場合レンダリングされた画面そのものです。agent-tuiはhtopをspawnし、wait --idle 500で画面が変化しなくなるのを待ち、テキストモードでスナップショットを撮ります。より良い信号がないアプリには--idleを使用し、構造があるアプリにはrefとセレクタを使用します。

tmuxとexpectはどうか?明らかな疑問は、これが単にtmux send-keysとcapture-pane、またはexpectのラッパーなのかということです。それらは有用ですが、より低いレベルで止まっています。tmux capture-paneはレンダリングされたグリッドからテキストを提供しますが、ロール、名前付き領域、永続的なハンドルは提供しません。tmux send-keysは入力を書き込めますが、それに続く画面状態についての意見はありません。expectは行指向であり、プロンプトと行を印刷するプログラムには優れていますが、その場でセルグリッドを再描画する全画面ncursesアプリには適していません。agent-tuiはバイトストリームの上、エージェントの下に位置し、ターミナル状態を読み取り、画面の各部分に名前を割り当て、呼び出し元がそれらの名前を待つことを可能にします。

stdoutで十分な場合はstdoutを使用。すべてのコマンドにライブターミナルが必要なわけではありません。プログラムにすでに非対話モードがある場合、正しいインターフェースはstdin、stdout、stderr、および終了コードです。runサブコマンドはデータを返すべきコマンドのために存在します。エージェントに型付けされたログ付きラッパーを提供し、PTY自動化はライブ画面に集中し続けます。runはまた、非対話モードを公開するAI CLI(claude -pcodex execpi --printopencode run)の前面にも立ち、あるモデルの答えを画面スクレイピングなしで別のステップの入力にできます。askはそのパスの短いラッパーです。

ライブAI CLIセッションには両方のパスがあります。1回限りの回答にはデータパスを使用します。人間のターミナルセッションにはPTYパスを使用します。プロバイダマニフェストはプロンプトと応答を画面領域として公開します。アダプタはレンダリングされたセルからプロンプトと応答を命名します。プロバイダのトランスクリプトAPIを読み取りません。ストリーミング回答がすべてのAI CLIにわたって最終であることを知るには、依然としてプロバイダ固有のイベントまたはサイドチャネルが必要です。この分割は重要であり、2つのケースを分離します。子がすでにデータ生成プロセスである場合はrunを使用し、子がインタラクティブ画面である場合はspawn、snapshot、press、waitを使用します。

アーティファクトのキャプチャ。ライブセッションはファイルも生成します。デーモンは各ペインをasciicast-v3形式で記録し、asciinemaエコシステムと互換性があります。agent-tuiは同じキャストをテスト入力として使用します。replayは元のプログラムを起動せず、記録された出力バイトを新しいターミナルエンジンに送り、レンダリングされたスナップショットと比較します。デモセッションは回帰テスト入力になります。スクリーンショットについては、snapshotは現在のグリッドをPNGにレンダリングでき、オプションの注釈と枠線をドキュメント用に追加できます。