Six outils, et l'endroit où vivent les règles
Un agent qui tient seul le suivi de projet, il faut quand même lui expliquer comment. Restait à savoir où écrire les consignes pour qu’il les lise au bon moment : dans la description des outils.
Dans le premier billet, j’ai présenté tsutsumbi, un suivi de projet que l’agent tient tout seul : il ouvre les tickets, change les statuts et les clôt sans rien me demander. Sur le papier, c’est parfait. En pratique, un agent laissé à lui-même ouvre des tickets du genre « ajouter la migration » ou « corriger le test qui échoue », qui ne voudront plus rien dire dans trois mois.
Un ticket bien formulé, une résolution honnête, un blocage expliqué : aucun code ne les obtient à ma place, ce sont des consignes. Il fallait bien les écrire quelque part, et cette histoire de « où » m’a occupé bien plus que prévu (je la croyais secondaire, j’avais tort).
Là où ça ne marche pas
J’ai fait le tour des endroits habituels. Le README, personne ne le lit au bon moment. Le fichier de consignes du projet, l’agent le lit au démarrage de la session : c’est utile, mais entre ce moment-là et l’appel d’outil, il s’est passé une heure de travail et une vingtaine de décisions. Un commentaire dans le code, il ne le voit jamais. Et une validation côté serveur arrive après coup : elle peut refuser l’appel, mais elle n’aide pas l’agent à choisir le bon.
Reste la description de l’outil. C’est le texte que l’agent a sous les yeux au moment précis où il choisit quoi appeler, donc c’est là que j’ai rangé les règles. C’est d’ailleurs écrit en tête du fichier qui déclare les outils (en anglais dans le code, je traduis) :
* Les descriptions sont l'endroit où vivent les règles d'usage. Un agent les lit
* au moment où il décide quoi appeler, le seul moment où elles peuvent changer ce
* qu'il fait : une règle écrite ailleurs est une règle lue trop tard.
Des descriptions qui disent quoi éviter
Du coup, les descriptions ne se contentent plus d’expliquer les paramètres : elles disent quoi faire, et surtout quoi ne pas faire. Celle de create_ticket aligne les contre-exemples :
Un ticket est une fonctionnalité ou un correctif que quelqu'un reconnaîtrait
trois mois plus tard : « export CSV des relevés », « TVA fausse sur les avoirs
partiels ». Jamais une étape technique : pas « refactoriser le repository »,
pas « ajouter la migration », pas « corriger le test qui échoue ». C'est la
façon dont le travail se fait, pas le travail.
Une règle, trois contre-exemples. Dans une doc, ce serait disproportionné. Ici, c’est la dernière chose que l’agent lit avant que l’appel parte, alors j’insiste (lourdement, j’avoue).
Il y a un revers : une consigne devient du code déployé. Pour la corriger, il faut toucher au serveur et relancer la session, alors qu’une ligne de README se change en trois secondes.
Le serveur ne décide de rien
Le serveur MCP, lui, sert de pont : il traduit sept appels d’outils en sept appels HTTP, et c’est tout. Aucune règle métier n’y vit : ni les statuts, ni les résolutions, ni la numérotation des tickets. Tout ça reste dans l’application.
La tentation existe, je l’avoue. Valider dans le pont, ça économise un aller-retour et ça donne de plus jolis messages d’erreur. Mais on se retrouve avec deux exemplaires de la même règle, et le jour où ils ne disent plus la même chose, c’est celui qui est le plus près du modèle qui gagne, donc celui du pont, qui n’est pas le bon.
Le pont fait quand même ce que l’application ne peut pas faire. Résoudre un lien symbolique, par exemple : l’application ne tourne pas forcément sur la machine où vivent les projets, le pont si.
Six outils, pas un de plus (enfin…)
Au départ, la règle affichée était « six outils, pas un de plus ». Elle a cédé sur un usage tout bête. list_tickets renvoie des résultats de recherche, et un résultat porte le titre et le statut, jamais le texte. L’agent voyait donc qu’un ticket existait sans jamais lire ce qu’on lui avait demandé dedans (pratique pour ignorer une consigne en toute bonne foi).
J’ai donc ajouté get_ticket, et sa description dit pourquoi il existe (je traduis toujours) : « list_tickets trouve un ticket, celui-ci le lit ». Elle se termine sur le défaut qu’il corrige : « un commentaire que personne n’a lu est une instruction manquée ».
Je garde la règle quand même. Elle a rendu l’ajout coûteux, et il a fallu l’argumenter avant de le faire. Le compteur est donc à sept.
Deux pièges
Vider un champ, ou ne pas y toucher
Dans un appel d’outil, une valeur vide et une clé qu’on n’a pas envoyée arrivent pareil. Or « vide la description » et « laisse la description tranquille », ce sont deux intentions opposées, et techniquement rien ne permet de les distinguer.
J’ai donc posé une convention : une chaîne vide efface, une clé absente laisse en place. Et pour que l’agent la connaisse, elle est écrite dans la description du paramètre (évidemment) : « envoyer une chaîne vide pour l’effacer ».
Un serveur « déconnecté », et c’est tout
Celui-là peut vous coûter une soirée. Le serveur MCP est lancé sans shell de connexion, donc sans le profil qui ajoute node au PATH quand il vient de nvm. À l’écran, on voit juste un serveur « déconnecté », sans un mot de plus, et rien ne fait penser au PATH.
La commande déclarée pointe donc sur un petit script de lancement, mcp/launch.sh, qui va chercher node au démarrage. Et ça tient même après un changement de version de node.
Bref, les règles sont dans les descriptions, et l’agent les lit au moment où elles servent.
github.com/beeraw/tsutsumbi : le dossier mcp/ tient en quelques centaines de lignes, dont la moitié sont les descriptions des outils. Si vous ne devez en lire qu’une partie, c’est celle-là.