bots-garden/mini-mepublic Fork
⑂ feature/sbx-template-kit
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 0394624 · on feature/sbx-template-kit · k33g · Sep 21, 2026
architecture.md · 66 lines · 10.5 KBmarkdown
Blame HistoryOpen raw

Architecture : une boucle, trois façades — explication

De quoi s'agit-il ?

mm est volontairement petit : une boucle d'agent, une poignée d'outils et un fichier de configuration. Ce qui mérite le regard, c'est la façon dont les pièces sont découpées pour que la même boucle puisse être pilotée depuis un terminal — ligne à ligne ou dans une fenêtre — et depuis un éditeur sans rien dupliquer.

Le point d'entrée, main.go, ne fait que du câblage : il charge les réglages, ouvre le moteur, construit la liste des outils, affiche la bannière et passe la main à l'une des trois façades. Chaque préoccupation vit dans son propre paquet sous internal/ :

Paquet Responsabilité
config Tous les réglages dans une structure, avec des valeurs par défaut intégrées surchargées par le YAML et quelques variables d'environnement.
engine La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, et le raisonnement qu'un modèle diffuse à côté de sa réponse (voir Modèles à raisonnement).
tools Les outils intégrés que le modèle peut appeler.
fileedit L'édition de fichiers par remplacement exact, les règles derrière edit_file.
lsp Le client de serveur de langage derrière l'outil lsp : démarre les serveurs configurés au premier usage (clé lsp.servers), synchronise les fichiers depuis le disque, demande définition, références, hover, symboles, diagnostics et renommage, et met les réponses en forme chemin:ligne:colonne <ligne>. Construit sur go.lsp.dev/protocol et go.lsp.dev/jsonrpc2.
skills La découverte des procédures markdown et le rendu du catalogue de read_skill.
command L'unique table des commandes slash : nom, description, façades qui l'offrent, argument éventuel. Le REPL s'y branche, la façade ACP en annonce la tranche Éditeur, le sélecteur / de la fenêtre la liste.
clipboard Met du texte dans le presse-papiers système : un outil natif du PATH d'abord (pbcopy, wl-copy, xclip, xsel), OSC 52 en repli, et il dit lequel a servi. Utilisé par /copy dans le REPL et la fenêtre.
store Fichiers de session par projet (<dir>/<id>.json, réécriture complète, atomique) : création, sauvegarde, chargement, liste, plus récente. Utilisé par les deux façades quand sessions.enabled.
mention La notation @chemin d'une question : les chemins existants deviennent des lignes [attached file: …], la même forme que le resource_link d'un éditeur. Utilisé par les deux façades.
detector La détection d'une même action répétée avec le même résultat.
compact La compression de l'historique en un résumé écrit par le modèle.
version Quel mm c'est : le tag estampillé à la construction, ou le commit que Go embarque sinon. Lu par --version, par la bannière de démarrage et par la barre d'état de la fenêtre.
spinner L'indicateur « toujours en cours » sur un terminal.
ui Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent.
agent Le REPL du terminal.
acp La façade Agent Client Protocol.
tui La façade fenêtre unique (mm -tui) : un ui.Sink qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils.

Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans docs/diagrams/packages.drawio.

Pourquoi c'est conçu ainsi

Les outils rapportent les échecs comme du texte, jamais comme des erreurs. Une commande qui sort avec un code non nul, un fichier qui n'existe pas, une édition ambiguë : tout revient au modèle comme sortie de l'outil. C'est le modèle qui doit réagir, donc c'est lui qui doit lire le message. Une erreur Go terminerait le tour à sa place.

L'historique complet est conservé même quand un tour échoue. Genkit ne renvoie que le dernier message ; le moteur renvoie toute la conversation, appels d'outils et résultats compris. Les façades gardent cet historique après une interruption, une coupure du watchdog ou un dépassement de maxTurns, parce que les commandes déjà exécutées sont des faits que le modèle ne doit ni rejouer ni inventer à la question suivante. Seule une question restée sans aucune réponse est retirée.

La sortie est montrée avant que le modèle la voie. Dans le terminal, bash affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien.

Une boucle, trois façades. Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes 🛠️ et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc main.go redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un ui.Sink à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements ToolStart, Allow, ToolRunning, ToolEnd au lieu d'afficher. Le paquet ui ne sait rien du protocole ; acp est le seul endroit qui traduit ces événements en notifications session/update et en demandes de permission. Le mode fenêtre rejoue le même tour : tui installe son propre ui.Sink à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de permissions: ask est la version terminal du dialogue de l'éditeur.

Une seule table de commandes. Les commandes slash vivent dans internal/command, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (/new, /compact) via available_commands_update, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois.

La fenêtre dessine depuis une valeur, sur une seule goroutine. Le moteur diffuse des fragments et les outils émettent des événements depuis leurs propres goroutines ; la fenêtre les enregistre dans une Transcript (une valeur protégée par un mutex) et poste un événement de réveil. Seule la boucle d'événements dessine, et elle redessine depuis l'état courant de la transcription à chaque image : un réveil perdu coûte une image en retard, jamais une image perdue. C'est la règle que suit la fenêtre d'agent de turbo-core, et la raison pour laquelle la fenêtre n'a besoin d'aucune bibliothèque de widgets : un peintre, une transcription, une fonction de mise en page et une table de touches suffisent.

La permission vit dans le puits, pas dans les outils. Un dialogue de permission est la principale chose qu'un éditeur ajoute et que le terminal n'a pas. Placer la politique dans le puits ACP garde les outils identiques dans les deux modes ; « toujours autoriser » est mémorisé par nom d'outil pour la session.

Les tours sont sérialisés en mode ACP. Le puits actif est global au processus et le moteur n'a jamais été conçu pour des générations concurrentes, donc un second session/prompt attend la fin du premier. Le protocole laisse ce choix à l'agent, et les éditeurs n'envoient de toute façon pas de prompts qui se chevauchent.

Le détecteur de boucle est séparé de l'historique. Il enregistre des triplets outil, entrée, sortie ; le même triplet trois fois de suite fait ajouter par l'outil une instruction de changer d'approche. Il n'est volontairement pas remis à zéro par /compact : oublier une conversation ne rend pas nouvelle une commande répétée. Le conseil ne suffit pas à tous les modèles : le 18 septembre 2026, un modèle de 12 milliards de paramètres a répété trente fois le même edit_file refusé, [LOOP_DETECTED] ajouté à chaque réponse, jusqu'à ce que l'utilisateur appuie sur Échap. Le détecteur a donc un second niveau : trois répétitions signalées après le seuil — six appels identiques — et l'outil renvoie une erreur Go au lieu de texte, la seule exception délibérée à « les outils répondent en texte » : l'agent s'arrête lui-même, il ne rapporte pas un échec à lire par le modèle. Le moteur affiche la phrase du détecteur comme erreur du tour, ne rejoue pas le tour, et le fichier de session l'enregistre. Un troisième garde-fou est dans le flux lui-même : les arguments d'un appel d'outil arrivent fragment par fragment et rien n'en est affiché, donc un modèle qui déraille en composant un appel — "Greet 2023-06-15 11:16:00 +0000 UTC 2023-06-15 …" pendant quatre minutes, vu le même jour — ressemble à « rien ne se passe ». Le moteur surveille ces fragments et arrête la génération dès qu'ils répètent un motif ou dépassent un quart de mégaoctet.

Alternatives écartées

  • Un binaire par façade. Écarté parce que le but de l'exercice est de prouver que la boucle est la même ; partager le câblage dans main.go est ce qui le rend visible.
  • Afficher directement depuis la façade ACP. Impossible : la spécification interdit tout ce qui n'est pas message de protocole sur stdout.
  • Renvoyer des erreurs Go depuis les outils. Écarté parce que cela termine le tour au lieu de laisser le modèle se rattraper.

Liens avec le reste

Carte des paquets

Le graphe d'import de tous les paquets, avec une ligne de rôle sur chacun, est tenu sous forme de diagramme draw.io dans docs/diagrams/packages.drawio (s'ouvre dans diagrams.net ou l'extension Draw.io de VS Code). Il est regénéré à partir de go list chaque fois qu'un paquet est ajouté, retiré ou recâblé.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# Architecture : une boucle, trois façades — explication

## De quoi s'agit-il ?

`mm` est volontairement petit : une boucle d'agent, une poignée d'outils et un fichier de configuration. Ce qui mérite le regard, c'est la façon dont les pièces sont découpées pour que la même boucle puisse être pilotée depuis un terminal — ligne à ligne ou dans une fenêtre — et depuis un éditeur sans rien dupliquer.

Le point d'entrée, `main.go`, ne fait que du câblage : il charge les réglages, ouvre le moteur, construit la liste des outils, affiche la bannière et passe la main à l'une des trois façades. Chaque préoccupation vit dans son propre paquet sous `internal/` :

| Paquet | Responsabilité |
|--------|----------------|
| `config` | Tous les réglages dans une structure, avec des valeurs par défaut intégrées surchargées par le YAML et quelques variables d'environnement. |
| `engine` | La connexion au serveur de modèle et la génération elle-même : streaming, watchdog, nouvelle tentative sans streaming, requêtes de résumé, le registre des fournisseurs, et le raisonnement qu'un modèle diffuse à côté de sa réponse (voir [Modèles à raisonnement](thinking-models.md)). |
| `tools` | Les outils intégrés que le modèle peut appeler. |
| `fileedit` | L'édition de fichiers par remplacement exact, les règles derrière `edit_file`. |
| `lsp` | Le client de serveur de langage derrière l'outil `lsp` : démarre les serveurs configurés au premier usage (clé `lsp.servers`), synchronise les fichiers depuis le disque, demande définition, références, hover, symboles, diagnostics et renommage, et met les réponses en forme `chemin:ligne:colonne  <ligne>`. Construit sur `go.lsp.dev/protocol` et `go.lsp.dev/jsonrpc2`. |
| `skills` | La découverte des procédures markdown et le rendu du catalogue de `read_skill`. |
| `command` | L'unique table des commandes slash : nom, description, façades qui l'offrent, argument éventuel. Le REPL s'y branche, la façade ACP en annonce la tranche Éditeur, le sélecteur `/` de la fenêtre la liste. |
| `clipboard` | Met du texte dans le presse-papiers système : un outil natif du `PATH` d'abord (`pbcopy`, `wl-copy`, `xclip`, `xsel`), OSC 52 en repli, et il dit lequel a servi. Utilisé par `/copy` dans le REPL et la fenêtre. |
| `store` | Fichiers de session par projet (`<dir>/<id>.json`, réécriture complète, atomique) : création, sauvegarde, chargement, liste, plus récente. Utilisé par les deux façades quand `sessions.enabled`. |
| `mention` | La notation `@chemin` d'une question : les chemins existants deviennent des lignes `[attached file: …]`, la même forme que le `resource_link` d'un éditeur. Utilisé par les deux façades. |
| `detector` | La détection d'une même action répétée avec le même résultat. |
| `compact` | La compression de l'historique en un résumé écrit par le modèle. |
| `version` | Quel `mm` c'est : le tag estampillé à la construction, ou le commit que Go embarque sinon. Lu par `--version`, par la bannière de démarrage et par la barre d'état de la fenêtre. |
| `spinner` | L'indicateur « toujours en cours » sur un terminal. |
| `ui` | Où va la sortie destinée à l'humain, et le puits d'événements que les façades partagent. |
| `agent` | Le REPL du terminal. |
| `acp` | La façade Agent Client Protocol. |
| `tui` | La façade fenêtre unique (`mm -tui`) : un `ui.Sink` qui dessine sur tcell, une valeur de transcription, une boucle d'événements réveillée par les goroutines du moteur et des outils. |

Un diagramme draw.io de ces paquets et de leurs dépendances est conservé dans [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio).

## Pourquoi c'est conçu ainsi

**Les outils rapportent les échecs comme du texte, jamais comme des erreurs.** Une commande qui sort avec un code non nul, un fichier qui n'existe pas, une édition ambiguë : tout revient au modèle comme sortie de l'outil. C'est le modèle qui doit réagir, donc c'est lui qui doit lire le message. Une erreur Go terminerait le tour à sa place.

**L'historique complet est conservé même quand un tour échoue.** Genkit ne renvoie que le dernier message ; le moteur renvoie toute la conversation, appels d'outils et résultats compris. Les façades gardent cet historique après une interruption, une coupure du watchdog ou un dépassement de `maxTurns`, parce que les commandes déjà exécutées sont des faits que le modèle ne doit ni rejouer ni inventer à la question suivante. Seule une question restée sans aucune réponse est retirée.

**La sortie est montrée avant que le modèle la voie.** Dans le terminal, `bash` affiche les premières lignes de la sortie d'une commande avant de la renvoyer. « Montre-moi ce fichier » veut dire le montrer ; laisser le modèle le résumer a été observé laissant l'utilisateur sans rien.

**Une boucle, trois façades.** Le REPL du terminal, la fenêtre et la façade ACP reçoivent exactement le même moteur, le même prompt système et la même liste d'outils. Ce qui diffère, c'est la destination des événements. Dans le terminal, les outils affichent eux-mêmes leurs lignes `🛠️` et personne ne demande de permission. En mode ACP, stdout appartient à JSON-RPC, donc `main.go` redirige la sortie humaine vers stderr, désactive le spinner, et la façade installe un `ui.Sink` à chaque tour. Les outils vérifient s'il y a un puits actif : s'il y en a un, ils émettent des événements `ToolStart`, `Allow`, `ToolRunning`, `ToolEnd` au lieu d'afficher. Le paquet `ui` ne sait rien du protocole ; `acp` est le seul endroit qui traduit ces événements en notifications `session/update` et en demandes de permission. Le mode fenêtre rejoue le même tour : `tui` installe son propre `ui.Sink` à chaque tour, et les événements deviennent des entrées d'une transcription dessinée à l'écran — les outils n'ont donc rien eu à changer pour gagner une troisième maison, et l'invite de permission en ligne de `permissions: ask` est la version terminal du dialogue de l'éditeur.

**Une seule table de commandes.** Les commandes slash vivent dans `internal/command`, marquées des façades qui les offrent. Le REPL et la fenêtre lisent la même table ; la façade ACP n'annonce que la tranche Éditeur (`/new`, `/compact`) via `available_commands_update`, parce que l'éditeur possède le processus et annule lui-même les tours. Une commande ajoutée à un endroit est donc offerte — ou délibérément non — partout à la fois.

**La fenêtre dessine depuis une valeur, sur une seule goroutine.** Le moteur diffuse des fragments et les outils émettent des événements depuis leurs propres goroutines ; la fenêtre les enregistre dans une `Transcript` (une valeur protégée par un mutex) et poste un événement de réveil. Seule la boucle d'événements dessine, et elle redessine depuis l'état courant de la transcription à chaque image : un réveil perdu coûte une image en retard, jamais une image perdue. C'est la règle que suit la fenêtre d'agent de turbo-core, et la raison pour laquelle la fenêtre n'a besoin d'aucune bibliothèque de widgets : un peintre, une transcription, une fonction de mise en page et une table de touches suffisent.

**La permission vit dans le puits, pas dans les outils.** Un dialogue de permission est la principale chose qu'un éditeur ajoute et que le terminal n'a pas. Placer la politique dans le puits ACP garde les outils identiques dans les deux modes ; « toujours autoriser » est mémorisé par nom d'outil pour la session.

**Les tours sont sérialisés en mode ACP.** Le puits actif est global au processus et le moteur n'a jamais été conçu pour des générations concurrentes, donc un second `session/prompt` attend la fin du premier. Le protocole laisse ce choix à l'agent, et les éditeurs n'envoient de toute façon pas de prompts qui se chevauchent.

**Le détecteur de boucle est séparé de l'historique.** Il enregistre des triplets outil, entrée, sortie ; le même triplet trois fois de suite fait ajouter par l'outil une instruction de changer d'approche. Il n'est volontairement pas remis à zéro par `/compact` : oublier une conversation ne rend pas nouvelle une commande répétée. Le conseil ne suffit pas à tous les modèles : le 18 septembre 2026, un modèle de 12 milliards de paramètres a répété trente fois le même `edit_file` refusé, `[LOOP_DETECTED]` ajouté à chaque réponse, jusqu'à ce que l'utilisateur appuie sur Échap. Le détecteur a donc un second niveau : trois répétitions signalées après le seuil — six appels identiques — et l'outil renvoie une erreur Go au lieu de texte, la seule exception délibérée à « les outils répondent en texte » : l'agent s'arrête lui-même, il ne rapporte pas un échec à lire par le modèle. Le moteur affiche la phrase du détecteur comme erreur du tour, ne rejoue pas le tour, et le fichier de session l'enregistre. Un troisième garde-fou est dans le flux lui-même : les arguments d'un appel d'outil arrivent fragment par fragment et rien n'en est affiché, donc un modèle qui déraille en composant un appel — `"Greet 2023-06-15 11:16:00 +0000 UTC 2023-06-15 …"` pendant quatre minutes, vu le même jour — ressemble à « rien ne se passe ». Le moteur surveille ces fragments et arrête la génération dès qu'ils répètent un motif ou dépassent un quart de mégaoctet.

## Alternatives écartées

- **Un binaire par façade.** Écarté parce que le but de l'exercice est de prouver que la boucle est la même ; partager le câblage dans `main.go` est ce qui le rend visible.
- **Afficher directement depuis la façade ACP.** Impossible : la spécification interdit tout ce qui n'est pas message de protocole sur stdout.
- **Renvoyer des erreurs Go depuis les outils.** Écarté parce que cela termine le tour au lieu de laisser le modèle se rattraper.

## Liens avec le reste

- Le registre de fournisseurs utilisé par le moteur est décrit dans [fournisseurs](providers.md).
- Pourquoi et comment l'historique est raccourci est dans [compression du contexte](context-compression.md).
- La surface exacte du protocole est dans la [référence ACP](../reference/acp.md) ; le contrat des outils est dans la [référence des outils](../reference/tools.md).

## Carte des paquets

Le graphe d'import de tous les paquets, avec une ligne de rôle sur chacun, est tenu sous forme de diagramme draw.io dans [`docs/diagrams/packages.drawio`](../../diagrams/packages.drawio) (s'ouvre dans diagrams.net ou l'extension Draw.io de VS Code). Il est regénéré à partir de `go list` chaque fois qu'un paquet est ajouté, retiré ou recâblé.