CLI / TUI / Web
Responsibility
The interface layer is the other group of entry points (besides the gateway's messaging platforms) through which users reach the agent: CLI (hermes_cli/, a command-line subcommand system), TUI (ui-tui/, a TypeScript terminal UI + the tui_gateway/ Python bridge), Web (web/, a browser interface). All three ultimately feed user input into the agent's run_conversation and render the output back. They sit alongside the gateway layer — the gateway connects to external platforms; the interface layer connects to local users.
Key Files
CLI
hermes_cli/__init__.py— package entry_parser.py— command-line argument parsingauth.py/auth_commands.py— authentication (Nous Portal, etc.)active_sessions.py— active-session managementbackup.py/checkpoints.py— backups and checkpointsblueprint_cmd.py— blueprint commandbrowser_connect.py/bundles.py/callbacks.pybanner.py/build_info.py— banner and build info
TUI
ui-tui/README— terminal UI (TypeScript, includes packages/src)ui-tui/package.json— dependencies and scriptstui_gateway/entry.py— TUI gateway entryserver.py— TUI servertransport.py/ws.py— transport and WebSocketevent_publisher.py— event publishinghost_supervisor.py/compute_host.py— host supervisionslash_worker.py/synthetic_turn.py/render.py/project_tree.py_stdin_recovery.py— stdin recovery
Web
web/README— browser interfaceweb/index.html/vite.config.ts— Vite entryweb/src/— frontend source
CLI Subcommand Surface
hermes_cli/_parser.py factors out the top-level argparse so that modules like relaunch.py can introspect available flags without running main. The parser only owns the chat subcommand and global options; other subcommands are constructed in place in main.py (tightly coupled to the cmd_* functions). The examples in _EPILOGUE are the full subcommand landscape the user sees:
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 is both a "conversation entry" and an "ops panel" — auth/model/config/gateway/dashboard/backup all hang off the same subcommand tree. Only chat and its derivatives (-c/--resume) actually enter run_conversation; the rest go straight to their own modules.
TUI Gateway's stdin/JSON-RPC Main Loop
The TUI puts terminal rendering on the TypeScript side (ui-tui/); the Python side runs only a thin tui_gateway bridge. The two communicate over stdin/stdout via JSON-RPC. The main() in tui_gateway/entry.py is a readline + dispatch loop at its core (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 decides whether to continue.
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)A few engineering details matter: handle_spurious_eof deals with "fake EOF" caused by a child process flipping O_NONBLOCK; _log_exit leaves a crash log behind when writes to stdout fail; SIGPIPE is explicitly ignored (signal.signal(signal.SIGPIPE, signal.SIG_IGN)), so that background TTS/beep threads writing to a closed pipe don't drag the whole process down.
All Three Converge on run_conversation
No matter which entry you come in from, conversation-type requests end up calling AIAgent.run_conversation — which itself is just a forwarder; the real main loop lives in 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, ...)The conversation ID is pushed down via a ContextVar — every LLM call inside the main loop (compression, vision, MoA slot, background-review fork) picks up the same tag automatically, no need to pass it explicitly at each call site. The agent instance is assembled by init_agent (agent/agent_init.py:276); the long string of callback parameters in its signature (step_callback/stream_delta_callback/event_callback/...) are the hooks for the three shells — same agent, different shells attach different callbacks, and the render form is completely different.
Data Flow
- CLI:
hermes <subcommand>→ parsed byhermes_cli/_parser.py→ executed by the subcommand module (auth/backup/blueprint/...); conversation-type commands ultimately callAIAgent.run_conversation:6350. - TUI:
ui-tui/(TS frontend) connects totui_gateway/ws.pyvia WebSocket;tui_gatewaybridges terminal events to the agent (tui_gateway/server.pyholds the agent); output is pushed back to the frontend viatui_gateway/event_publisher.py. - Web:
web/(Vite frontend) similarly connects to the agent through a backend, rendering the conversation and tool results. - All three assemble their agent instance via
init_agent:276and share the same capability layer.
Design Motivation
Why do three shells share one run_conversation instead of each running its own loop? The core is "rendering differs vs business is consistent". Streaming tokens, tool progress, event format — those are the shell's business; conversation state, tool calls, budget decrement, memory injection — those are the agent's business. Keeping shells isolated at the callback layer means swapping a shell doesn't require touching business, and adding business doesn't require changing all three places. tui_gateway exists on its own because the TS side can't directly hold a Python agent — it's a language bridge, not a business layer.
Boundaries and Failure
- The TUI's stdin/stdout is a JSON-RPC pipe, not a log. Stray
print-debugging into stdout will break the TUI's parser; Python-side logs go to stderr, which the TUI surfaces asgateway.stderrevents in the Activity panel. - A crashed
tui_gatewaysubprocess doesn't auto-reconnect._CRASH_LOGis for post-mortem forensics, not recovery — the user has to manually restarthermes --tui. - Web is not a replacement for TUI.
web/runs throughhermes dashboard(port 9119); its scope is a local console + long-conversation viewer, not a remote multi-user web product.
Summary
The interface layer = three shells (CLI/TUI/Web) + one tui_gateway bridge. They hold no business logic, only the input/output form. They sit alongside the gateway layer: the gateway connects to external messaging platforms; the interface layer connects to local terminals/browsers. ACP (see here) connects to the IDE, completing the full "reach surface".