Un routage précis pour chaque commande : Implémentation de la prise en charge de plusieurs compétences dans HagiCode Preset Task
Un routage précis pour chaque commande : Implémentation de la prise en charge de plusieurs compétences dans HagiCode Preset Task
Un preset contient plusieurs commandes, mais elles doivent partager les mêmes exigences de compétences ? Cette réforme permet à chaque commande de déclarer indépendamment les compétences dont elle dépend, et d’afficher cette liaison dans le panneau visuel — badges, résumés, installation en un clic, le tout de manière fluide.
Contexte
Commençons par un peu de contexte.
Le preset task de HagiCode est un système d’outils plug-in. Les utilisateurs n’ont pas besoin de taper des commandes manuellement, ils remplissent simplement quelques champs dans le panneau visuel, cliquent sur un bouton, et peuvent créer une session de tâche automatique. Chaque preset est essentiellement un répertoire, qui ressemble généralement à ceci :
manifest.json: informations d’identité du presetpanel.json: définition des formulaires du panneau visuelcommands.json: liste des commandes à exécutertask-preset.jsonouprompts.json: paramètres de tâche et exigences de compétences
Ce système est pratique à utiliser, mais nous avons rapidement rencontré un problème gênant.
Dans les premières versions, les compétences ne pouvaient être déclarées que dans le tableau requirements au niveau du preset. Cela signifie que toutes les commandes du même preset partageaient les mêmes exigences de compétences. Cela peut sembler anodin, mais dans la pratique, cela crée des scénarios comme celui-ci :
Un preset contient cinq commandes. La première veut utiliser la compétence last30days, la troisième veut utiliser ui-master, et les trois autres n’ont besoin d’aucune compétence. Dans l’ancienne conception, c’était impossible. Pour router différentes commandes vers différentes compétences, vous deviez diviser ces commandes en plusieurs presets, ce qui faisait gonfler la configuration.
C’est ce que la proposition extend-preset-task-multiple-skills-support vise à résoudre : permettre à chaque commande de déclarer indépendamment les compétences dont elle dépend, et de visualiser cette liaison dans l’interface.
À propos de HagiCode
La solution partagée dans cet article provient de notre expérience pratique dans le projet HagiCode. HagiCode est un projet d’assistant de code IA, et le système preset task est précisément son point d’entrée pour les opérations rapides des utilisateurs. Chaque modification décrite ci-dessous a été réalisée en surmontant des obstacles réels et en optimisant concrètement — car la théorie ne suffit jamais. Le code source du projet est disponible sur HagiCode-org/site, les intéressés peuvent y laisser une étoile.
Clarifier d’abord le problème : pourquoi pas une table de mappage
Avant de commencer, la solution la plus évidente serait de créer une table de mappage commandSkillMappings séparée pour stocker la relation “ID de commande → compétence”. Cela semble propre, une séparation des responsabilités.
Mais en y réfléchissant attentivement, on réalise que ce n’est pas correct.
Dans commands.json, chaque commande a déjà un ID, et il faudrait le copier à nouveau dans la table de mappage. Deux fichiers, le même ID : si un jour quelqu’un modifie une commande et oublie de synchroniser la table de mappage, les données dérivent. Cette conception “séparer pour séparer” a un coût de maintenance bien supérieur à la propreté qu’elle apporte. Au final, cela ne fait qu’ajouter des soucis.
Nous avons donc choisi une voie plus directe : placer directement un champ skill optionnel sur la définition de commande. Une commande déclare elle-même la compétence à laquelle elle est liée, maintenue à proximité, et rien ne se perd.
Cette décision repose sur un principe de conception plus important, qui mérite d’être mentionné séparément.
Point clé 1 : Séparation des responsabilités sur deux couches de données
C’est la compréhension la plus cruciale de cette réforme.
Beaucoup de gens réagissent d’abord : puisqu’il y a un skill sur les commandes, lors du requirement check (contrôle des exigences de compétences), ne devrait-on pas parcourir le champ skill de chaque commande ?
Non.
Nous avons délibérément séparé cela en deux couches :
- Le champ
skilldanscommands.json: est uniquement responsable de déclarer la liaison. Il indique au système “cette commande doit être liée à quelle compétence”, utilisé pour le rendu du préambule du prompt et l’affichage de l’interface. - Le tableau
requirementsdanstask-preset.json: est l’énumération faisant autorité. C’est le véritable portail, qui détermine quelles compétences doivent être satisfaites pour qu’un preset puisse s’exécuter.
En d’autres termes, skill répond à “lié à quoi, rendre quoi”, et requirements répond à “est-il autorisé à s’exécuter”. Deux choses différentes, ne les mélangez pas.
L’avantage de cette séparation est que la logique de vérification est naturellement simple. Comme le portail est toujours basé sur le requirements au niveau du preset, dédupliqué par CacheKey, plusieurs commandes liées à la même compétence ne seront sondées qu’une seule fois, sans duplication de points. Le skill au niveau de la commande n’introduit aucune surcharge de détection supplémentaire.
Ce principe est également la raison fondamentale pour laquelle nous avons rejeté la solution de la table de mappage — la table de mappage ferait croire que “la liaison équivaut au portail”, remélangeant les deux couches de responsabilités. Trop malin, c’est tout.
Point clé 2 : À quoi ressemble la définition de commande
Après la réforme, la définition de commande ajoute simplement un champ skill optionnel à la base existante. Prenons l’exemple du preset groupé last30days, son commands.json ressemble à peu près à ceci :
{ "$schema": "../../schemas/commands.schema.json", "version": "1.1", "commands": [ { "id": "research", "skill": "last30days", "prompt": "Faites des recherches sur les discussions réelles des 30 derniers jours concernant {topic}" }, { "id": "summarize", "prompt": "Organisez les résultats de recherche ci-dessus en un résumé" } ]}Quelques points importants :
- La
versionest passée à1.1, et le schéma correspondant a également ajouté le champskilloptionnel. - La première commande
researchest liée à la compétencelast30days, lors de l’exécution elle sera routée vers cette compétence. - La deuxième commande
summarizen’est liée à aucune compétence, c’est juste une instruction normale, elle suit le chemin par défaut. - Notez qu’il n’y a aucune exigence écrite dans la commande. Le véritable portail se trouve dans le
requirementsdetask-preset.json:
{ "requirements": [ { "key": "last30days", "cacheKey": "skill:last30days" } ]}Le last30days lié à la commande research doit apparaître dans ce requirements, sinon il y a un problème — c’est précisément la contrainte stricte dont parle la section suivante. On ne peut pas forcer les choses.
Point clé 3 : Validation croisée lors du chargement
Déclarer la liaison dans les données ne suffit pas, il faut quelqu’un pour garantir, empêcher qu’une “commande liée à une compétence, mais qui n’est pas du tout déclarée dans les requirements” ne se glisse en production.
Cette garantie est ValidateCommandSkills. Il s’exécute une fois lors du chargement du preset, vérifiant une par une si le skill de chaque commande peut trouver un élément correspondant dans le requirements au niveau du preset. S’il ne trouve pas, le preset est jugé illégal, tout le preset est directement désactivé, et le code de diagnostic command-skill-not-in-requirements est lancé.
Pourquoi désactiver tout le package au lieu de simplement sauter cette commande ? Parce qu’un preset est un tout, les commandes ont souvent des relations de dépendance (la sortie de la première est transmise à la suivante). Si on saute silencieusement une commande, les commandes suivantes reçoivent une entrée vide, et le comportement devient complètement incontrôlable. Après tout, les cœurs sont opaques, et le code aussi. Il vaut mieux que l’utilisateur voie une erreur explicite, plutôt que la tâche ne parte nulle part de manière incompréhensible. Ce point ne doit pas être pris à la légère.
Cette validation est effectuée lors du chargement, c’est-à-dire que le problème sera découvert au moment de l’enregistrement du preset, et ne sera pas retardé jusqu’à ce que l’utilisateur clique sur “exécuter” pour exploser. Pour l’expérience utilisateur, une erreur précoce est toujours meilleure qu’une erreur tardive.
Point clé 4 : Concaténation idempotente du préambule de prompt
Ensuite, c’est le lien le plus subtil de la chaîne d’exécution.
Lorsqu’une commande est liée à une compétence, comme last30days, le système avant l’exécution réelle doit “coller” cette information de compétence devant la commande, formant une instruction complète sur une seule ligne pour l’exécuteur. Ce processus est géré par CombineCommandSkillPrelude.
Prenons un exemple concret. Le prompt de la commande research est “Faites des recherches sur les discussions réelles des 30 derniers jours concernant {topic}”, la compétence liée est last30days, alors l’instruction finalement transmise à l’exécuteur est approximativement :
/last30days Faites des recherches sur les discussions réelles des 30 derniers jours concernant {topic}C’est-à-dire qu’on a ajouté le préambule /last30days devant le prompt. L’exécuteur voyant ce préambule sait qu’il faut d’abord basculer le contexte sur la compétence last30days.
Il y a ici un piège facile : l’idempotence.
Pourquoi insister sur l’idempotence ? Parce que dans certains scénarios, le prompt lui-même peut déjà porter ce préambule de compétence (par exemple l’utilisateur a écrit manuellement la moitié, ou l’a copié d’ailleurs). Si le système bêtement colle encore une fois, cela devient /last30days /last30days Faites des recherches..., l’exécuteur soit signale une erreur, soit se comporte de manière anormale.
Donc CombineCommandSkillPrelude détecte avant de concaténer, si le préfixe existe déjà, il ne l’ajoute pas en double. Cette étape semble insignifiante, mais peut bloquer une classe de bugs très subtils.
Il convient de mentionner que toute cette logique d’injection de préambule est achevée au niveau de la définition du preset (BuildCommandPrelude dans PresetTaskCatalogProvider), le code de création de session du côté de SessionsController n’a pas besoin d’être modifié du tout. C’est aussi l’avantage apporté par la séparation des responsabilités — le point d’entrée d’exécution reste stable, la complexité du routage des compétences est contenue à l’intérieur de la couche de définition.
Point clé 5 : Comment le frontend affiche la liaison
Le backend a bien organisé le modèle de données et la chaîne d’exécution, la dernière étape est de permettre à l’utilisateur de “voir” cette liaison dans l’interface. Après tout, si l’utilisateur ne perçoit pas une fonctionnalité, c’est comme si elle n’existait pas.
Le frontend a fait trois choses.
Premièrement, ajouter des badges sur le sélecteur de commandes. Dans le command-picker, à côté de chaque commande liée à une compétence, un petit badge s’affiche, indiquant de quelle compétence elle dépend. L’utilisateur d’un coup d’œil sait quelle commande est “avec compétence”, quelle est une commande normale.
Deuxièmement, un bloc de résumé requirement-check. Sur le panneau, il y a une zone de résumé dédiée, listant toutes les exigences de compétences que le preset actuel doit satisfaire, et quelle compétence est liée à chaque commande. Les données de ce bloc proviennent du mappage commandSkillsByRequirementKey — qui regroupe les commandes par leur requirement key lié, permettant à l’utilisateur de comparer d’un coup d’œil si “exigences” et “liaison réelle” correspondent. Dessiner un tigre et se retrouver avec un chat, c’est à peu près ça — donc la logique d’agrégation doit être directe, pas fantaisiste.
Troisièmement, lien profond d’installation en un clic en cas d’échec. Si le requirement check découvre qu’une compétence n’est pas installée, l’utilisateur n’a pas besoin de chercher la documentation pour trouver l’entrée d’installation. L’interface donne directement un bouton de lien profond, cliquez pour sauter au processus d’installation correspondant. Cette étape réduit au minimum la distance entre “découvrir le problème” et “résoudre le problème”.
Côté types du frontend, c’est aussi très sobre, le type de commande ajoute simplement un skill?: string, et fait une normalisation (|| undefined), évitant que les valeurs frontières comme les chaînes vides ne causent des problèmes dans les jugements ultérieurs.
Pratique : cinq étapes pour une réforme complète
En reliant les points épars précédents, toute la réforme se résume en fait à cinq étapes :
- Étendre le schéma :
commands.schema.jsonajoute le champskilloptionnel, le numéro de version passe à1.1. - Analyse + validation :
NormalizeCommandsest responsable de l’analyse des définitions de commandes,ValidateCommandSkillsfait la validation croisée, le skill de commande doit pouvoir être trouvé dans le requirements au niveau du preset. - Injection du préambule :
BuildCommandPreludecolle idempotemment le préambule/skilldevant la commande avant l’exécution, pas besoin de modifierSessionsController. - Migration des presets groupés :
last30daysetui-master, ces deux presets intégrés, modifient leurcommands.json, en ajoutant le champskillaux commandes correspondantes. La migration ne touche que commands.json, n’altère pas les autres fichiers. - Visualisation frontend : le type complète le champ, command-picker ajoute des badges, requirement-check ajoute un bloc de résumé, en cas d’échec donne un lien profond d’installation en un clic.
Quelques points d’attention dans la pratique, listés séparément :
- Une commande ne peut être liée qu’à une compétence. C’est la contrainte actuelle. Si un scénario a vraiment besoin qu’une commande déclenche plusieurs compétences, l’échappatoire est de déclarer plusieurs compétences dans le
requirementsau niveau du preset, pour qu’elles coexistent au niveau du preset. - Le code de diagnostic en cas d’échec de validation est
command-skill-not-in-requirements, lors du dépannage recherchez directement ce code. - Normalisation frontend rappelez-vous
|| undefined, ne laissez pas les chaînes vides se mélanger dans la logique de jugement. - Lors de la migration ne touchez que commands.json, le côté requirements reste inchangé, évitez d’introduire des modifications accidentelles.
- Tests backend couvrent trois scénarios : le skill de commande est dans le requirements (passe), n’est pas (désactive le package), plusieurs commandes liées à la même compétence (déduplication normale).
Conclusion
Cette réforme de la prise en charge de plusieurs compétences du preset task, en apparence ajoute simplement un champ skill aux commandes, mais derrière elle se cache un problème de conception qui mérite réflexion : la liaison et le portail, doivent-ils être séparés ou non ?
Notre réponse est de les séparer. Le champ skill ne gère que “lié à quoi, rendre quoi”, le requirements gère “autoriser ou non l’exécution”. Une fois ces deux couches de responsabilités mélangées, que ce soit avec une table de mappage ou toute autre forme, cela rendra la validation, la déduplication et l’affichage de l’interface maladroits. Après les avoir séparés, chaque couche devient simple : le portail est toujours basé sur une énumération faisant autorité, la liaison est maintenue à proximité sans dériver, la concaténation du préambule est idempotente et contrôlable, l’interface ne fait qu’afficher des données déjà claires.
En regardant en arrière, toute la réforme n’a utilisé aucune technique sophistiquée, elle repose simplement sur la séparation propre des responsabilités, puis sur la garantie de chaque couche. Le système preset task de HagiCode après ce polissage peut enfin router chaque commande avec précision vers la compétence qu’elle doit atteindre. En fin de compte, les choses devraient être aussi simples…
Références
- HagiCode-org/site : code source du projet, l’implémentation complète du système preset task est ici.
- Site officiel HagiCode : pour découvrir les capacités globales de HagiCode.
- Proposition OpenSpec
extend-preset-task-multiple-skills-support: document de conception original de cette réforme, contient la proposition, le design et les tâches.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。