Skip to content

CLI / TUI / Web

源码版本v2026.7.20

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

TUI

Web

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 :

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)

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

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

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, ...)

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

  1. CLI : hermes <subcommand> → parsing via hermes_cli/_parser.py → module de sous-commande (auth/backup/blueprint/…) exécuté ; les commandes de conversation appellent en fin de compte AIAgent.run_conversation:6350.
  2. TUI : ui-tui/ (frontend TS) se connecte par WebSocket à tui_gateway/ws.py, tui_gateway fait le pont entre les événements du terminal et l'agent (tui_gateway/server.py détient l'agent), la sortie est repoussée vers le frontend via tui_gateway/event_publisher.py.
  3. Web : web/ (frontend Vite) relie de même l'agent via un backend, rend la conversation et les résultats d'outils.
  4. 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 print de 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énements gateway.stderr dans le panneau Activity.
  • La TUI ne se reconnecte pas automatiquement après un crash du sous-processus tui_gateway. _CRASH_LOG sert à l'investigation post-mortem, pas à la récupération — l'utilisateur doit relancer manuellement hermes --tui.
  • Le Web n'est pas un substitut de la TUI. web/ tourne via hermes 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 ».

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).