hachiflow.com
Architecture

Le bus est le substrat

Il m'a fallu un an pour dire simplement ce qui m'agace dans deux produits que j'utilise tous les jours. L'un a réussi le mode solo et s'est arrêté là. L'autre a réussi la pièce et y a soudé les agents. Waggle est l'architecture née de cet agacement, et les notes ci-dessous en sont les preuves : un identifiant de règle, un type d'événement, une trace, et le bogue que j'ai attrapé le deuxième jour.

Deux produits, une pièce manquante

Grokbot a remporté l'expérience solo, et personne ne la lui a reprise depuis. Une personne, un bot, un ordinateur, et la boucle est si serrée qu'on cesse de remarquer le logiciel. Les clones sont d'accord. J'ai passé une journée à lire Rakazo au commit 616d2353, un clone de Grok Bot sous licence Apache-2.0, et c'est du bon travail : une approbation qui persiste l'appel d'outil en attente et rejoue exactement les arguments approuvés au lieu de redemander au modèle de les déduire. Ça vaut le coup d'être piqué.

J'ai ensuite cherché les pièces qui me permettraient de changer son comportement sans toucher à son code. Elles n'étaient pas là. L'entrée est soudée à une destination : une URL de webhook est POST /api/v1/bots/:botId/webhook, donc l'appelant nomme le bot et il ne reste rien à router. Pas d'artefact de règles, donc rien à comparer. Pas d'identifiant de trace persistant : AdapterContext.traceId est un champ de journal qui porte l'identifiant d'exécution, et aucune exécution n'enregistre de parent, si bien qu'une chaîne de bot à bot sur six sauts ne peut être remontée par aucune requête. Rien de tout cela ne peut être optimisé, et aucun agent ne peut partir.

Buzz est l'autre moitié de l'agacement, et j'en vends l'hébergement, donc j'ai le droit d'être honnêtement franc. Buzz a ce que Grokbot ne peut pas exprimer : plus d'un humain dans la pièce, avec les agents comme membres plutôt que comme extensions facturées à l'usage. Un membre est une paire de clés. Un agent est une paire de clés dont l'événement d'authentification porte la signature de son propriétaire, si bien que le badge de bot et l'attestation sont des faits du relais, pas de l'habillage applicatif (le mécanisme est ici). Tout le reste est soudé : l'identité est frappée sur le poste de travail via Keys::generate(), la clé privée voyage jusqu'à l'exécuteur dans la charge utile de déploiement, et le corps tourne là où il a été lancé. Fermez l'ordinateur portable, et votre coéquipier disparaît.

Le bus est le substrat

Waggle est ce qu'il reste quand on refuse de souder quoi que ce soit de tout cela. Six pièces, chacune remplaçable. L'entrée estampille la confiance et l'identité. Un journal à ajout seul garde chaque événement. Un fichier de règles décide qui se réveille. Les exécuteurs sont des destinations interchangeables. Les identifiants reposent dans un séquestre avec des baux et un proxy. Un unique identifiant de trace relie tout l'ensemble.

Rien de tout cela n'est abstrait. Un événement atterrit sur ev.<tenant>.<source>.<type> avec deux champs que la plupart des systèmes confondent : principal, qui l'a déposé, et trust, si cette affirmation a été vérifiée. Un envoi Stripe avec une signature valide est partner:stripe avec une confiance de partenaire. Le même envoi avec une signature invalide est un 401, et n'est jamais ajouté du tout. L'identifiant dérive d'une clé d'idempotence qui inclut le locataire, si bien que le même événement livré deux fois obtient le même identifiant et le journal rejette la seconde copie.

Le routeur lit un fichier TOML. Ni un prompt, ni une ligne de base de données : un fichier contenant des identifiants de règles, rechargé à chaud, validé contre le registre en direct avant d'être écrit, refusé en bloc si deux règles d'un même locataire partagent un identifiant. Ce fichier est l'artefact : ce que vous comparez dans une pull request, ce qu'un agent peut proposer de modifier, et ce qu'une trace nomme quand elle dit pourquoi quelque chose s'est réveillé. Une destination est aussi une ligne, avec un livreur, donc changer où un agent s'exécute revient à modifier une seule ligne.

Buzz, Slack toute surface webhook signé ou non cron à l'horloge entrée locataire et principal confiance estampillée ici fichier de règles r-cos2-addressed qui se réveille et pourquoi exécuteur un tour par réveil séquestre bail et proxy la surface relay et liste de membres espace réservé ev.<tenant>.<source>.<type> ajout seul hachi trace un seul id, de la touche à l'appel du modèle
Six pièces, chacune remplaçable. La surface, ce sont deux choses ordinaires, une source d'entrée et un livreur, donc Buzz et Slack sont des lignes et non des réécritures. Le séquestre remet un espace réservé à l'exécuteur et substitue la valeur au proxy, si bien que le modèle ne lit jamais de clé. Tout atterrit dans un seul journal, sous un même identifiant de trace.

Les agents ne parlent pas, ils émettent

La règle qui m'a pris le plus de temps à croire est celle que je préfère aujourd'hui : les agents ne tiennent pas une conversation, ils émettent des événements, et un événement adressé à un agent le réveille. Il n'y a pas un bus de messages pour le chat et un autre pour le travail. Il y a un seul journal. Une réponse est un événement avec addressed_to renseigné, et la trace l'enregistre, qu'un humain regarde ou non.

Deux jours après avoir fait tourner de vrais tours, j'ai découvert que c'était faux. Le 8 septembre à 05:07:59Z, j'ai demandé à agent:cos2, un chef de cabinet sur le vrai runtime Claude, des nouvelles de son agent marketing. Il a fait ce qu'il fallait : il a émis un ask adressé à agent:growth, événement 3YRSRD08JX549QHDHCXPZXRK7P, sous la trace 7MDV4GJYX2TWTMQH5SP23PYSJ8. Rien ne correspondait. Les règles générées correspondaient aux lignes de réveil, aux messages console et aux avis de séquestre, si bien qu'un événement de partenaire dont le sujet était un autre agent ne correspondait à aucune d'elles. Le ask est tombé sur la destination par défaut, growth ne s'est jamais réveillé, l'exécution de cos2 s'est terminée trois secondes plus tard, et le chat n'a rien montré du tout.

Le correctif est ce qui m'a convaincu que la forme est la bonne, parce que ce n'était pas une fonctionnalité. C'était une règle standard de plus, générée pour chaque agent, r-<name>-addressed, correspondant à subject = "agent:<name>" et trust = "partner", placée après les propres lignes de réveil de l'agent pour qu'une règle spécifique l'emporte toujours, avec une migration au démarrage qui l'ajoute une fois à chaque agent déjà existant, et une vérification à l'entrée qui refuse un envoi adressé à soi-même. La conversation d'agent à agent, sur un bus, tient en cinq lignes de TOML généré. Sans cela, c'est un élément de feuille de route avec une date de lancement.

Le modèle ne lit jamais de clé

Les identifiants sont l'endroit où cela cesse d'être une question de goût architectural. Un jeton d'abonnement est une concession dans un séquestre : service claude_code, champ oauth_token, hôte api.anthropic.com. Au réveil, le séquestre émet un bail et le conteneur reçoit l'espace réservé __claude_code_oauth_token__, exactement la variable dans laquelle Claude Code lit un jeton. La seule sortie du conteneur est un proxy qui substitue la vraie valeur dans l'en-tête de la requête, pour cet unique hôte. Le modèle voit un espace réservé. Le disque aussi : le journal d'une exécution qui passe indique AGENTMAIL_API_KEY=__agentmail_api_key__, parce que c'est ce qu'on a donné au processus.

Chaque étape est un événement et aucun ne porte de valeur : credential.requested, credential.granted, credential.leased au réveil, credential.used une fois par requête vue par le proxy, credential.revoked quand vous le reprenez. La piste d'audit n'est pas un journal que quelqu'un a pensé à écrire, c'est le même journal que tout le reste. Le crédit à qui il revient : la forme du runner et du courtier est celle de Jake Gaylor, tirée de Fountain, qui avait des bacs à sable par coéquipier qui se mettent en pause et se réveillent, et des identifiants d'inférence du locataire que l'agent ne lit jamais, bien avant que j'aie quoi que ce soit de tout cela. Nommons l'anti-motif sans détour. Mettez la clé dans l'environnement, et n'importe quelle commande shell exécutée par le modèle peut la lire, et aucune rédaction de sous-chaîne ne referme cela, parce que le base64 existe.

Un seul humain, et la trace est le canal

Alors, où va la fenêtre de chat ? Avec un seul humain, nulle part. Waggle est l'interface et la trace est le canal : un message qui entre, la règle qui a correspondu, le réveil, les appels d'outils du modèle, ses émissions, le code de sortie, tout sous un identifiant que vous pouvez imprimer. hachi trace montre le saut d'entrée, la règle qui s'est déclenchée, la destination qui a accusé réception, et chaque enfant lié sous le saut qui l'a causé. Le rendu de tout cela en conversation est une vue, pas un second produit.

Un second humain change l'exigence sans changer l'architecture. Ce dont deux personnes ont besoin, c'est d'un lieu partagé avec une liste de membres, une présence et un historique, ce en quoi un produit de chat excelle précisément, et ce qu'un bus ne devrait justement pas essayer de devenir. Une surface est donc deux choses ordinaires : une source d'entrée et un livreur.

Pour Buzz, c'est une connexion de relais par agent lié, chaque événement accepté ajouté comme une enveloppe buzz.message dont la signature est revérifiée à l'entrée, des règles ordinaires qui l'acheminent vers l'agent et ramènent l'answer de l'agent, et un livreur qui signe la réponse au nom de l'agent. J'ai chiffré cette démo à environ quatre jours répartis sur six éléments, et aucun n'est un concept nouveau : Buzz sait déjà invoquer le binaire d'un fournisseur. Slack, ce sont les deux mêmes choses avec une cravate différente. Buzz devrait être une surface. Slack est une surface.

L'agent est à vous, le lieu de travail est à eux

Une fois que l'exécution, l'inférence, les identifiants et l'identité sont des choses séparées, la propriété cesse d'être une question de philosophie et devient quatre champs. La propriété repose sur l'agent. L'inférence, l'exécution et les données reposent sur le lien, ce que détient un lieu de travail : quel abonnement paie le tour, quel exécuteur le fait tourner, et ce qu'il peut garder. L'identifiant d'un agent est sa propre clé publique, frappée à sa création, la moitié privée étant une concession dans le séquestre, révélée seulement à l'intérieur du démon et jamais prêtée à un exécuteur.

L'expérience se divise sur la ligne de confidentialité : les notes de métier appartiennent au propriétaire et voyagent avec lui, les notes du locataire appartiennent au lieu de travail et n'en sortent jamais. Un passeport porte l'identité, l'âme, le métier, et des références aux concessions du propriétaire sans aucune valeur à l'intérieur, jamais la mémoire d'une entreprise. Le trousseau qui porterait ces concessions vers une autre instance est en cours de rédaction en amont sous la forme de block/buzz PR #6011. Les conditions sont vérifiables plutôt que promises : sur le mini, j'ai réglé un lien sur inference: owner pour un propriétaire sans aucune concession d'abonnement, et son réveil suivant a produit un credential.refused disant, en mots, qu'aucune concession claude_code du propriétaire ne couvrait cet agent dans ce locataire. Les conditions ne sont pas de la documentation. Ce sont ce que lit l'émetteur du bail.

Les chiffres, petits et réels

Rien de tout cela ne tourne sur un cluster. Un Mac mini, docker compose. Tout le scénario, depuis la création d'un chef de cabinet jusqu'au réveil de marketing avec un bail qu'il ne voit jamais que comme un espace réservé, prend 15 secondes de la première création au PASS, et quatre tours de conteneur. Chacun des huit secrets du fichier d'environnement a été recherché par grep à la fois dans les journaux de bout en bout et dans le journal de build, avant que ces journaux ne quittent la machine. Zéro correspondance pour chacun. La fausse clé que colle l'exécution n'a été trouvée dans aucun journal, aucune configuration, aucune boîte d'envoi ni aucune trace.

Le plan estimait 39 à 54 jours ouvrés pour un ingénieur sur cinq phases, plus 14 à 24 pour le jalon de propriété. Un essaim d'agents a construit les cinq phases, et elles sont passées sur le mini le 7 septembre ; la migration de propriété s'est exécutée une fois au démarrage le 8 septembre, a transformé 13 agents existants en objets possédés avec leurs propres identités, et a laissé leurs 33 règles générées intactes. Deux jours pour les deux. La couverture sur la cible de vérification est de 88,7 pour cent, et la suite en direct du builder passe 15 sur 15. Les lacunes, parce qu'une note de terrain sans elles est une publicité : un véritable appel sortant via le proxy avec l'espace réservé substitué est couvert par un test contre un amont local et non par le mini, et le lien avec Buzz décrit ci-dessus est une lecture de conception contre le code source du relais, pas une fonctionnalité livrée.

Le substrat est le produit

Voici l'argumentaire sans le vernis. Le substrat est le produit. Les surfaces sont des commodités : une appli de chat, c'est une liste de membres, un relais et une case de texte, et il y en aura une meilleure l'année prochaine. Les exécuteurs sont des commodités : un conteneur aujourd'hui, du calcul loué demain, une microVM ensuite. Les modèles sont des commodités, et ils deviennent moins chers exprès. Ce qui n'est pas une commodité, c'est le registre de ce qui s'est passé, pourquoi c'est arrivé, et ce qu'on lui a permis de toucher. Ce registre est le seul artefact contre lequel quiconque peut optimiser. Le fossé, c'est la trace.

Alors prenez le test plutôt que l'argumentaire. Trouvez la dernière fois qu'un de vos agents a fait ce qu'il ne fallait pas, et demandez-vous quel artefact vous modifieriez pour empêcher que cela se reproduise. Si la réponse est un prompt plus long, vous avez un bot. Si la réponse est un identifiant de règle que vous pouvez comparer et un identifiant de trace que vous pouvez imprimer, vous avez un bus. Construisez le bus.

← Toutes les notes