Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Écrire un plugin de mémoire

Un plugin de mémoire est un backend de stockage : il persiste ce dont l’agent se souvient et répond aux requêtes de récupération. Il s’agit du type de plugin le plus complexe sur le plan du modèle de données. Alors qu’un outil possède une seule fonction et qu’un canal définit une forme de message, un backend de mémoire gère l’attribution multi-agents, les espaces de noms, les sessions, les catégories et la pondération de l’importance, et les sémantiques de mémoire du runtime (rappel par portée, export RGPD, substitution) reposent sur le fait que votre implémentation définit correctement le modèle de ligne.

Ce guide suppose les bases du plugin d’outil et le cycle de vie du warm-store du guide des canaux. Il est vérifié par rapport à wit/v0/memory.wit et à l’adaptateur hôte dans crates/zeroclaw-plugins/src/wasm_memory.rs.

État de l’intégration. WasmMemory implémente le trait Memory complet du runtime pour le world memory-plugin, avec contrôle par capacité et couverture par des tests unitaires. Le runtime ne le construit pas encore comme backend configurable ; l’hôte ne dispose pas encore d’un équivalent mémoire de channel_plugin_details(). Comme pour les canaux, construisez en vous basant sur le contrat : le world WIT et la sémantique de l’adaptateur sont les éléments qui sont figés. Le world mémoire ne dispose pas encore non plus d’un export de configuration ; ne demandez donc pas config_read et ajoutez-le uniquement après avoir intégré dans WasmMemory une ABI de configuration typée et un résolveur.

Le modèle de données

Un type d’enregistrement traverse la frontière dans les deux directions, memory-entry (memory.wit). Internalisez ses champs avant de concevoir le stockage, car les méthodes optionnelles sont toutes des vues sur eux :

ChampSignification
idIdentité de ligne.
keyClé de recherche. Non unique : plusieurs lignes peuvent partager une clé, une par agent. Votre stockage doit utiliser (key, agent-id) comme clé, et non key.
contentLe texte mémorisé.
categorycore (faits à long terme), daily (journaux de session), conversation (contexte), ou custom(string).
timestampHeure de création RFC 3339. Les bornes de rappel par plage de temps sont inclusives.
session-idPortée de conversation optionnelle.
namespaceLimite d’isolation entre les agents ou les contextes.
scorePertinence de récupération 0.0-1.0 ; none pour le rappel non vectoriel.
importancePoids de priorisation optionnel 0.0-1.0.
superseded-byID de l’entrée qui a remplacé celle-ci, le cas échéant.
agent-alias / agent-idNom affiché vs identifiant de stockage brut. Utilisez agent-id pour les vérifications d’égalité de portée, agent-alias pour l’affichage.

Le composite (key, agent-id) est l’erreur de loin la plus courante. Le contrat de base de get dit explicitement : lorsque plusieurs lignes partagent une clé, une ligne correspondante arbitraire est renvoyée, et la recherche limitée à un agent passe par get-for-agent. De même, forget supprime toutes les lignes d’une clé indépendamment de l’attribution, tandis que forget-for-agent supprime exactement la ligne (key, agent-id) et laisse les sœurs intactes.

Exports requis

Douze fonctions n’ont pas de défaut et doivent fonctionner (memory.wit, required-methods section) :

ExporterNotes de contrat
nameNom du backend.
get-memory-capabilitiesMasque de bits des méthodes optionnelles ; lu une seule fois au chargement.
store-entryStocker (key, content, category, session-id). Nommé store-entry car store est réservé dans wit-bindgen.
recallRequête + limite + session optionnelle et bornes temporelles RFC 3339 (inclusives). Une requête vide ou bare-* signifie un rappel uniquement temporel : renvoyer les entrées les plus récentes.
getPar clé ; ligne arbitraire en cas de collision de clé multi-agent.
list-entriesFiltres optionnels de catégorie et de session. Nommé d’après le list réservé à wit.
forgetSupprime toutes les lignes pour la clé ; true si des éléments ont été supprimés.
forget-for-agentSupprimer uniquement la ligne (key, agent-id).
countTotal des entrées.
health-checkAtteignabilité.
store-with-agent / recall-for-agentsLa paire consciente de l’attribution ; voir ci-dessous.

recall-for-agents prend une variante agent-filter : all (aucun filtre d’agent) ou some(list<string>) (restreindre aux IDs d’agents listés). Le runtime mappe sa slice Rust &[&str] comme empty-slice-means-all, donc traitez some([]) comme ne correspondant à rien, pas à tout.

Indicateurs de capacités : les 11 méthodes optionnelles

Même mécanisme que les canaux : l’hôte lit get-memory-capabilities une fois, et pour chaque drapeau non défini, il utilise la valeur par défaut du trait Rust au lieu de vous appeler. Les valeurs par défaut sont documentées en ligne dans memory.wit à côté des drapeaux, et les replis côté hôte sont visibles dans wasm_memory.rs (chaque méthode conditionnée vérifie le drapeau et emprunte le chemin de repli en cas d’absence) :

IndicateurHôte de secours lorsqu’il n’est pas défini
get-for-agentHost combine get + un filtre d’égalité sur agent-id
purge-namespace, purge-session, purge-session-for-agent, purge-agentHost retourne “non pris en charge”
reindexLe hôte renvoie 0
store-proceduralno-ops de l’hôte
ensure-agent-uuidHôte renvoie l’alias inchangé
recall-namespacedL’hôte appelle recall et post-filtre par namespace.
export-entriesL’hôte appelle list-entries et post-filtre
store-with-metadataHost délègue à store-entry, en ignorant le namespace et l’importance

Lisez cette dernière ligne deux fois : si vous n’implémentez pas store-with-metadata, l’espace de noms et l’importance demandés par le runtime sont silencieusement abandonnés par le repli. Un backend qui stocke des données dans des espaces de noms doit implémenter store-with-metadata, recall-namespaced et purge-namespace comme un ensemble, sinon l’isolation des espaces de noms se dégrade discrètement en post-filtrage et en écritures avec pertes.

La famille purge est votre surface de suppression de données. purge-agent prend un agent-alias (pas un ID) ; export-entries existe pour la portabilité des données du RGPD art. 20 et doit renvoyer les entrées ordonnées par date de création croissante avec les embeddings exclus. Si votre backend sert de véritables données utilisateur, implémentez les flags purge et export ; “not supported” est une réponse acceptable uniquement pour les backends jetables.

Esquisse : la forme du stockage

Le pattern de composant est celui du channel (instance chaude, état thread_local), donc seule la couche de données diffère. Un backend minimal honnête est une map en mémoire, correctement indexée :

#![allow(unused)]
fn main() {
use std::collections::HashMap;

struct Row {
    id: String,
    content: String,
    category: Category,
    timestamp: String,
    session_id: Option<String>,
    namespace: String,
    importance: Option<f64>,
    superseded_by: Option<String>,
    agent_alias: Option<String>,
}

/// (key, agent_id) -> Row. agent_id None représente les lignes non attribuées.
type Table = HashMap<(String, Option<String>), Row>;
}

Chaque méthode requise est ensuite un simple parcours :

  • store-entry insère à (key, None) avec l’espace de noms "default".
  • store-with-agent insère à (key, agent-id) avec l’espace de noms et l’importance de l’appelant.
  • recall filtre par sous-chaîne/rang sur content, applique le filtre de session, applique des bornes RFC 3339 inclusives sur timestamp, trie et tronque à limit. Traitez la requête vide/* comme plus-récent-en-premier.
  • recall-for-agents ajoute le parcours du filtre d’agent sur le second composant de la clé.

Un backend réel peut remplacer la map par un magasin embarqué sans modifier la structure du contrat. L’adaptateur mémoire ne lie intentionnellement pas encore wasi:http, même lorsque sa portée inclut http_client; les backends distants exigent une frontière mémoire-réseau distincte, testée au niveau des composants.

Ce que l’hôte fait autour de vous

Comprendre le comportement de l’adaptateur dans wasm_memory.rs explique plusieurs limites du contrat :

  • Cache chaud, rechargé à chaque appel. Même principe que les canaux : une instance unique pour toute la durée de vie du plugin, nouvelles données à chaque appel, appels sérialisés par un mutex. Votre backend ne voit jamais d’appels concurrents.
  • Capacités mises en cache au chargement. L’hôte lit vos indicateurs une fois lors de from_wasm et plus jamais par la suite. Il n’y a pas de découverte dynamique des capacités ; le redémarrage du daemon permet de les relire.
  • Enveloppement des traps. Chaque site d’appel enveloppe les traps avec un contexte nommé (memory.recall-namespaced trapped, etc.). Un trap dans un appel n’entraîne pas l’arrêt du plugin, mais des traps répétés rendent le backend inutilisable ; retournez err(string) pour les échecs attendus au lieu de paniquer.
  • Les mécanismes de repli de post-filtrage s’exécutent côté hôte. Lorsque votre indicateur recall-namespaced n’est pas défini, le filtre d’espace de noms s’exécute sur l’hôte après le retour de recall. La gestion de votre limit interagit avec ce mécanisme : l’hôte transmet la limite de l’appelant à recall, ce qui fait que le post-filtrage peut sous-remplir le résultat. C’est une autre raison d’implémenter nativement les variantes avec espaces de noms.

Manifeste, construction, installation

Le manifeste est le fichier nommé manifest.toml dans le répertoire du plugin. Ses champs sont la surface serde de PluginManifest dans crates/zeroclaw-plugins/src/lib.rs, qui est la source de vérité :

ChampObligatoireSignification
nameouiSlug canonique unique du package et composant du package de chaque clé de configuration d’instance dérivée. Il ne constitue pas lui-même une clé de configuration d’opérateur. Utilisez 1–128 caractères ASCII en minuscules ; commencez et terminez par [a-z0-9], avec uniquement [a-z0-9._-] entre les deux. La découverte rejette les noms non valides ou en double.
versionouiChaîne de version, p. ex. 0.1.0.
descriptionnonDescription lisible affichée par zeroclaw plugin list.
authornonNom de l’auteur ou de l’organisation.
wasm_pathpour les capacités WASMNom du fichier du composant, relatif au répertoire du plugin. Obligatoire sauf si la seule capacité est skill. La découverte ignore le plugin si le fichier nommé n’existe pas.
capabilitiesoui, non videCe qu’est le plugin : l’un de tool, channel, memory, observer, skill (PluginCapability, sérialisé en snake_case).
permissionsnonServices de l’hôte auxquels le code peut accéder : http_client, config_read, file_read, file_write, memory_read, memory_write (PluginPermission). Seuls les deux premiers sont appliqués aujourd’hui ; les autres sont acceptés mais inactifs. Déclarer config_read nécessite config_schema, et seuls les adaptateurs d’outils/de canaux le fournissent actuellement.
config_schemaexactement avec config_readRédigez un schéma JSON Draft 2020-12 pour la configuration privée de ce plugin ; il est inclus dans les octets du manifeste canonique et est donc couvert lorsque le manifeste est signé. La racine doit être un objet avec un mappage properties et additionalProperties = false. Chaque propriété de premier niveau doit avoir un type pris en charge explicite, directement ou via un pointeur JSON local : string, boolean, integer, number, array ou object. Les consommateurs d’outils et de canaux peuvent définir x-secret = true directement sur une propriété de type chaîne de premier niveau pour la retirer de la configuration publique et l’exposer via l’import d’hôte à portée limitée secrets.get. Les outils reçoivent la configuration publique sous __config et peuvent lire les secrets pendant execute. Les canaux lisent l’objet public actuel via config.get et les secrets via secrets.get pendant configure et les appels opérationnels ; ces deux imports sont indisponibles lors de l’instanciation et de la découverte des métadonnées statiques. Les marqueurs secrets imbriqués, false ou non booléens, ainsi que les propriétés secrètes qui ne sont pas de type chaîne sont rejetés. Un schéma sans config_read, ou config_read sans schéma, est rejeté.
signaturenonSignature Ed25519 en Base64url sur les octets canoniques du manifest. Défini lors de la signature pour la distribution.
publisher_keynonClé publique Ed25519 encodée en hexadécimal du signataire.

Déclarez uniquement les permissions effectivement utilisées par le code. Une permission non déclarée est une surface d’hôte que le composant ne peut pas atteindre ; une permission déclarée de manière superflue est une surface d’attaque que vous avez sollicitée et une charge d’audit pour quiconque révise votre plugin.

Les valeurs des opérateurs restent des chaînes dans plugins.entries et sont chiffrées lors de leur persistance, avec pour clé une chaîne zpi1_… versionnée dérivée de l’identité du package détenu par l’hôte, de la capacité et de la liaison (l’installation affiche et initialise la clé d’instance complète de la liaison d’outil par défaut) : les chaînes sont stockées telles quelles, les booléens et les nombres sont représentés par du texte scalaire JSON, et les tableaux et les objets par du texte JSON. Avant l’exécution de tout code invité, l’hôte matérialise ces chaînes selon les types du schéma du package et valide l’objet complet pour les adaptateurs d’outils et de canaux. Les propriétés non secrètes de l’outil constituent __config ; un canal obtient l’objet non secret via config.get. Une propriété marquée x-secret = true est exclue des deux surfaces publiques et n’est accessible que via secrets.get("property") dans un cadre de service autorisé. Les lectures publiques et secrètes d’un canal au cours d’un même appel partagent une révision canonique unique, et l’hôte supprime cette vue matérialisée à la fin de l’appel. Un plugin de canal conforme doit résoudre les deux à chaque point d’utilisation et ne doit pas conserver les valeurs de configuration ou d’identifiants dans l’état invité conservé à chaud ; renvoyer du texte en clair à l’invité signifie que l’hôte ne peut pas imposer la non-rétention face à du code malveillant. Si config_read a été demandé mais n’a pas été effectivement accordé, l’hôte valide un objet vide ; par conséquent, un schéma comportant des propriétés obligatoires échoue en mode sécurisé au lieu de démarrer sans la configuration requise. Si l’objet vide est valide, un outil omet __config lorsqu’il est vide et les imports config/secret du canal renvoient access-denied ; les appels effectués hors d’un cadre autorisé, les échecs de résolution et l’épuisement du budget d’appels à l’hôte renvoient unavailable.

Pour un backend mémoire : capabilities contenant memory. Ne demandez pas encore config_read : l’admission nécessite un schéma, mais le monde mémoire actuel n’a aucune exportation permettant à l’hôte de fournir l’objet résultant. Ne vous appuyez pas non plus sur http_client : une autorisation seule ne peut pas étendre l’adaptateur mémoire, qui n’expose actuellement aucune interface réseau.

Installez la cible WASI Preview 2 une fois, puis générez le composant :

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2

Le composant se trouve à target/wasm32-wasip2/release/<crate_name>.wasm (les tirets dans le nom du crate deviennent des underscores). Renommez-le selon ce que déclare le wasm_path de votre manifeste lorsque vous assemblez le répertoire du plugin.

[!IMPORTANT] Les fichiers .wasm et .cwasm compilés sont des artefacts binaires, souvent de plusieurs mégaoctets chacun. Ne les versionnez pas dans un arbre source git sans Git LFS : chaque reconstruction validée en tant que blob simple gonfle l’historique du dépôt de manière permanente, et les outils de git diff/revue peinent avec eux. Traitez-les comme n’importe quelle autre sortie de compilation : ajoutez target/ et *.wasm/*.cwasm à .gitignore, et distribuez-les plutôt via un artefact de release ou une archive de registre de plugins. Si un artefact doit vraiment vivre dans l’arbre, suivez le motif avec LFS (git lfs track "*.wasm") avant le premier commit.

Si l’hôte cible est un build d’exécution uniquement (sans backend JIT compilé), il ne peut pas compiler de .wasm au chargement ; il désérialise plutôt un .cwasm précompilé. Précompilez-le avec une wasmtime CLI dont la version correspond à celle de l’hôte et fournissez le .cwasm en tant qu’artefact wasm_path. Un artefact dont la version ne correspond pas sera rejeté par le contrôle de désérialisation de wasmtime et ne sera pas chargé à tort silencieusement.

Ces commandes nécessitent un binaire dans lequel l’hôte de plugins a été compilé. Les binaires de publication précompilés fournis par le programme d’installation sont compilés sans la fonctionnalité plugins-wasm, donc zeroclaw plugin ... y est une sous-commande non reconnue et les plugins installés ne sont jamais découverts. Compilez depuis les sources avec un backend d’exécution de plugins, par ex. cargo build --release --features plugins-wasm-cranelift.

Chaque plugin réside dans son propre sous-répertoire du répertoire des plugins (par défaut ~/.zeroclaw/plugins/, résolu via plugins.plugins_dir), contenant le manifeste et le composant dont le nom correspond au wasm_path du manifeste :

~/.zeroclaw/plugins/
└── my-plugin/
    ├── manifest.toml
    └── my-plugin.wasm

Installer à partir d’un répertoire local (cela valide la structure du manifeste et applique la politique de signature avant de copier quoi que ce soit) :

zeroclaw plugin install ./my-plugin/

Activer le système de plugins et confirmer la découverte :

zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin

zeroclaw plugin list et zeroclaw plugin info confirment qu’un package est installé et détectable, mais la découverte n’équivaut pas à l’activation. plugins.enabled = true active l’hôte de plugins ; les fonctionnalités d’outils et de compétences découvertes automatiquement sont chargées à l’exécution uniquement lorsque plugins.auto_discover = true l’est également, et cet indicateur vaut false par défaut (refus en cas d’échec) :

zeroclaw config set plugins.auto_discover true

Ainsi, plugins.enabled = true à lui seul vous fournit les canaux que vous déclarez sous [channels.plugin.<alias>], mais aucun outil ni aucune compétence de plugin : un paquet d’outils ou de compétences peut apparaître dans zeroclaw plugin list tout en n’apportant rien à l’exécution. Les liaisons de canaux explicites sont nommées par l’opérateur plutôt que découvertes automatiquement ; elles n’ont donc pas besoin de auto_discover ; cet indicateur contrôle uniquement les outils et compétences découverts automatiquement.

Un plugin absent de zeroclaw plugin list a été ignoré lors de la découverte : consultez le journal de démarrage pour l’avertissement d’ignorance (manifeste corrompu, fichier wasm_path manquant ou rejet par la politique de signature).

Suivant