CLI / TUI / Web
职责
界面层是用户触达 agent 的另一组入口(除网关消息平台外):CLI(hermes_cli/,命令行子命令体系)、TUI(ui-tui/,TypeScript 终端 UI + tui_gateway/ Python 桥)、Web(web/,浏览器界面)。三者都最终把用户输入送进 agent 的 run_conversation,把输出渲染回来。它们和网关层是并列关系——网关接外部平台,界面层接本地用户。
关键文件
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— 浏览器界面web/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 调用(压缩、视觉、MoA slot、后台审视 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、工具进度、事件格式是壳的事;会话状态、工具调用、预算扣减、记忆注入是 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,构成完整的「触达面」。