
L'état d'un agent comme fichier portable : déplacer un agent en marche entre machines
La plupart des outils d'agents rangent une conversation dans une ligne de base de données. Trois choses deviennent pénibles : la déplacer vers une autre machine, changer de modèle à mi-chemin, lui faire confiance après un plantage. Les trois deviennent plus simples quand l'état est un dossier de fichiers simples que n'importe qui peut ouvrir.
Ce que vous obtenez. Un format de capsule que l'on inspecte avec tar, une passation qui rejoue la même histoire pour n'importe quel fournisseur, un protocole de déplacement qui ne peut pas exécuter un agent deux fois, et une façon de tester les cas de panne au lieu de l'espérer.
1. La capsule : un état lisible
Une capsule est un dossier (ou son tar) :
manifest.json identité, détenteur, une empreinte pour chaque autre fichier
memory/working_memory.md les notes de l'agent
memory/timeline_events.jsonl le journal : un événement immuable par ligne
memory/vector_index/index.json index sémantique local
artifacts/snapshot.json l'espace de travail : chemin vers empreinte de contenu
artifacts/blobs/<sha256> corps des fichiers, stockés une fois par contenu
environment/spec.json comment reconstruire le bac à sable
- Chaque fichier a son empreinte dans le manifeste : une troncature ou une altération se détecte hors ligne.
- Le journal est chaîné par machine : chaque événement porte l'empreinte du précédent de la même machine, et une horloge logique ordonne les événements entre machines. Fusionner deux copies divergentes hors ligne est une union d'ensembles : aucun conflit à résoudre.
- La spécification d'environnement contient des noms de secrets, jamais de valeurs, et refuse une variable nommée
TOKENouKEY. Les valeurs viennent du trousseau de la machine, le temps d'une exécution.
2. Changer de modèle en pleine conversation
Le journal enregistre ce qui s'est passé (un message, un appel d'outil, un résultat), pas la façon dont un fournisseur l'écrit. Le rendu pour un fournisseur est une fonction pure du journal : on change et on revient sans perte. Trois règles :
- Le raisonnement privé est écarté quand le fournisseur change : on ne peut pas le rejouer à un autre modèle.
- Un appel d'outil sans résultat (la machine est morte en plein appel) reçoit un résultat d'erreur explicite. Tous les fournisseurs rejettent un appel resté en suspens.
- Pour une fenêtre de contexte plus petite, garder les événements récents qui tiennent, ne jamais séparer un appel de son résultat, et placer en tête un court condensé de ce qui a été retiré.
3. Déplacer un agent en marche
L'invariant : au plus une machine exécute l'agent, toujours. Le protocole est un commit en deux phases avec une époque de verrouillage (« fencing »).
geler sur la source -> proposer(époque+1) à la cible
n'envoyer que les blobs que la cible n'a pas -> la cible vérifie chaque empreinte
la cible prépare (rend le journal, construit le bac à sable)
--- point de non-retour ---
activer(époque+1) sur la cible -> la source se déclare retirée
Toute panne avant le point de non-retour annule : la cible jette son espace de préparation et la source reprend à son ancienne époque, comme si rien ne s'était passé. Une proposition refusée, un morceau corrompu et un lien coupé se traitent de la même façon.
4. L'unique moment ambigu
Si la réponse à activer est perdue, la source ne sait pas si la cible a pris le relais. Deviner dans un sens ou l'autre risque deux copies. Il faut demander à la cible quelle époque elle détient :
- Elle détient la nouvelle époque : le déplacement a eu lieu, retirer la source.
- Elle détient autre chose : il n'a pas eu lieu, reprendre la source.
- Elle est injoignable : rester gelé et attendre. Attendre coûte de la disponibilité, jamais de la justesse.
Une machine isolée qui se réveille avec une vieille époque peut proposer un déplacement, mais la cible refuse toute époque qui n'est pas plus récente que celle qu'elle détient. Une époque se compare, elle ne s'attend pas : c'est pourquoi elle est plus sûre qu'un verrou à délai.
5. Tester les pannes, pas relire le code
Enveloppez la cible dans un double de test qui fait échouer un appel nommé, soit avant son exécution (la requête est perdue), soit après (la réponse est perdue). Pilotez ensuite le vrai code à travers chaque phase et vérifiez l'invariant : un seul détenteur, une zone de préparation vide, une capsule qui se vérifie encore. Ajoutez un morceau corrompu, une époque périmée et un plantage qui perd le registre du détenteur. Écrire ces tests a révélé un vrai bug dans notre première version : la source et la cible désignaient l'agent par des identifiants différents, donc la cible ne reconnaissait jamais l'époque.
Pièges
Écrivez chaque fichier de façon atomique (fichier temporaire, puis renommage). Un plantage doit laisser un fichier complet ou absent, jamais à moitié écrit.
Une signature sur une capsule atteste un état exporté. Une capsule qui continue de tourner change de détenteur et de journal : signez à l'export, pas au repos.
Authentifier un lien n'est pas le chiffrer. Faites passer le transport par un réseau privé comme votre tailnet, ou un tunnel TLS.
Source, MIT : git.freedzhost.cloud/bengua/inari-core. À lire aussi : Inari, auto-hébergé et le glossaire.
Utile ? Dites-nous ce qu'il faut mesurer ensuite.