Skip to content

CLI / TUI / Web

源码版本v2026.7.20

職責

界面層是使用者接觸 agent 的另一組入口(除網關訊息平台外):CLI(hermes_cli/,命令列子命令體系)、TUI(ui-tui/,TypeScript 終端 UI + tui_gateway/ Python 橋)、Web(web/,瀏覽器界面)。三者都最終把使用者輸入送進 agent 的 run_conversation,把輸出渲染回來。它們和網關 (gateway) 層是並列關係——網關接外部平台,界面層接本地使用者。

關鍵檔案

CLI

TUI

Web

CLI 子命令表面

hermes_cli/_parser.py 把頂層 argparse 單獨抽出来,這樣 relaunch.py 等模組能在不跑 main 的前提下內省有哪些 flag。解析器只管 chat 子命令和全域選項,其他子命令在 main.py 裡就地建構(和 cmd_* 函式強耦合)。_EPILOGUE 裡這串範例就是使用者面對的子命令全景:

python
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.pymain() 核心是一個 readline + dispatch 迴圈(tui_gateway/entry.py):

python
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):

python
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 slot、背景審視 fork)都自動帶上同一個標籤,不需要在每個呼叫點顯式傳。Agent 實例由 init_agent(agent/agent_init.py:276)裝配,簽名裡一長串 callback 參數(step_callback/stream_delta_callback/event_callback/...)就是三種殼的鉤子——同一份 agent,不同殼掛不同回調,渲染形態就完全不同。

資料流

  1. CLI:hermes <subcommand>hermes_cli/_parser.py 解析 → 子命令模組(auth/backup/blueprint/...)執行;對話類命令最終呼叫 AIAgent.run_conversation:6350
  2. TUI:ui-tui/(TS 前端)經 WebSocket 連 tui_gateway/ws.py,tui_gateway 把終端事件橋到 agent(tui_gateway/server.py 持有 agent),輸出經 tui_gateway/event_publisher.py 推回前端。
  3. Web:web/(Vite 前端)同樣經後端連 agent,渲染對話與工具結果。
  4. 三者的 agent 實例都經 init_agent:276 裝配,共用同一套能力層。

設計動機

為什麼三套殼共用一個 run_conversation 而不是各自跑一套迴圈?核心是「渲染差異 vs 業務一致」。串流 (stream) token、工具進度、事件格式是殼的事;會話狀態、工具呼叫、預算 (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),定位是本地控制台 + 長會話查看,不是遠端多使用者存取的 web 產品。

小結

界面層 = 三種殼(CLI/TUI/Web)+ 一個 tui_gateway 橋。它們不持業務,只負責輸入輸出形態。與網關層並列:網關接外部訊息平台,界面層接本地終端/瀏覽器。ACP(見此)則接 IDE,構成完整的入口面。

非官方社群學習站,內容以 MIT 授權的 NousResearch/hermes-agent 原始碼為依據。