CLI / TUI / Web
Responsabilité
La couche interfaces est un autre groupe d'entrées vers l'agent (en plus des plateformes de messagerie de la passerelle (gateway)) : CLI (hermes_cli/, système de sous-commandes en ligne de commande), TUI (ui-tui/, terminal UI en TypeScript + pont Python tui_gateway/), Web (web/, interface navigateur). Les trois envoient l'entrée utilisateur dans le run_conversation de l'agent et restituent la sortie. Ils sont en parallèle de la couche passerelle — la passerelle branche les plateformes externes, la couche interfaces branche l'utilisateur local.
Fichiers clés
CLI
hermes_cli/__init__.py— entrée du package_parser.py— parsing des arguments CLIauth.py/auth_commands.py— authentification (Nous Portal, etc.)active_sessions.py— gestion des sessions activesbackup.py/checkpoints.py— sauvegarde et points de contrôleblueprint_cmd.py— commande blueprintbrowser_connect.py/bundles.py/callbacks.pybanner.py/build_info.py— bannière et infos de build
TUI
ui-tui/README— terminal UI (TypeScript, contient packages/src)ui-tui/package.json— dépendances et scriptstui_gateway/entry.py— entrée de la passerelle TUIserver.py— serveur TUItransport.py/ws.py— transport et WebSocketevent_publisher.py— publication d'événementshost_supervisor.py/compute_host.py— supervision de l'hôteslash_worker.py/synthetic_turn.py/render.py/project_tree.py_stdin_recovery.py— récupération de stdin
Web
web/README— interface navigateurweb/index.html/vite.config.ts— entrée Viteweb/src/— source frontend
Surface des sous-commandes CLI
hermes_cli/_parser.py extrait le argparse de plus haut niveau séparément, pour que relaunch.py et d'autres modules puissent introspecter les flags sans lancer main. Le parser ne gère que la sous-commande chat et les options globales ; les autres sous-commandes sont construites en place dans main.py (fortement couplées aux fonctions cmd_*). La chaîne d'exemples dans _EPILOGUE est la vue d'ensemble des sous-commandes que l'utilisateur voit :
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)Le CLI est à la fois « entrée de conversation » et « panneau ops » — auth / model / config / gateway / dashboard / backup sont tous accrochés au même arbre de sous-commandes. Seul chat et ses dérivés (-c / --resume) entrent dans run_conversation ; les autres passent directement par leur module respectif.
Boucle principale stdin / JSON-RPC de la passerelle TUI
La TUI place le rendu terminal côté TypeScript (ui-tui/), le côté Python ne fait tourner qu'un pont léger tui_gateway. Les deux communiquent en JSON-RPC sur stdin/stdout. Le cœur de main() dans tui_gateway/entry.py est une boucle 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 décide de continuer ou non.
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)Quelques détails d'ingénierie à noter : handle_spurious_eof traite les « faux EOF » causés par le retournement de O_NONBLOCK par le sous-processus ; _log_exit laisse un crash log quand l'écriture sur stdout échoue ; SIGPIPE est explicitement ignoré (signal.signal(signal.SIGPIPE, signal.SIG_IGN)), pour qu'un thread en arrière-plan (TTS/beep) qui écrit dans un pipe fermé ne fasse pas planter tout le processus.
Les trois finissent tous par converger vers run_conversation
Peu importe l'entrée, les requêtes de conversation finissent toutes par appeler AIAgent.run_conversation — qui n'est lui-même qu'un forwarder, la véritable boucle principale étant dans 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, ...)L'ID de session (session) est propagé via un ContextVar — tous les appels LLM à l'intérieur de la boucle principale (compression (compression), vision, slot MoA, fork d'audit en arrière-plan) portent automatiquement le même tag, sans avoir à le passer explicitement à chaque site d'appel. L'instance agent est assemblée par init_agent (agent/agent_init.py:276), et la longue liste de paramètres de callback dans la signature (step_callback / stream_delta_callback / event_callback / ...) sont les crochets des trois coquilles — un même agent, des coquilles différentes qui attachent des callbacks différents, et le rendu est totalement différent.
Flux de données
- CLI :
hermes <subcommand>→ parsing viahermes_cli/_parser.py→ module de sous-commande (auth/backup/blueprint/…) exécuté ; les commandes de conversation appellent en fin de compteAIAgent.run_conversation:6350. - TUI :
ui-tui/(frontend TS) se connecte par WebSocket àtui_gateway/ws.py,tui_gatewayfait le pont entre les événements du terminal et l'agent (tui_gateway/server.pydétient l'agent), la sortie est repoussée vers le frontend viatui_gateway/event_publisher.py. - Web :
web/(frontend Vite) relie de même l'agent via un backend, rend la conversation et les résultats d'outils. - Les instances d'agent des trois sont assemblées via
init_agent:276, et partagent la même couche capacités.
Mot de conception
Pourquoi trois coquilles partagent-elles un seul run_conversation plutôt que de chacune faire tourner sa propre boucle ? Au cœur : « différence de rendu vs cohérence métier ». Tokens en streaming, progression d'outils (tools), format d'événements — ça, c'est l'affaire de la coquille ; état de conversation, appels d'outils, décompte de budget (budget), injection de mémoire — ça, c'est l'affaire de l'agent. Isoler la coquille au niveau des callbacks permet de changer de coquille sans toucher au métier, et d'ajouter du métier sans le répliquer à trois endroits. tui_gateway existe séparément parce que le côté TS ne peut pas détenir directement un agent Python — c'est un pont de langage, pas une couche métier.
Limites et échecs
- stdin/stdout de la TUI est un pipe JSON-RPC, pas un log. Un
printde debug qui écrit n'importe comment sur stdout fait planter le parsing côté TUI ; les logs côté Python vont sur stderr, et la TUI les expose comme événementsgateway.stderrdans le panneau Activity. - La TUI ne se reconnecte pas automatiquement après un crash du sous-processus
tui_gateway._CRASH_LOGsert à l'investigation post-mortem, pas à la récupération — l'utilisateur doit relancer manuellementhermes --tui. - Le Web n'est pas un substitut de la TUI.
web/tourne viahermes dashboard(port 9119), positionné comme console locale + consultation des longues conversations, pas comme produit web d'accès multi-utilisateur distant.
Résumé
La couche interfaces = trois coquilles (CLI/TUI/Web) + un pont tui_gateway. Elle ne porte pas de logique métier, seulement la forme d'entrée/sortie. En parallèle de la couche passerelle : la passerelle branche les plateformes de messagerie externes, la couche interfaces branche le terminal / navigateur local. ACP (voir ici) branche l'IDE, ce qui complète la « surface de contact ».