bots-garden/mini-mepublic Fork
⑂ feature/improve-init
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. fd5d2a7 · on feature/improve-init · k33g · yesterday
declared-tools.md · 66 lines · 10.0 KBmarkdown
Blame HistoryOpen raw

Les outils déclarés — explication

De quoi s'agit-il ?

Avant le ticket 88, mm offrait deux façons de laisser le modèle lancer l'une de vos commandes : un skill qui dit « appelle ton outil bash avec cette commande », et un serveur MCP qui expose un outil. Les outils déclarés en sont une troisième : une entrée tools: dans le fichier d'agent, qui transforme une commande en outil typé, sans serveur à écrire. Cette page explique pourquoi ils ont été ajoutés à côté des skills plutôt qu'à leur place, et pourquoi ils ont cette forme.

Pourquoi les skills ne suffisaient pas

Un skill lance une commande en deux temps. Le modèle doit d'abord trouver le skill dans le catalogue et appeler read_skill, puis composer une ligne de commande bash à partir de ce qu'il a lu. Chaque étape peut échouer, et sur un petit modèle, elle échoue :

  • Le skill n'est jamais trouvé. Le catalogue est aiguillé par le modèle, qui lit des noms, des descriptions et des mots-clés. Mesuré sur mellum2-12b, un skill status (« Report the status of one of the company's services ») n'a jamais été lu pour « What is the status of the billing service? » — 0 run sur 10, en anglais comme en français. Le modèle a improvisé curl localhost:8080/health et ps aux | grep billing, puis a répondu que le service « semble tourner ». C'est une mauvaise réponse, pas seulement un raté.
  • La commande est réécrite. Quand le skill est sauté mais que le modèle trouve quand même le script, il devine les arguments : ./pkgtest.sh greet au lieu de ./greet.
  • Le consentement est tout ou rien. Tout ce qu'un skill lance passe par bash, donc « toujours autoriser ce script » ne peut se dire que « toujours autoriser bash ».

Un outil déclaré supprime l'étape d'aiguillage : son nom, sa description et ses paramètres figurent dans chaque requête, comme ceux de bash.

Ce qui a été mesuré

Trois actions (release <version> <channel>, pkgtest <package>, status <service>), chacune demandée en anglais et en français, 5 runs chacune, mm -p, sur deux modèles. Un run réussit quand le script a vraiment tourné avec exactement les arguments attendus — vérifié dans un fichier que le script écrit, pas dans la réponse du modèle.

Modèle Skill + bash Outil typé via une doublure MCP Outil déclaré
mellum2-12b 15/30 (4,5 s en moyenne) 25/30 (2,8 s) 30/30 (1,3 s)
qwen3.5-4b 30/30 (12,7 s) 30/30 (7,7 s) 30/30 (5,8 s)

La doublure MCP a été mesurée en premier, avant d'écrire la moindre ligne : un petit serveur MCP HTTP exposant les trois mêmes outils donnait la même comparaison sans rien construire. C'est elle qui a tranché la question. Les outils déclarés ont été mesurés à nouveau une fois construits.

Deux choses ressortent. D'abord, un outil typé est plus rapide même quand le skill fonctionne : un aller-retour avec le modèle au lieu de deux. Ensuite, les cinq échecs de la doublure sur mellum2 — la demande de release en français — venaient du modèle qui tassait tous les arguments dans un seul champ : {version: "1.4.2 1.4.2, channel beta"}. Avec l'outil déclaré, la même demande a réussi 5 fois sur 5, chaque fois en un seul appel. L'outil déclaré diffère de la doublure sur plusieurs points à la fois — pas de préfixe proj_ dans le nom, pas de « MCP tool from server … » dans la description, une phrase finale « Give each parameter as its own field. » — et l'effet de chacun n'a pas été mesuré séparément.

Ces chiffres viennent d'un seul petit projet de test, de deux modèles et de cinq runs par cas. Ils répondent à « est-ce que ça vaut la peine de le construire ? », pas à « de combien est-ce mieux en général ? ».

Pourquoi c'est conçu ainsi

Pas de shell. La commande est une liste, une entrée argv par élément, et une valeur remplace son {{placeholder}} à l'intérieur d'une seule entrée. Une valeur du modèle — ou une valeur qu'une injection de prompt aurait glissée dans la conversation — ne peut ni ajouter un argument, ni enchaîner une seconde commande, ni rediriger une sortie : 1.4.2; rm -rf ~ arrive au script comme une seule chaîne de version, étrange. Un modèle de type command: "./release.sh {{version}}" exécuté par bash -c a été écarté pour exactement cette raison. Une commande qui a besoin d'un pipe va dans un script.

Le programme ne peut pas venir d'un paramètre. Sinon, un outil déclaré n'est qu'un bash avec des étapes en plus.

Les appels invalides reçoivent une réponse, pas un rejet par le schéma. Le schéma que lit le modèle porte les types, mais pas required ni enum comme mots-clés — ceux-là vont dans les descriptions. Genkit valide un appel contre le schéma déclaré avant de lancer l'outil, et un appel qu'il rejette met fin au tour. mm préfère répondre en texte, en disant ce qui ne va pas, ce que le modèle a envoyé et ce que doit être chaque paramètre, pour que le modèle puisse réessayer. Le même choix a été fait pour les outils MCP. Citer l'appel compte : informé seulement que « channel is missing », mellum2 a renvoyé le même appel tassé six fois, jusqu'à ce que le détecteur de boucle arrête le tour.

Une conversion tolérante, rien de plus. Les modèles écrivent "3" pour 3 et "true" pour true assez souvent pour que ces chaînes soient acceptées. Ce qui demanderait de deviner ("yes", "three") est refusé, avec la raison.

Le consentement suit yolo. Un outil déclaré exécute quelque chose : avec yolo: false, il demande comme bash ; avec yolo: true, il ne demande pas. Demander « même en yolo » a été envisagé puis écarté : c'est l'auteur de l'agent qui a écrit la commande, ce qui engage déjà plus qu'un modèle qui compose une ligne bash.

Les noms sont réservés tôt. Un outil déclaré ne peut pas prendre le nom d'un outil intégré, même désactivé, pour que l'activer plus tard ne casse pas le fichier. Un conflit avec un outil MCP ne se voit qu'une fois le serveur interrogé : c'est donc un avertissement au démarrage, pas une erreur.

Outils wasm

Un outil commande s'exécute avec tous les droits de l'utilisateur : l'auteur de l'agent a écrit la commande, mais quoi qu'elle fasse, elle peut le faire partout. Un outil qui doit seulement lire un dossier, ou calculer quelque chose à partir de ses arguments, n'en a pas besoin. Une entrée wasm: (ticket 119) exécute à la place un module WebAssembly dans mm, avec wazero, et le module part de rien : aucun fichier, aucune variable d'environnement, aucun réseau. L'entrée lui donne ce dont il a besoin, dossier par dossier — read: ou write: — et cette liste est tout son monde. Une valeur du modèle, même plantée par une injection de prompt, ne peut pas aller au-delà : read_note appelé avec ../agent.yaml dans la démo ne trouve aucun fichier de ce nom, parce que rien au-dessus de notes/ n'est là pour être trouvé.

Pourquoi une commande WASI, avec du JSON sur stdin. Le module est un programme ordinaire avec un main — ce que TinyGo construit avec -target=wasip1 — donc en écrire un ne demande aucun SDK ni aucune fonction exportée à apprendre. Les arguments arrivent en un seul objet JSON, déjà vérifié et typé par mm, la forme qu'utilisent déjà les hooks ; un gabarit argv comme command: était l'autre choix, écarté parce qu'il remet chaque valeur en texte que le module doit analyser à nouveau. Ce contrat est celui sur lequel compte l'auteur d'un module : il est fait pour durer.

Pourquoi wazero. Il est écrit en Go pur : mm reste un seul binaire statique, sans CGO, sur toutes les cibles de release, sans rien à installer à côté. Le prix est la taille : le binaire mm est passé de 38,97 Mo à 42,87 Mo (linux/arm64).

Ce que ça coûte à l'exécution, mesuré dans le sandbox où il a été construit (linux/arm64) : compiler un module TinyGo d'environ 550 Ko a pris 176 à 183 ms, une fois par processus mm, à son premier appel ; chaque appel suivant, une instance neuve, a pris 0,2 à 0,5 ms. À titre de comparaison, le ticket 46 a mesuré un hook Node.js à environ 62 ms par appel. Une instance neuve par appel signifie qu'aucun état ne survit entre deux appels, et que des appels peuvent se chevaucher.

Ce qu'il ne fait pas. WASI preview 1 n'a pas de réseau, donc un outil qui appelle une API reste une commande (ou un outil MCP). Le consentement est le même que pour une commande (yolo) : le bac à sable limite ce qu'un module peut toucher, mais la décision de l'exécuter reste celle de l'utilisateur. Seuls des modules TinyGo ont été essayés.

Alternatives écartées

  • Seulement des skills. Moins chers à écrire et adaptés aux procédures, mais mesurés en échec là où ça compte, sur les petits modèles que vise mm.
  • Seulement MCP. Donne déjà des outils typés, mais chacun demande un serveur à écrire, à lancer et à maintenir, en HTTP seulement (mm n'a pas de transport MCP stdio). Trop lourd pour « lancer ce script avec deux arguments ».
  • Un système général de plugins Wasm (ticket 46) — hooks, fournisseurs, n'importe quoi en plugin. Seuls les outils déclarés sont devenus Wasm (ticket 119, ci-dessus) : une entrée, un module, un contrat documenté. Les hooks en modules Wasm y restent une idée ouverte.

Liens avec le reste

Les outils déclarés se rangent à côté des outils intégrés : le même détecteur de boucle, la même troncature et le même écho de la sortie, le même répertoire de travail et la même annulation que bash, les mêmes dialogues de permission (voir yolo et permissions), et les hooks preToolCall / postToolCall. Les skills restent l'endroit du savoir-faire et des procédures ; un skill peut aussi dire au modèle d'appeler un outil déclaré.

 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
# Les outils déclarés — explication

## De quoi s'agit-il ?

Avant le ticket 88, mm offrait deux façons de laisser le modèle lancer l'une de vos commandes : un [skill](../how-to/add-a-skill.md) qui dit « appelle ton outil `bash` avec cette commande », et un serveur MCP qui expose un outil. Les outils déclarés en sont une troisième : une entrée `tools:` dans le fichier d'agent, qui transforme une commande en outil typé, sans serveur à écrire. Cette page explique pourquoi ils ont été ajoutés à côté des skills plutôt qu'à leur place, et pourquoi ils ont cette forme.

## Pourquoi les skills ne suffisaient pas

Un skill lance une commande en deux temps. Le modèle doit d'abord trouver le skill dans le catalogue et appeler `read_skill`, puis composer une ligne de commande `bash` à partir de ce qu'il a lu. Chaque étape peut échouer, et sur un petit modèle, elle échoue :

- **Le skill n'est jamais trouvé.** Le catalogue est aiguillé par le modèle, qui lit des noms, des descriptions et des mots-clés. Mesuré sur mellum2-12b, un skill `status` (« Report the status of one of the company's services ») n'a jamais été lu pour « What is the status of the billing service? » — 0 run sur 10, en anglais comme en français. Le modèle a improvisé `curl localhost:8080/health` et `ps aux | grep billing`, puis a répondu que le service « semble tourner ». C'est une mauvaise réponse, pas seulement un raté.
- **La commande est réécrite.** Quand le skill est sauté mais que le modèle trouve quand même le script, il devine les arguments : `./pkgtest.sh greet` au lieu de `./greet`.
- **Le consentement est tout ou rien.** Tout ce qu'un skill lance passe par `bash`, donc « toujours autoriser ce script » ne peut se dire que « toujours autoriser bash ».

Un outil déclaré supprime l'étape d'aiguillage : son nom, sa description et ses paramètres figurent dans chaque requête, comme ceux de `bash`.

## Ce qui a été mesuré

Trois actions (`release <version> <channel>`, `pkgtest <package>`, `status <service>`), chacune demandée en anglais et en français, 5 runs chacune, `mm -p`, sur deux modèles. Un run réussit quand le script a vraiment tourné avec exactement les arguments attendus — vérifié dans un fichier que le script écrit, pas dans la réponse du modèle.

| Modèle | Skill + `bash` | Outil typé via une doublure MCP | Outil déclaré |
|--------|----------------|----------------------------------|---------------|
| mellum2-12b | 15/30 (4,5 s en moyenne) | 25/30 (2,8 s) | 30/30 (1,3 s) |
| qwen3.5-4b | 30/30 (12,7 s) | 30/30 (7,7 s) | 30/30 (5,8 s) |

La doublure MCP a été mesurée en premier, avant d'écrire la moindre ligne : un petit serveur MCP HTTP exposant les trois mêmes outils donnait la même comparaison sans rien construire. C'est elle qui a tranché la question. Les outils déclarés ont été mesurés à nouveau une fois construits.

Deux choses ressortent. D'abord, un outil typé est plus rapide même quand le skill fonctionne : un aller-retour avec le modèle au lieu de deux. Ensuite, les cinq échecs de la doublure sur mellum2 — la demande de release en français — venaient du modèle qui tassait tous les arguments dans un seul champ : `{version: "1.4.2 1.4.2, channel beta"}`. Avec l'outil déclaré, la même demande a réussi 5 fois sur 5, chaque fois en un seul appel. L'outil déclaré diffère de la doublure sur plusieurs points à la fois — pas de préfixe `proj_` dans le nom, pas de « MCP tool from server … » dans la description, une phrase finale « Give each parameter as its own field. » — et l'effet de chacun n'a pas été mesuré séparément.

Ces chiffres viennent d'un seul petit projet de test, de deux modèles et de cinq runs par cas. Ils répondent à « est-ce que ça vaut la peine de le construire ? », pas à « de combien est-ce mieux en général ? ».

## Pourquoi c'est conçu ainsi

**Pas de shell.** La commande est une liste, une entrée argv par élément, et une valeur remplace son `{{placeholder}}` à l'intérieur d'une seule entrée. Une valeur du modèle — ou une valeur qu'une injection de prompt aurait glissée dans la conversation — ne peut ni ajouter un argument, ni enchaîner une seconde commande, ni rediriger une sortie : `1.4.2; rm -rf ~` arrive au script comme une seule chaîne de version, étrange. Un modèle de type `command: "./release.sh {{version}}"` exécuté par `bash -c` a été écarté pour exactement cette raison. Une commande qui a besoin d'un pipe va dans un script.

**Le programme ne peut pas venir d'un paramètre.** Sinon, un outil déclaré n'est qu'un `bash` avec des étapes en plus.

**Les appels invalides reçoivent une réponse, pas un rejet par le schéma.** Le schéma que lit le modèle porte les types, mais pas `required` ni `enum` comme mots-clés — ceux-là vont dans les descriptions. Genkit valide un appel contre le schéma déclaré avant de lancer l'outil, et un appel qu'il rejette met fin au tour. mm préfère répondre en texte, en disant ce qui ne va pas, ce que le modèle a envoyé et ce que doit être chaque paramètre, pour que le modèle puisse réessayer. Le même choix a été fait pour les outils MCP. Citer l'appel compte : informé seulement que « channel is missing », mellum2 a renvoyé le même appel tassé six fois, jusqu'à ce que le [détecteur de boucle](../reference/tools.md#détection-de-boucle) arrête le tour.

**Une conversion tolérante, rien de plus.** Les modèles écrivent `"3"` pour 3 et `"true"` pour true assez souvent pour que ces chaînes soient acceptées. Ce qui demanderait de deviner (`"yes"`, `"three"`) est refusé, avec la raison.

**Le consentement suit `yolo`.** Un outil déclaré exécute quelque chose : avec `yolo: false`, il demande comme `bash` ; avec `yolo: true`, il ne demande pas. Demander « même en yolo » a été envisagé puis écarté : c'est l'auteur de l'agent qui a écrit la commande, ce qui engage déjà plus qu'un modèle qui compose une ligne `bash`.

**Les noms sont réservés tôt.** Un outil déclaré ne peut pas prendre le nom d'un outil intégré, même désactivé, pour que l'activer plus tard ne casse pas le fichier. Un conflit avec un outil MCP ne se voit qu'une fois le serveur interrogé : c'est donc un avertissement au démarrage, pas une erreur.

## Outils wasm

Un outil commande s'exécute avec tous les droits de l'utilisateur : l'auteur de l'agent a écrit la commande, mais quoi qu'elle fasse, elle peut le faire partout. Un outil qui doit seulement lire un dossier, ou calculer quelque chose à partir de ses arguments, n'en a pas besoin. Une entrée `wasm:` (ticket 119) exécute à la place un module WebAssembly dans mm, avec [wazero](https://wazero.io), et le module part de **rien** : aucun fichier, aucune variable d'environnement, aucun réseau. L'entrée lui donne ce dont il a besoin, dossier par dossier — `read:` ou `write:` — et cette liste est tout son monde. Une valeur du modèle, même plantée par une injection de prompt, ne peut pas aller au-delà : `read_note` appelé avec `../agent.yaml` dans la démo ne trouve aucun fichier de ce nom, parce que rien au-dessus de `notes/` n'est là pour être trouvé.

**Pourquoi une commande WASI, avec du JSON sur stdin.** Le module est un programme ordinaire avec un `main` — ce que TinyGo construit avec `-target=wasip1` — donc en écrire un ne demande aucun SDK ni aucune fonction exportée à apprendre. Les arguments arrivent en un seul objet JSON, déjà vérifié et typé par mm, la forme qu'utilisent déjà les hooks ; un gabarit argv comme `command:` était l'autre choix, écarté parce qu'il remet chaque valeur en texte que le module doit analyser à nouveau. Ce contrat est celui sur lequel compte l'auteur d'un module : il est fait pour durer.

**Pourquoi wazero.** Il est écrit en Go pur : mm reste un seul binaire statique, sans CGO, sur toutes les cibles de release, sans rien à installer à côté. Le prix est la taille : le binaire mm est passé de 38,97 Mo à 42,87 Mo (linux/arm64).

**Ce que ça coûte à l'exécution**, mesuré dans le sandbox où il a été construit (linux/arm64) : compiler un module TinyGo d'environ 550 Ko a pris 176 à 183 ms, une fois par processus mm, à son premier appel ; chaque appel suivant, une instance neuve, a pris 0,2 à 0,5 ms. À titre de comparaison, le ticket 46 a mesuré un hook Node.js à environ 62 ms par appel. Une instance neuve par appel signifie qu'aucun état ne survit entre deux appels, et que des appels peuvent se chevaucher.

**Ce qu'il ne fait pas.** WASI preview 1 n'a pas de réseau, donc un outil qui appelle une API reste une commande (ou un outil MCP). Le consentement est le même que pour une commande (`yolo`) : le bac à sable limite ce qu'un module peut toucher, mais la décision de l'exécuter reste celle de l'utilisateur. Seuls des modules TinyGo ont été essayés.

## Alternatives écartées

- **Seulement des skills.** Moins chers à écrire et adaptés aux procédures, mais mesurés en échec là où ça compte, sur les petits modèles que vise mm.
- **Seulement MCP.** Donne déjà des outils typés, mais chacun demande un serveur à écrire, à lancer et à maintenir, en HTTP seulement (mm n'a pas de transport MCP stdio). Trop lourd pour « lancer ce script avec deux arguments ».
- **Un système général de plugins Wasm** (ticket 46) — hooks, fournisseurs, n'importe quoi en plugin. Seuls les outils déclarés sont devenus Wasm (ticket 119, ci-dessus) : une entrée, un module, un contrat documenté. Les hooks en modules Wasm y restent une idée ouverte.

## Liens avec le reste

Les outils déclarés se rangent à côté des outils intégrés : le même détecteur de boucle, la même troncature et le même écho de la sortie, le même répertoire de travail et la même annulation que [`bash`](../reference/tools.md#bash), les mêmes dialogues de permission (voir [yolo et permissions](yolo-and-permissions.md)), et les [hooks](hooks.md) `preToolCall` / `postToolCall`. Les skills restent l'endroit du savoir-faire et des procédures ; un skill peut aussi dire au modèle d'appeler un outil déclaré.