Quand Hermes dit qu'il a joint le fichier et que votre plateforme ne reçoit rien

Le pire type de bug est celui où l’agent est sûr de lui et où l’utilisateur ne voit rien. Vous demandez à Hermes d’envoyer un rapport vers votre propre plateforme de chat — une application mobile, un outil interne, un bridge que vous avez écrit vous-même — et il répond « envoyé, le fichier est joint ». Rien n’arrive. Vous redemandez, l’agent s’excuse et renvoie, et rien n’arrive toujours, parce que l’assistant n’a aucun moyen de savoir que la pièce jointe a été abandonnée quelque part en dessous de lui.
Le symptôme et l’erreur exacte
La plateforme en question n’est pas cassée. Elle implémente send_document, send_image_file et send_video, et son sender gère media_files. Elle est simplement en dehors de l’ensemble des plateformes que l’outil send_message considère comme capables de média. Demandez-lui de livrer uniquement du média et l’outil refuse carrément :
send_message MEDIA delivery is currently only supported for telegram, discord, matrix, weixin, signal, yuanbao, feishu, whatsapp, slack and qqbot; target mobile had only media attachments
Ajoutez du texte et un fichier et vous obtenez l’échec plus silencieux : le texte passe, les pièces jointes sont abandonnées avec un avertissement qui n’apparaît que dans le tool result. Du point de vue de l’utilisateur, le message semble envoyé — c’est exactement pourquoi le mainteneur à l’origine du rapport a décrit le résultat comme son agent répétant à un utilisateur que « le fichier est joint » alors que rien n’arrivait.
Où la décision est prise
tools/send_message_tool.py répond à la question « cette plateforme peut-elle recevoir des fichiers ? » à deux endroits, et un seul concerne votre plugin :
- Une note lisible par un humain, utilisée dans ce message d’erreur :
telegram, discord, matrix, weixin, signal, yuanbao, feishu, whatsapp, slack and qqbot. - Un dict qui active réellement le chemin plugin, bien plus court :
_PLUGIN_STANDALONE_MEDIA = {
"discord": ("Discord", False, True, [], False),
"feishu": ("Feishu", True, False, None, False),
"slack": ("Slack", True, True, [], False),
"whatsapp": ("WhatsApp", True, True, None, True),
}
Chaque valeur est un tuple (label, discover_plugins_first, caption_capable, media_files_sentinel, pass_force_document), conformément au commentaire qui le précède. Cette liste existe parce que construire des fichiers dans les payloads de ces quatre plateformes demande des connaissances que le sender générique n’a pas — découverte forcée du registre de plugins, gestion des légendes, ou forme particulière pour les chunks média non finaux.
Toute plateforme plugin absente de ce dict retombe sur le chemin générique texte seul : le texte survit, les pièces jointes sont abandonnées, et un appel média seul échoue comme montré plus haut. Rien dans le code du plugin lui-même n’est consulté, et c’est la partie qui pique — la plateforme peut être parfaitement capable et rester invisible à la décision.
Pourquoi « ajouter la plateforme » ne suffit pas
Le mainteneur à l’origine du rapport a fait l’expérience évidente : ajouter localement un tuple pour sa plateforme (("Mobile App", True, True, [], False)) et l’outil achemine alors la pièce jointe via le standalone_sender_fn du plugin. Le fichier est arrivé sous forme de frame média — kind=document, mime=text/markdown, identique octet pour octet à l’original. La tuyauterie fonctionne donc ; c’est la porte qui ne fonctionne pas.
Mais un dict codé en dur signifie que chaque future plateforme plugin aura besoin d’une modification du core avant de pouvoir envoyer un fichier, et la personne qui sait que la plateforme gère le média n’est jamais celle qui édite le core. L’issue propose la sortie évidente : laisser le plugin le déclarer dans son entrée de registre — quelque chose comme media_files_supported et un flag captionable, lu là où _plugin_standalone_sender résout le sender — pour que la capacité voyage avec le plugin au lieu de vivre dans une allow-list du core.
Il y avait une seconde couche dans cette chasse au bug, et c’est un rappel utile pour quiconque écrit un adaptateur de plugin : le mainteneur a aussi trouvé deux erreurs côté adaptateur, corrigées dans la version 1.1.1 de son adaptateur. Les descripteurs de média arrivent sous forme de tuples (path, is_voice), et son sender les avait traités comme de simples chaînes, si bien qu’aucun fichier n’était récupéré. Autrement dit, « ma plateforme gère le média » doit être vrai de bout en bout — l’outil doit acheminer la pièce jointe et l’adaptateur doit comprendre ce qu’il reçoit.
Si vous tombez dessus aujourd’hui
- Regardez le tool result, pas le chat. Quand un agent annonce qu’un fichier a été envoyé, la raison pour laquelle une pièce jointe a disparu est écrite dans le tool result de
send_message, pas dans la réponse de l’assistant. Si vous déboguez un bridge à vous, cette chaîne est la première chose à lire. - Gardez le texte et le média séparés pendant votre mise au point. Les messages texte seul fonctionnent toujours sur une plateforme plugin ; un envoi média seul échoue bruyamment, ce qui est en fait plus utile pour le diagnostic.
- Pour les auteurs de plugins : suivez la question de la capacité dans votre registre au lieu de la supposer, et assurez-vous que votre
standalone_sender_fngère la forme de tuple qu’on lui donne. Si vous avez besoin de pièces jointes aujourd’hui, vos options sont un patch local du dict dans votre propre checkout Hermes, ou attendre un changement du core — ni l’un ni l’autre ne devrait incomber silencieusement aux utilisateurs. - Pour tous les autres : si vous utilisez une plateforme plugin et que la livraison de fichiers compte pour vous, ouvrez une issue ou votez pour le flag déclaré dans le registre. Une entrée par plateforme dans un dict du core est une charge de maintenance qu’une capacité déclarée fait disparaître.
État des lieux
Signalé comme issue #139354 le 15 octobre 2026, avec un cas reproductible contre un plugin mobile et le correctif vérifié localement décrit plus haut. Au moment d’écrire ces lignes, le comportement du core est inchangé : c’est le dict à quatre plateformes qui décide, et la conception suggérée dans l’issue — une capacité déclarée par le plugin — est une proposition, pas un changement livré.
À garder en perspective : send_message est un seul outil, et ceci n’est qu’une de ses portes. Si vous développez plus largement une plateforme plugin, l’entrée écosystème hermes-mobile est un exemple concret d’adaptateur qui a dû corriger cette classe de problème côté plateforme, et la référence de la commande hermes send couvre la surface CLI que l’outil enveloppe.
Sources : issue #139354 dans le dépôt Hermes Agent (y compris la vérification locale du rapporteur et les correctifs de l’adaptateur en version 1.1.1) ; tools/send_message_tool.py sur main pour _PLUGIN_STANDALONE_MEDIA et la note sur les plateformes média utilisée dans le message d’erreur.