Pourquoi les hooks existent
Les instructions dans une invite système sont des suggestions. Un LLM suffisamment têtu ou confus peut les ignorer. Pour la plupart des comportements d’agent, c’est exactement ce que vous voulez — les instructions donnent au modèle la possibilité de s’adapter. Mais certaines exigences ne sont pas des suggestions. « Chaque appel d’outil sensible doit être enregistré. » « Les opérations d’écriture sont bloquées quand l’organisation est en mode lecture seule. » « Les paiements supérieurs à ¥50k nécessitent une validation humaine avant exécution. » Ce sont des invariants — des faits sur le système qui doivent tenir indépendamment de ce que le modèle décide à chaque tour. Un hook est du code qui s’exécute en dehors de la boucle LLM à un point bien défini du cycle de vie d’exécution de l’agent. Le LLM ne peut pas voir le hook. Le LLM ne peut pas contester le hook. Le LLM ne peut pas convaincre le hook de sauter une étape. Si un hookPreToolUse retourne allow=False, l’appel d’outil ne se produit pas — peu importe la persistance de la trace de raisonnement.
Voici la distinction architecturale critique :
Les hooks sont comment FIM One transforme « l’agent est supposé… » en « l’agent ne peut pas contourner… ».
Où les hooks se connectent
Trois points de hook sont définis aujourd’hui. Chacun marque une limite que l’agent franchit lors d’une itération de boucle :
Plusieurs hooks au même point s’exécutent dans l’ordre de priorité. Les args réécrits d’un hook
PreToolUse antérieur sont transmis aux hooks ultérieurs, de sorte que les middleware se composent.
Quand utiliser un hook plutôt qu’une instruction
Décider s’il faut résoudre une exigence avec une instruction de prompt ou un hook revient au même calcul que « assertion à l’exécution vs. commentaire de code » :
Règle empirique : si le mauvais comportement est un incident, utilisez un hook. Si le mauvais comportement est une légère gêne, une instruction suffit.
Le contrat du hook
Un hook est une sous-classe dePreToolUseHook, PostToolUseHook, ou SessionStartHook avec une méthode requise :
HookContext transmis contient tool_name, tool_args, agent_id, user_id, et un dictionnaire metadata flexible que le moteur remplit avec des faits par requête (identifiant org, identifiant conversation, le drapeau requires_confirmation de l’action du connecteur, …).
Le HookResult retourné contrôle le résultat :
allow: bool = True— si l’appel d’outil procède (ignoré pourPostToolUse/SessionStart)error: str | None— raison lisible par l’homme, présentée au LLM comme l’observation en cas de blocagemodified_args: dict | None— si défini, remplace les arguments de l’outil avant l’exécutionmodified_result: Any | None— si défini (PostToolUse), remplace l’observation avant qu’elle ne revienne au LLMside_effects: list[str]— piste d’audit de ce que le hook a fait, fusionnée dans la trace de l’agent
Étude de cas : FeishuGateHook
Le premier hook déployé sur ce système est FeishuGateHook — un hook PreToolUse qui transforme tout outil marqué requires_confirmation=True en une carte d’approbation interactive affichée dans le groupe Feishu de l’organisation.
Ce hook exerce le cycle de vie complet :
Ce design apporte :
- L’appel d’outil est véritablement suspendu. Le flux SSE de l’agent s’interrompt entre « Je vais appeler
oa__purchase_pay» et l’observation. L’utilisateur voit l’agent en attente, ce qui correspond à ce qui se passe en arrière-plan. - L’approbation survit à un redémarrage du processus. La ligne en attente est dans la base de données, pas en mémoire. Si le backend redémarre pendant qu’une carte est en attente, le prochain sondage reprend là où il s’était arrêté.
- La décision est auditée.
ConfirmationRequestconservepayload,responded_at,responded_by_open_idet le statut final — un enregistrement auditable de qui a approuvé quoi et quand. - Pas de LLM dans la boucle de décision. Le modèle produit l’appel d’outil. Les humains produisent le verdict. Le hook est le pont déterministe.
FeishuGateHook dépend d’un Canal Feishu configuré — le hook envoie la carte via la méthode send_interactive_card() du canal et écoute les événements de rappel que le canal a analysés. La séparation est intentionnelle : le hook gère la « machine d’état d’approbation », le canal gère la « mécanique de la plateforme IM ». Le même hook pourrait cibler Slack ou WeCom demain sans changer sa logique — seule l’implémentation du canal changerait.
Crochets planifiés (v0.9)
Quatre modèles de crochets sont sur la feuille de route v0.9, tous réutilisant le même cycle de vie :
Une couche de crochet définie par l’utilisateur est également planifiée : configuration YAML par agent (
hooks: [...]) déclarant des commandes shell ou des callables Python à exécuter sur les événements d’outils correspondants. Cela suit le même modèle que les frameworks d’agents modernes (Claude Code, OpenDevin) ont convergé — l’application basée sur les crochets maintient la logique « doit toujours se produire » en dehors des prompts.
Hooks vs. Channels
Les deux abstractions résolvent des problèmes orthogonaux :
Les Hooks consomment les Channels — un hook qui doit communiquer avec le monde extérieur (envoyer une carte, publier une alerte, escalader vers un groupe) appelle le Channel de l’organisation. Un channel sans aucun hook l’utilisant est toujours utile (par exemple, les agents peuvent envoyer proactivement des notifications via un outil), mais le modèle d’approbation spécifiquement nécessite que les deux parties soient en place.
Autrement dit : Les Channels sont la « plomberie » pour « où parler aux humains », les Hooks sont la « politique » pour « quand dois-je parler aux humains ». Les workflows de production avec humain dans la boucle nécessitent les deux.
État actuel (v0.8.4)
Snapshot de ce qui a été livré et ce qui est encore à venir :-
✅
HookRegistry,HookContext,HookResultprimitives intégrées dans ReAct et DAG -
✅
PreToolUseHook/PostToolUseHook/SessionStartHookclasses de base abstraites -
✅
FeishuGateHook— complet, incluant la tableConfirmationRequest, la boucle de polling, le timeout/expiration et les changements d’état pilotés par callback -
✅ Point de terminaison de callback du canal Feishu qui décode
card.action.triggeret met à jour la ligne en attente -
✅ Déclarations de hook au niveau de l’agent :
agent.model_config_json.hooks.class_hooksse résout en unHookRegistryinstancié sur chaque session ReAct/DAG -
✅ Les hooks se déclenchent sur chaque surface d’exécution : le chemin de chat principal (Portal, API, DAG), les sous-agents délégués (
CallAgentTool) et les nœudsAGENTdu Workflow. L’Eval Center contourne intentionnellement les hooks (l’évaluation automatisée ne doit pas être bloquée par une approbation humaine). Un agent délégué n’hérite pas du registre de son appelant — il construit le sien à partir de son propremodel_config_json. Comme la porte de confirmation est automatiquement attachée à chaque agent, la reconstruction est un sur-ensemble de l’héritage : le délégué obtient la porte plus tout ce qu’il déclare pour lui-même, et la porte résout lerequire_confirmation_for_alldu délégué plutôt que celui de son appelant. Les deux surfaces échouent en fermé : un agent dont les hooks ne peuvent pas être construits n’est pas exécuté. -
❌
AuditLogHook,ReadOnlyGuard,ResultTruncateHook,ConnectorRateLimitHook(v0.9) - ❌ Déclarations de hook YAML définies par l’utilisateur (v0.9)
FeishuGateHook) est également une fonctionnalité de production à part entière, c’est pourquoi le squelette a été livré tôt pour la roadshow du 2026-04-24 plutôt que d’attendre le catalogue complet des hooks.