Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Escribiendo un complemento de memoria

Un complemento de memoria es un backend de almacenamiento: persiste lo que el agente recuerda y responde a consultas de recuperación. Es el tipo de complemento con mayor carga de modelo de datos. Mientras una herramienta tiene una función y un canal tiene una forma de mensaje, un backend de memoria tiene atribución multiagente, espacios de nombres, sesiones, categorías y ponderación de importancia, y la semántica de memoria del runtime (recuperación con ámbito, exportación GDPR, superposición) depende de que tu implementación modele correctamente las filas.

Esta guía asume los conceptos básicos del plugin de herramienta y el ciclo de vida de warm-store de la guía de canal. Está contrastada con wit/v0/memory.wit y el adaptador de host en crates/zeroclaw-plugins/src/wasm_memory.rs.

Estado de integración. WasmMemory implementa el trait Memory completo del runtime contra el mundo memory-plugin, con control de capacidades y cubierto por pruebas unitarias y una prueba end-to-end con un componente real. El runtime todavía no lo construye como backend configurable; el host carece de una contraparte de memoria para channel_plugin_details(). Igual que con los canales, hay que construir contra el contrato: el mundo WIT y la semántica del adaptador son la parte que se congela. El mundo de memoria tampoco tiene todavía una exportación de configuración, así que no solicites config_read; añádelo solo después de que un ABI de configuración tipado y un resolutor estén conectados a WasmMemory.

El modelo de datos

Un tipo de registro cruza la frontera en ambas direcciones, memory-entry (memory.wit). Interioriza sus campos antes de diseñar el almacenamiento, porque los métodos opcionales son solo vistas sobre ellos:

CampoSignificado
idIdentidad de fila.
keyClave de búsqueda. No única: varias filas pueden compartir una clave, una por agente. Tu almacenamiento debe usar como clave (key, agent-id), no key.
contentEl texto recordado.
categorycore (hechos a largo plazo), daily (registros de sesión), conversation (contexto) o custom(string).
timestampHora de creación RFC 3339. Los límites del intervalo de tiempo son inclusivos.
session-idÁmbito opcional de la conversación.
namespaceLímite de aislamiento entre agentes o contextos.
scoreRelevancia de recuperación 0.0-1.0; none para recuperación no vectorial.
importancePeso de priorización opcional 0.0-1.0.
superseded-byID de la entrada que reemplazó a esta, si la hubiera.
agent-alias / agent-idNombre para mostrar versus identificador de almacenamiento sin procesar. Usa agent-id para las comprobaciones de igualdad de ámbito, agent-alias para la visualización.

El compuesto (key, agent-id) es el error más común con diferencia. El contrato base de get dice explícitamente: cuando varias filas comparten una clave, se devuelve una fila coincidente arbitraria, y la búsqueda con ámbito de agente pasa por get-for-agent. Del mismo modo, forget elimina todas las filas de una clave independientemente de la atribución, mientras que forget-for-agent elimina exactamente la fila (key, agent-id) y deja intactas las filas hermanas.

Exportaciones requeridas

Doce funciones no tienen valor predeterminado y deben funcionar (memory.wit, sección required-methods):

ExportarNotas del contrato
nameNombre del backend.
get-memory-capabilitiesMáscara de bits de métodos opcionales; se lee una sola vez al cargar.
store-entryAlmacena (key, content, category, session-id). Se denomina store-entry porque store está reservado en wit-bindgen.
recallConsulta + límite + sesión opcional y límites de tiempo RFC 3339 (inclusivos). Una consulta vacía o solo * significa recuperación solo por tiempo: devuelve las entradas más recientes.
getPor clave; fila arbitraria en colisión de claves de múltiples agentes.
list-entriesFiltros opcionales de categoría y sesión. Nombrados en honor al list reservado por wit.
forgetEliminar todas las filas para la clave; true si se eliminó algo.
forget-for-agentElimina solo la fila (key, agent-id).
countEntradas totales.
health-checkAccesibilidad
store-with-agent / recall-for-agentsEl par con reconocimiento de atribución; véase abajo.

recall-for-agents toma una variante de agent-filter: all (sin filtro de agente) o some(list<string>) (restringe a los IDs de agente indicados). El entorno de ejecución mapea su slice de Rust &[&str] como slice vacío significa all, así que trata some([]) como que no coincide con nada, no con todo.

Banderas de capacidad: los 11 métodos opcionales

Mismo mecanismo que para los canales: el host lee get-memory-capabilities una vez y, para cada bandera no establecida, usa el valor predeterminado del trait de Rust en lugar de llamarte. Los valores predeterminados están documentados en línea en memory.wit junto a las banderas, y los valores de reserva del lado del host son visibles en wasm_memory.rs (cada método con control de compuerta comprueba la bandera y toma la ruta de reserva cuando está ausente):

BanderaHost alternativo cuando no está establecido
get-for-agentEl host compone get + filtro de igualdad de agent-id
purge-namespace, purge-session, purge-session-for-agent, purge-agentEl host devuelve “no compatible”
reindexHost devuelve 0
store-proceduralAnfitrión sin operaciones
ensure-agent-uuidEl host devuelve el alias sin cambios
recall-namespacedEl host llama a recall y luego filtra por namespace
export-entriesEl host llama a list-entries y filtra después
store-with-metadataHost delega en store-entry, eliminando el espacio de nombres y la importancia

Lee esa última fila dos veces: si no implementas store-with-metadata, el espacio de nombres y la importancia que pidió el runtime se descartan silenciosamente por la alternativa de respaldo. Un backend que almacena datos con espacio de nombres debe implementar store-with-metadata, recall-namespaced y purge-namespace como un conjunto, o el aislamiento por espacio de nombres se degrada silenciosamente a posfiltrado y escrituras con pérdida.

La familia purge es tu superficie de eliminación de datos. purge-agent toma un agent-alias (no un ID); export-entries existe para la portabilidad de datos del Art. 20 del RGPD y debe devolver las entradas ordenadas por hora de creación ascendente, con los embeddings excluidos. Si tu backend sirve datos reales de usuarios, implementa los flags de purge y export; “not supported” es una respuesta aceptable solo para backends desechables.

Sketch: la forma de almacenamiento

El patrón de componente es el del canal (instancia cálida, estado thread_local), así que solo difiere la capa de datos. Un backend mínimo y fiel es un mapa en memoria, indexado correctamente:

#![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 modela filas no atribuidas.
type Table = HashMap<(String, Option<String>), Row>;
}

Cada método requerido es entonces una implementación directa:

  • store-entry inserta en (key, None) con el espacio de nombres "default".
  • store-with-agent inserta en (key, agent-id) con el espacio de nombres y la importancia del llamador.
  • recall filtra por subcadena/rango contra content, aplica el filtro de sesión, aplica límites RFC 3339 inclusivos contra timestamp, ordena y trunca a limit. Maneja la consulta vacía/* como más recientes primero.
  • recall-for-agents añade el recorrido de filtro de agente sobre el segundo componente de la clave.

Un backend real puede intercambiar el mapa por un almacén embebido sin cambiar la forma del contrato. El adaptador de memoria intencionalmente no enlaza wasi:http aún, incluso cuando su ámbito lleva http_client; los backends remotos requieren el límite memoria-red separado y probado a nivel de componente.

Lo que hace el host a tu alrededor

Conocer el comportamiento del adaptador en wasm_memory.rs explica varios límites del contrato:

  • Almacén cálido, reabastecido en cada llamada. Igual que channels: una instancia durante toda la vida del plugin, combustible nuevo en cada llamada, llamadas serializadas detrás de un mutex. Tu backend nunca ve llamadas concurrentes.
  • Capacidades almacenadas en caché al cargar. El host lee tus flags una sola vez durante from_wasm y nunca más. No hay detección dinámica de capacidades; reiniciar el daemon es lo que las vuelve a leer.
  • Un trap termina el backend. Cada punto de llamada envuelve los traps con un contexto con nombre (memory.recall-namespaced trapped, etc.). Un trap o el agotamiento del fuel deja un store que Wasmtime se niega a volver a ejecutar, y un plazo vencido interrumpe tu código a mitad de ejecución; en todos los casos el host descarta tu instancia y cada llamada posterior que llegue a ella falla con plugin instance is unavailable. Lo mismo ocurre con un resultado que el host no puede deserializar, como uno que copie más de los 128 MiB por defecto de Wasmtime desde la memoria del guest en una sola llamada. El host no la reconstruye: tus entradas viven solo en esa instancia, y una nueva respondería como si nada se hubiera almacenado. Devuelve err(string) para los fallos esperados en lugar de entrar en pánico; un error que devuelves conserva la instancia.
  • Los valores de reserva posteriores al filtrado son del lado del host. Cuando tu marca recall-namespaced no está establecida, el filtro de espacio de nombres se ejecuta en el host después de que tu recall haya devuelto el resultado. El manejo de limit interactúa con eso: el host pasa el límite del llamante a recall, así que el posfiltrado puede dejar el resultado por debajo del límite. Otra razón para implementar de forma nativa las variantes con espacio de nombres.

Manifiesto, compilación, instalación

El manifest es el archivo llamado manifest.toml en el directorio del plugin. Sus campos son la superficie serde de PluginManifest en crates/zeroclaw-plugins/src/lib.rs, que es la fuente de verdad:

CampoObligatorioSignificado
nameSometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence.Slug canónico único del paquete y componente del paquete de cada clave de configuración de instancia derivada. No es en sí una clave de configuración del operador. Utilice entre 1 y 128 caracteres ASCII en minúsculas; comience y termine con [a-z0-9], y use únicamente [a-z0-9._-] entre ellos. El descubrimiento rechaza los nombres no válidos o duplicados.
versionSometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence.Cadena de versión, p. ej. 0.1.0.
descriptionnoDescripción legible para humanos mostrada por zeroclaw plugin list.
authornoNombre del autor u organización.
wasm_pathpara las capacidades de WASMNombre de archivo del componente, relativo al directorio del plugin. Obligatorio a menos que la única capacidad sea skill. La detección omite el plugin si el archivo especificado no existe.
wasm_sha256en modo estricto para las capacidades de WASMSHA-256 hexadecimal de los bytes exactos del componente. Cuando está presente, siempre se aplica; la política estricta de firmas lo exige para que el manifiesto firmado vincule el ejecutable admitido.
capabilitiessí, no vacíoQué es el plugin: cualquiera de tool, channel, memory, observer, skill (PluginCapability, serializado en snake_case).
providesnoPara un plugin de channel que es un drop-in de un canal compilado en el binario: el id de configuración en snake_case que replica (telegram, gmail_push). El host admite una instancia lógica por cada [channels.<id>.<alias>] configurado y habilitado que un agente habilitado enruta, de modo que la configuración canónica de canales sigue siendo el único lugar para esos ajustes. Requiere config_read. La familia plugin está reservada y se rechaza; un id que esta compilación no define se rechaza, y dos paquetes instalados que reclaman el mismo id se rechazan ambos. Ausente (el valor por defecto) significa un canal nuevo configurado desde [channels.plugin.<alias>]. Las réplicas se admiten, pero todavía no se construyen; consulte el plan de activación en crates/zeroclaw-runtime/src/plugin_runtime.rs.
permissionsnoServicios del host a los que el código puede acceder: http_client, config_read, state_read, state_write, file_read, file_write, memory_read, memory_write (PluginPermission). Los permisos de HTTP, configuración y estado de instancia exacta se aplican a los adaptadores de herramientas y canales; los permisos de sistema de archivos y memoria permanecen inactivos. Declarar config_read requiere config_schema.
config_schemaexactamente con config_readEl esquema JSON Draft 2020-12 para la configuración privada de este plugin se incluye en los bytes canónicos del manifiesto y, por tanto, queda cubierto cuando el manifiesto se firma. La raíz debe ser un objeto con un mapa properties y additionalProperties = false. Cada propiedad de nivel superior debe tener un único tipo explícito compatible, directamente o mediante un JSON Pointer local: string, boolean, integer, number, array u object. Los consumidores de herramientas y canales pueden establecer x-secret = true directamente en una propiedad string de nivel superior para eliminarla de la configuración pública y exponerla mediante la importación secrets.get con alcance limitado. Un nombre de propiedad secreta es un nombre ASCII portable de 1 a 128 bytes que solo contiene letras, dígitos, _, - o .. Las herramientas reciben la configuración pública bajo __config y pueden leer secretos durante execute. Los canales leen el objeto público actual mediante config.get y los secretos mediante secrets.get durante configure y en las llamadas operativas; ambas importaciones no están disponibles durante la instanciación ni durante el descubrimiento de metadatos estáticos. Se rechazan los marcadores secretos anidados, falsos o no booleanos, así como las propiedades secretas que no son de tipo string. Un esquema sin config_read, o un config_read sin esquema, se rechaza.
signaturenoFirma Ed25519 en Base64url sobre los bytes canónicos del manifiesto. Se establece al firmar para distribución.
publisher_keynoClave pública Ed25519 codificada en hexadecimal del firmante.
egressnoTabla opcional [egress] que contiene una lista hosts: los destinos que este plugin declara necesitar, como hosts exactos (api.example.com) o patrones de sufijo explícitos (*.example.com, que coinciden con subdominios pero no con el dominio raíz). Forma parte de los bytes canónicos del manifiesto, por lo que queda cubierta al firmarlo, y cambiarla requiere volver a firmar. Una gramática inválida rechaza el manifiesto completo durante el descubrimiento y durante la instalación. Ausente, vacía y “no declara nada” son el mismo estado.

Declare solo los permisos que el código realmente usa. Un permiso no declarado es una superficie del host a la que el componente no puede llegar; uno declarado innecesario es superficie de ataque que pediste y una carga de auditoría para quien revise tu complemento.

[egress] es una declaración, no una concesión. Nada en un manifiesto otorga acceso de red: la lista de permitidos que aplica el host es plugins.entries[].egress_hosts de the operador en la fila zpi1_… de la propia instancia, por lo que un componente sin firmar que escriba su propia tabla [egress] tampoco alcanza nada. Lo que aporta la declaración es la ceremonia de instalación: zeroclaw plugin install la siembra en una fila que crea e imprime lo que concedió, y nunca amplía una fila que ya existe. Véase Declaring and granting egress.

Los valores del operador siguen siendo cadenas en plugins.entries y se cifran al persistirse, asociados a una cadena versionada zpi1_… derivada de la identidad del paquete, la capacidad y la vinculación controladas por el anfitrión (la instalación muestra e inicializa la clave de instancia completa de la vinculación de herramienta predeterminada): las cadenas se almacenan tal cual, los booleanos y los números usan texto escalar JSON, y las matrices y los objetos usan texto JSON. Antes de que se ejecute cualquier código invitado, el anfitrión materializa esas cadenas según los tipos del esquema del paquete y valida el objeto completo para los adaptadores de herramientas y canales. Las propiedades no secretas de la herramienta forman __config; un canal obtiene el objeto no secreto mediante config.get. Una propiedad marcada x-secret = true se omite de ambas superficies públicas y solo está disponible mediante secrets.get("property") en un marco de servicio autorizado. Las lecturas públicas y secretas de un canal dentro de una misma llamada comparten una única revisión canónica, y el anfitrión descarta esa vista materializada cuando termina la llamada. Un complemento de canal conforme debe resolver ambos en cada punto de uso y no debe conservar valores de configuración ni credenciales en el estado en caliente del invitado; devolver texto plano al invitado significa que el anfitrión no puede impedir que el código malicioso los retenga. Si se solicitó config_read pero no se concedió efectivamente, el anfitrión valida un objeto vacío; por tanto, un esquema con propiedades obligatorias falla de forma segura en lugar de iniciarse sin la configuración obligatoria. Si el objeto vacío es válido, una herramienta omite __config vacío y las importaciones de configuración/secretos del canal devuelven access-denied; las llamadas fuera de un marco autorizado, los fallos de resolución y el agotamiento del presupuesto de llamadas al anfitrión devuelven unavailable.

Para un backend de memoria: capabilities que contenga memory. No solicites config_read todavía: la admisión requiere un esquema, pero el mundo de memoria actual no tiene ninguna exportación mediante la cual el host pueda entregar el objeto resultante. Tampoco dependas de http_client: una concesión por sí sola no puede ampliar el adaptador de memoria, que actualmente no expone ninguna superficie de red.

Instala el objetivo WASI Preview 2 una vez y, luego, compila el componente:

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

El componente se encuentra en target/wasm32-wasip2/release/<crate_name>.wasm (los guiones en el nombre del crate se convierten en guiones bajos). Cámbiale el nombre a lo que declare tu wasm_path en el manifiesto cuando ensambles el directorio del plugin.

[!IMPORTANTE] Los archivos .wasm y .cwasm compilados son artefactos binarios, a menudo de varios megabytes cada uno. No los incluyas en un árbol de código fuente de git sin Git LFS: cada recompilación confirmada como un blob plano infla el historial del repositorio de forma permanente, y las herramientas de git diff/revisión no pueden con ellos. Trátalos como cualquier otro resultado de compilación: añade target/ y *.wasm/*.cwasm a .gitignore, y distribúyelos mediante un artefacto de lanzamiento o un archivo del registro de plugins en su lugar. Si un artefacto realmente debe vivir en el árbol, registra el patrón con LFS (git lfs track "*.wasm") antes del primer commit.

Si el host de destino es una compilación solo de ejecución (sin backend JIT compilado), no puede compilar .wasm al cargar; en su lugar, deserializa un .cwasm precompilado. Precompílalo con una CLI de wasmtime cuya versión coincida con la del host y distribuye el .cwasm como el artefacto wasm_path. Un artefacto con versión no coincidente es rechazado por la comprobación de deserialización de wasmtime, no se carga incorrectamente de forma silenciosa.

Estos comandos necesitan un binario con el host de plugins compilado. Los archivos de release para los destinos de escritorio y servidor de 64 bits (Linux x86_64 y aarch64 con glibc o musl, macOS en Apple silicon e Intel, Windows x86_64) lo incluyen, y cada release ejecuta un plugin de cada uno de esos archivos antes de publicarse. Los dos archivos de ARM de 32 bits y el archivo experimental de Android no lo incluyen, como tampoco una compilación desde el código fuente con los valores predeterminados de Cargo (cargo install zeroclaw, install.sh --source o cargo build sin features): en ese caso zeroclaw plugin ... es un subcomando no reconocido y los plugins instalados nunca se detectan. Para esos casos, compila desde el código fuente con un backend de ejecución de plugins, por ejemplo cargo build --release --features plugins-wasm-cranelift. La página del protocolo indica el soporte de plugins para cada artefacto.

Cada complemento vive en su propio subdirectorio del directorio de plugins (predeterminado ~/.zeroclaw/plugins/, resuelto mediante plugins.plugins_dir), que contiene el manifiesto y el componente nombrado para coincidir con wasm_path:

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

Instala desde un directorio local (esto valida la forma del manifiesto y ejecuta la directiva de firmas antes de copiar nada):

zeroclaw plugin install ./my-plugin/

Habilita el sistema de plugins y confirma la detección:

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

zeroclaw plugin list y zeroclaw plugin info confirman que un paquete está instalado y se puede descubrir, pero el descubrimiento no implica la activación. plugins.enabled = true activa el host de plugins; las capacidades de herramientas y habilidades descubiertas automáticamente se cargan en tiempo de ejecución solo cuando plugins.auto_discover = true también está habilitado, y ese indicador es false de forma predeterminada (cierre ante fallos):

zeroclaw config set plugins.auto_discover true

Por sí solo, plugins.enabled = true te proporciona los canales que declares en [channels.plugin.<alias>] y ninguna herramienta ni habilidad de plugin: un paquete de herramientas o habilidades puede aparecer en zeroclaw plugin list y, aun así, no aportar nada en tiempo de ejecución. Las vinculaciones explícitas de canales reciben su nombre del operador en lugar de descubrirse automáticamente, por lo que no necesitan auto_discover; la opción controla únicamente las herramientas y habilidades descubiertas automáticamente.

Se omitió en el descubrimiento un plugin que faltaba en zeroclaw plugin list: consulta el registro de inicio para ver la advertencia de omisión (manifiesto mal formado, archivo wasm_path faltante o rechazo por la política de firmas).

Siguiente