CLI / TUI / Web
職務
UI 層はユーザーが agent に触れるもう一組の入口(ゲートウェイ (gateway) のメッセージプラットフォーム以外):CLI(hermes_cli/、コマンドラインのサブコマンド体系)、TUI(ui-tui/、TypeScript 端末 UI + tui_gateway/ Python 橋渡し)、Web(web/、ブラウザ UI)。三者とも最終的にユーザー入力を agent の run_conversation に送り、出力を描画し戻す。ゲートウェイ層とは並列——ゲートウェイが外部プラットフォームを、UI 層がローカルユーザーを受け持つ。
主要ファイル
CLI
hermes_cli/__init__.py— パッケージ入口_parser.py— コマンドライン引数解析auth.py/auth_commands.py— 認証(Nous Portal など)active_sessions.py— アクティブセッション管理backup.py/checkpoints.py— バックアップとチェックポイントblueprint_cmd.py— ブループリントコマンドbrowser_connect.py/bundles.py/callbacks.pybanner.py/build_info.py— バナーとビルド情報
TUI
ui-tui/README— 端末 UI(TypeScript、packages/src を含む)ui-tui/package.json— 依存とスクリプトtui_gateway/entry.py— TUI ゲートウェイ入口server.py— TUI サーバーtransport.py/ws.py— トランスポートと WebSocketevent_publisher.py— イベントパブリッシュhost_supervisor.py/compute_host.py— ホスト監督slash_worker.py/synthetic_turn.py/render.py/project_tree.py_stdin_recovery.py— stdin 回復
Web
web/README— ブラウザ UIweb/index.html/vite.config.ts— Vite 入口web/src/— フロントエンドソース
CLI サブコマンドの表層
hermes_cli/_parser.py はトップレベルの argparse を独立して切り出し、relaunch.py などのモジュールが main を走らせずにどんな flag があるか内省できるようにしている。パーサーは chat サブコマンドとグローバルオプションだけを管り、他のサブコマンドは main.py でその場で構築する(cmd_* 関数と強結合)。_EPILOGUE の例がユーザーが向き合うサブコマンドの全景だ。
hermes Start interactive chat
hermes --tui Launch the modern TUI
hermes auth add <provider> Add a pooled credential
hermes gateway Run messaging gateway
hermes dashboard Start web UI dashboard (port 9119)CLI は「対話入口」であると同時に「運用パネル」——auth/model/config/gateway/dashboard/backup がすべて同じサブコマンドツリーにぶら下がる。chat とその派生(-c/--resume)だけが run_conversation に入り、他は各自のモジュールに直行する。
TUI ゲートウェイの stdin/JSON-RPC 主ループ
TUI は端末描画を TypeScript 側(ui-tui/)に置き、Python 側は軽量な tui_gateway 橋だけを走らせる。両者は stdin/stdout 上の JSON-RPC で通信する。tui_gateway/entry.py の main() の核心は readline + dispatch ループだ(tui_gateway/entry.py)。
while True:
raw = sys.stdin.readline()
if not raw:
# Stdin fell through — spurious EOF (child flipped O_NONBLOCK)
# or genuine close. handle_spurious_eof が継続判定を行う。
if not handle_spurious_eof(_recovery_times, _log_exit):
break
continue
try:
req = json.loads(raw.strip())
except json.JSONDecodeError:
write_json({"jsonrpc": "2.0", "error": {"code": -32700, ...}, "id": None})
continue
resp = dispatch(req)いくつかのエンジニアリング詳細に注意:handle_spurious_eof は子プロセスが O_NONBLOCK を反転させたことによる「偽の EOF」を処理する。_log_exit は stdout への書き込み失敗時にクラッシュログを残す。SIGPIPE は明示的に無視し(signal.signal(signal.SIGPIPE, signal.SIG_IGN))、バックグラウンドの TTS/beep スレッドが既に閉じたパイプに書き込み、プロセス全体を道連れにするのを防ぐ。
三者は最終的に run_conversation に合流
どの入口から入っても、対話系のリクエストは最終的に AIAgent.run_conversation に至る——これは単なる転送器で、本当の主ループは agent/conversation_loop.py にある(run_agent.py:6350)。
def run_conversation(self, user_message, system_message=None,
conversation_history=None, ..., moa_config=None) -> Dict[str, Any]:
"""Forwarder — see ``agent.conversation_loop.run_conversation``."""
from agent.conversation_loop import run_conversation
from agent.portal_tags import set_conversation_context
token = set_conversation_context(self._conversation_root_id())
with scoped_runtime_main({}):
return run_conversation(self, user_message, ...)会話 ID は ContextVar で押し下げられる——主ループ内のすべての LLM 呼び出し(圧縮 (compression)、ビジョン、MoA スロット、バックグラウンド審視の fork)が自動的に同じタグを持ち、各呼び出し点で明示的に渡す必要がない。Agent インスタンスは init_agent(agent/agent_init.py:276) で組み立てられ、シグネチャの長い callback 引数群(step_callback/stream_delta_callback/event_callback/...)が三つの殻のフックだ——同じ agent で殻ごとに異なるコールバックを掛け、描画形態が完全に変わる。
データフロー
- CLI:
hermes <subcommand>→hermes_cli/_parser.pyが解析 → サブコマンドモジュール(auth/backup/blueprint/...)が実行。対話系コマンドは最終的にAIAgent.run_conversation:6350を呼ぶ。 - TUI:
ui-tui/(TS フロントエンド)が WebSocket でtui_gateway/ws.pyに接続、tui_gatewayが端末イベントを agent に橋渡し(tui_gateway/server.pyが agent を保持)。出力はtui_gateway/event_publisher.pyでフロントに送られる。 - Web:
web/(Vite フロントエンド)も同様にバックエンド経由で agent に接続し、対話とツール結果を描画する。 - 三者の agent インスタンスはすべて
init_agent:276で組み立てられ、同じ機能層を共有する。
設計動機
なぜ三つの殻がそれぞれ独自のループを走らせず run_conversation を共用するのか? 核心は「描画の差異 vs 業務の一致」だ。ストリーミング token、ツール進度、イベント形式は殻の役目で、セッション (session) 状態、ツール呼び出し (tool dispatch)、予算 (budget) 減算、記憶注入は agent の役目だ。殻をコールバック層に隔離すれば、殻を替えても業務を変えず、業務を追加しても三箇所を直す必要がない。tui_gateway が単独で存在するのは、TS 側が Python agent を直接保持できないからだ——これは言語橋であって、業務層ではない。
境界と失敗
- TUI の stdin/stdout は JSON-RPC パイプであってログではない。
printデバッグで stdout に書くと TUI のパースを壊す。Python 側のログは stderr に流れ、TUI はそれをgateway.stderrイベントとして Activity パネルに届ける。 tui_gateway子プロセスが落ちても TUI は自動再接続しない。_CRASH_LOGは事後取证用であって、回復機構ではない——ユーザーが手動でhermes --tuiを再起動しなければならない。- Web は TUI の代替ではない。
web/はhermes dashboard(ポート 9119)を走り、位置付けはローカルコンソール + 長会話表示で、リモート多ユーザーアクセスのウェブ製品ではない。
まとめ
UI 層 = 三つの殻(CLI/TUI/Web)+ 一つの tui_gateway 橋渡し。業務を持たず、入出力の形だけを担う。ゲートウェイ層と並列:ゲートウェイは外部メッセージプラットフォーム、UI 層はローカル端末/ブラウザを受け持つ。ACP(参照)は IDE を受け持ち、完全な「触達面」を構成する。