Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

编写内存插件

记忆插件是一种存储后端:它持久化代理记住的内容并回答回忆查询。它是数据模型负担最重的插件类型。工具有一个函数,channel 有一种消息形状,而记忆后端则有多代理归属、命名空间、会话、类别和重要性权重,并且运行时的记忆语义(作用域回忆、GDPR 导出、替代)依赖于你的实现正确处理行模型。

本指南假设你已了解 工具插件 的基础知识,以及来自 channel guide 的 warm-store 生命周期。它已针对 wit/v0/memory.witcrates/zeroclaw-plugins/src/wasm_memory.rs 中的主机适配器进行校验。

接入状态。 WasmMemory 已针对 memory-plugin world 实现运行时完整的 Memory trait,并已进行能力门控和单元测试覆盖。运行时尚未将其构造为可配置后端;宿主端还缺少与内存对应的 channel_plugin_details()。与通道一样,应依据契约进行构建:真正固定下来的是 WIT world 和适配器语义。内存 world 也尚未导出配置,因此不要请求 config_read;只有在将类型化配置 ABI 和解析器接入 WasmMemory 后,才添加该功能。

数据模型

一种记录类型会在两个方向上跨越边界,memory-entry (memory.wit)。在设计存储之前先将其字段内部化,因为这些可选方法都只是对它们的视图:

字段含义
id行标识。
key查找键。不唯一:多行可能共享一个键,每个 agent 一行。你的存储必须以 (key, agent-id) 为键,而不是 key
content记住的文本。
categorycore(长期事实)、daily(会话日志)、conversation(上下文)或 custom(string)
timestampRFC 3339 创建时间。时间范围回溯边界是包含性的。
session-id可选会话范围。
namespace代理或上下文之间的隔离边界。
score检索相关性 0.0-1.0;非向量召回时使用 none
importance可选的优先级权重 0.0-1.0。
superseded-by替换此项的条目 ID(如果有)。
agent-alias / agent-id显示名称与原始存储标识符。使用 agent-id 进行作用域相等性检查,使用 agent-alias 进行显示。

(key, agent-id) 组合是最常见的错误。基础 get 契约明确说明:当多行共享同一个 key 时,返回的是任意一个匹配的行,而按 agent 范围的查找走的是 get-for-agent。同样,forget 会删除某个 key 的所有行,而不管归属如何;而 forget-for-agent 只会删除恰好对应 (key, agent-id) 的那一行,并保留其他同级行。

必需的导出

有十二个函数没有默认值,且必须实现(memory.wit,required-methods 部分):

导出契约说明
name后端名称。
get-memory-capabilities可选方法的位掩码;在加载时读取一次。
store-entry存储 (key, content, category, session-id)。之所以命名为 store-entry,是因为 store 在 wit-bindgen 中是保留字。
recall查询 + 限制 + 可选会话和 RFC 3339 时间边界(含)。空查询或仅为 * 的查询表示仅按时间检索:返回最近的条目。
get按键;多代理键冲突时的任意行。
list-entries可选的类别和会话筛选器。命名为 wit 保留的 list
forget删除键的所有行;如果删除了任何内容,则为 true
forget-for-agent仅删除 (key, agent-id) 行。
count总条目。
health-check可达性。
store-with-agent / recall-for-agents感知归因的成对项;见下文。

recall-for-agents 接受一个 agent-filter 变体:all(不筛选 agent)或 some(list<string>)(限制为列出的 agent ID)。运行时将其 Rust &[&str] 切片映射为“空切片表示 all”,因此应将 some([]) 视为匹配不到任何内容,而不是匹配所有内容。

能力标志:11 个可选方法

与通道相同:宿主只读取一次 get-memory-capabilities,对于每个未设置的标志,它会使用 Rust trait 默认值,而不是调用你。默认值在 memory.wit 中紧挨着这些标志以内联注释的形式记录,宿主侧的回退逻辑可在 wasm_memory.rs 中看到(每个受保护的方法都会检查该标志,并在缺失时走回退路径):

标志未设置时使用主机回退
get-for-agent主机组合 get + agent-id 相等过滤器
purge-namespace, purge-session, purge-session-for-agent, purge-agentHost 返回“not supported”
reindex主机返回 0
store-procedural主机无操作
ensure-agent-uuid主机会原样回显别名
recall-namespacedHost 调用 recall 并按命名空间进行后过滤
export-entries主机调用 list-entries 并在之后进行过滤
store-with-metadataHost 委托给 store-entry,丢弃 namespace 和 importance

请把最后一行读两遍:如果你不实现 store-with-metadata,运行时请求的命名空间和重要性会被回退机制悄悄丢弃。存储命名空间数据的后端应当成套实现 store-with-metadatarecall-namespacedpurge-namespace,否则命名空间隔离会悄然退化为后过滤和有损写入。

purge 家族是你的数据删除入口。purge-agent 接受的是 agent-alias(不是 ID);export-entries 用于 GDPR 第 20 条的数据可携性,且必须返回按创建时间升序排列、排除 embeddings 的 entries。如果你的后端提供真实用户数据,请实现 purge 和 export 标志;只有对于一次性后端,“not supported” 才是可接受的回答。

Sketch:存储形状

组件模式采用的是 channel 模式(warm 实例、thread_local 状态),因此只有数据层不同。一个最小且诚实的后端实现是内存中的 map,并使用正确的键:

#![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 表示未归因的行。
type Table = HashMap<(String, Option<String>), Row>;
}

然后每个必需的方法都是一次直接的遍历:

  • store-entry 以命名空间 "default" 插入 (key, None)
  • store-with-agent 使用调用者的命名空间和重要性在 (key, agent-id) 处插入。
  • recallcontent 进行子串/排序筛选,应用会话筛选器,对 timestamp 应用包含式 RFC 3339 边界,排序,并截断到 limit。将空/* 查询按最近优先处理。
  • recall-for-agents 在键的第二个组件上添加 agent-filter 遍历。

真实的后端可以在不改变契约结构的情况下,将 map 替换为嵌入式存储。内存适配器有意不链接 wasi:http,即使其作用域携带 http_client;远程后端需要经过独立组件测试的内存网络边界。

主机在你周围所做的事情

了解 wasm_memory.rs 中的适配器行为,有助于解释几个合约边缘情况:

  • 温存储,每次调用重新加油。 与 channels 相同:插件生命周期内一个实例,每次调用补充新燃料,调用在互斥锁后串行化。你的后端永远不会看到并发调用。
  • 能力在加载时缓存。 主机仅在 from_wasm 期间读取一次你的标志,此后不再读取。没有动态能力发现;重启守护进程才会重新读取它们。
  • 陷阱包装。 每个调用点都会用一个命名上下文包装陷阱(memory.recall-namespaced trapped 等)。某个调用中的陷阱不会拆除插件,但重复的陷阱会使后端失去作用;对于预期的失败,返回 err(string),而不是 panic。
  • 后置过滤回退在主机端执行。 当你的 recall-namespaced 标志未设置时,命名空间过滤会在你的 recall 返回后在主机端运行。你的 limit 处理会与此交互:主机把调用方的 limit 传给 recall,因此后置过滤可能会使结果不足。这也是要原生实现 namespaced 变体的另一个原因。

清单、构建、安装

manifest 是插件目录中名为 manifest.toml 的文件。其字段是 crates/zeroclaw-plugins/src/lib.rsPluginManifest 的 serde 表面,这是唯一的事实来源:

字段必填含义
name唯一的规范包 slug,也是每个派生实例配置键中的包组件。它本身不是操作符配置键。使用 1–128 个小写 ASCII 字符;以 [a-z0-9] 开头和结尾,中间只能包含 [a-z0-9._-]。发现过程会拒绝无效或重复的名称。
version版本字符串,例如 0.1.0
descriptionzeroclaw plugin list 显示的人类可读描述。
author作者姓名或组织。
wasm_path用于 WASM 能力组件文件名,相对于插件目录。除非唯一的 capability 是 skill,否则为必填。如果指定的文件不存在,发现过程会跳过该插件。
capabilities是,非空插件类型:toolchannelmemoryobserverskill 中的任意一个(PluginCapability,序列化为 snake_case)。
permissions代码可能访问的主机服务:http_clientconfig_readfile_readfile_writememory_readmemory_writePluginPermission)。目前仅前两项会被强制执行;其余项虽会被接受,但不起作用。声明 config_read 需要 config_schema,目前只有工具/通道适配器会提供它。
config_schema恰好使用 config_read为此插件的私有配置起草 2020-12 JSON Schema;它会包含在规范清单字节中,因此在清单签名时也会受到保护。根必须是一个包含 properties 映射且 additionalProperties = false 的对象。每个顶层属性都必须具有一个明确的受支持类型,可直接指定,也可通过本地 JSON Pointer 指定:stringbooleanintegernumberarrayobject。工具和通道使用者可以直接在顶层字符串属性上设置 x-secret = true,以便将其从公共配置中移除,并通过带作用域的 secrets.get 主机导入来公开。工具会在 execute 期间通过 __config 接收公共配置,并可以读取机密。通道会在 configure 和操作调用期间通过 config.get 读取当前公共对象,并通过 secrets.get 读取机密;这两个导入在实例化和静态元数据发现期间均不可用。嵌套、值为 false 或非布尔值的机密标记,以及非字符串的机密属性都会被拒绝。没有 config_read 的 schema,或没有 schema 的 config_read,都会被拒绝。
signature对规范化清单字节进行的 Base64url Ed25519 签名。用于发布签名时设置。
publisher_key签名者的十六进制编码 Ed25519 公钥。

只声明代码实际使用的权限。未声明的权限是组件无法触达的宿主表面;不必要声明的权限则是你主动增加的攻击面,也是审查你的插件的人需要承担的审核负担。

操作方提供的值在 plugins.entries 中仍保持为字符串,并会在持久化时加密,以一个由宿主拥有的包、能力和绑定标识派生的版本化 zpi1_… 字符串作为键(安装过程会打印并初始化默认工具绑定的完整实例密钥):字符串按原样存储,布尔值和数字使用 JSON 标量文本,数组和对象使用 JSON 文本。在任何来宾代码运行之前,宿主会将这些字符串物化为包模式规定的类型,并针对工具和频道适配器验证完整对象。非机密工具属性构成 __config;频道通过 config.get 获取非机密对象。标记为 x-secret = true 的属性会从两个公共接口中省略,仅可在授权的服务帧中通过 secrets.get("property") 获取。频道在一次调用中的公共和机密读取共享同一个规范修订版本,调用结束时宿主会丢弃该物化视图。符合要求的频道插件 必须 在每个使用点都解析这两类值,并且不得在来宾的热状态中保留配置或凭据值;将明文返回给来宾意味着宿主无法针对恶意代码强制实施不保留策略。如果请求了 config_read 但未实际获授予,宿主会验证一个空对象;因此,包含必需属性的模式会拒绝启动,而不会在缺少必需配置的情况下启动。如果空对象有效,工具会省略空的 __config,而频道的配置/机密导入会返回 access-denied;在授权帧之外的调用、解析失败以及宿主调用预算耗尽会返回 unavailable

对于内存后端:capabilities 包含 memory。暂时不要请求 config_read:准入需要一个模式,但当前内存世界没有可供主机交付生成对象的导出机制。也不要依赖 http_client:仅授予权限无法扩展内存适配器,而该适配器目前不提供任何网络接口。

先安装一次 WASI Preview 2 目标,然后构建组件:

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

该组件位于 target/wasm32-wasip2/release/<crate_name>.wasm(crate 名称中的连字符会变成下划线)。在组装插件目录时,请将其重命名为你的清单中 wasm_path 声明的名称。

[!IMPORTANT] 编译后的 .wasm.cwasm 文件是二进制构件,通常每个都有数 MB。不要在没有 Git LFS 的情况下将它们提交到 git 源树中:每次重建都作为普通 blob 提交,会永久膨胀仓库历史,并且 git diff/审查工具会因此卡住。把它们当作其他构建输出一样处理:将 target/*.wasm/*.cwasm 添加到 .gitignore,并改为通过发布构件或插件注册表归档分发。如果某个构件确实必须留在树中,请在首次提交之前使用 LFS 跟踪该模式(git lfs track "*.wasm")。

如果目标主机是仅运行时构建(未编译 JIT 后端),它在加载时无法编译 .wasm;而是反序列化预编译的 .cwasm。请使用与主机版本匹配的 wasmtime CLI 进行预编译,并将 .cwasm 作为 wasm_path 构件分发。版本不匹配的构件会被 wasmtime 的反序列化检查拒绝,而不会被静默错误加载。

这些命令需要一个编译了插件宿主的二进制文件。 安装程序附带的预构建发布二进制文件在构建时未启用 plugins-wasm 特性,因此 zeroclaw plugin ... 在该版本中是无法识别的子命令,已安装的插件也永远不会被发现。请从源码构建并选择一个插件执行后端,例如 cargo build --release --features plugins-wasm-cranelift

每个插件都位于 plugins 目录的各自子目录中(默认 ~/.zeroclaw/plugins/,通过 plugins.plugins_dir 解析),其中包含清单以及与清单的 wasm_path 匹配的组件:

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

从本地目录安装(这会在复制任何内容之前验证清单形状并运行签名策略):

zeroclaw plugin install ./my-plugin/

启用插件系统并确认发现:

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

zeroclaw plugin listzeroclaw plugin info 可确认软件包已安装且可被发现,但发现并不等于激活。plugins.enabled = true 会开启插件主机;只有同时将 plugins.auto_discover = true 设为 true 时,自动发现的工具和技能功能才会在运行时加载,而该标志默认为 false(故障关闭):

zeroclaw config set plugins.auto_discover true

因此,仅设置 plugins.enabled = true 时,会启用你在 [channels.plugin.<alias>] 下声明的通道,而不会启用任何插件工具或技能:工具或技能包可能会出现在 zeroclaw plugin list 中,但在运行时不会产生任何作用。显式通道绑定由操作员命名,而不是自动发现,因此不需要 auto_discover;该标志仅控制自动发现的工具和技能。

zeroclaw plugin list 中缺失的插件在发现时已被跳过:请检查启动日志中的跳过警告(清单格式错误、缺少 wasm_path 文件,或签名策略拒绝)。

下一个