Memory API
每个 Agent 有一份自己的记忆。Agent 里的 Gene(基因)用它保存每次运行之间学到的东西:用户的偏好、反复出现的模式、关于某项任务的知识,以及值得缓存的计算结果。
记忆属于 Agent,不属于任何一个 Gene。替换或更新 Agent 基因组里的 Gene,记忆原样保留。
每条记忆属于四类之一。类别决定它大概能留多久,以及 Agent 迁移时它会怎样。
| 类别 | 存什么 | 记忆满了会被自动清理吗 | Agent 迁到别的 binding 时会带走吗 |
|---|---|---|---|
PREFERENCE | Agent 学到的用户习惯和偏好 | 不会,从不 | 会 |
PATTERN | Agent 在多次运行中发现的模式 | 会,在 CACHE 之后,从最旧的开始 | 会 |
CONTEXT | 针对某项任务或某个领域积累的知识 | 会,在 PATTERN 之后,从最旧的开始 | 会 |
CACHE | 可以重新算出来的中间结果 | 会,最先清,从最旧的开始 | 可能不带 |
类别名在任何地方都用大写:TypeScript、JSON、WASM 宿主调用都一样。
每条记忆包含:
| 字段 | 含义 |
|---|---|
entryId | 条目的固定标识:Agent 标识与键名的 SHA-256。更新条目时标识不变。 |
category | PREFERENCE、PATTERN、CONTEXT 或 CACHE |
key | Gene 读写这条记忆时用的名字 |
value | 存的字节。在 JSON 里用 base64 编码。 |
createdBy | 创建这条记忆的 Gene 的 v2 标识(v2: 加 64 位十六进制;rotifer compile 和 rotifer wrap 会把它打印为 Gene ID (v2)) |
lastAccessedBy | 最近一次读或写它的 Gene 的 v2 标识。查询不会更新它。新建的条目里它和 createdBy 相同。 |
createdAt、updatedAt | 创建时间与最近修改时间(RFC 3339,UTC) |
accessPolicy | 谁可以读、写、分享它(见下文) |
retentionPolicy | 保留多久:必须正好是 { maxRetentionDays, autoDeleteOnTermination, anonymizationSupported } 三个字段,或者 null(跟随 Agent 的数据策略) |
每条记忆都有一份访问策略,分三部分:谁可以读(readScope)、谁可以改或删(writeScope)、能不能离开这个 Agent(sharePolicy)。
readScope 和 writeScope 各取以下之一:
| 范围 | 谁可以访问 |
|---|---|
CREATOR_GENE_ONLY | 只有创建这条记忆的 Gene |
SAME_DOMAIN | 与创建者处于同一功能域的 Gene |
ALL_GENES | Agent 里的所有 Gene |
DEPLOYER_CONFIGURED | 按 Agent 的数据策略决定。数据策略里目前还没有这一项设置,所以这一版一律拒绝;写入时设成它也会被拒绝 |
sharePolicy 取以下之一:
| 分享策略 | 含义 |
|---|---|
PRIVATE | 只留在这个 Agent 里 |
ANONYMIZED_SHARE | 去掉所有能追溯到用户的信息后可以分享 |
KNOWLEDGE_GENE | 可以编码成知识基因,以基因的形式分享 |
写入时不给访问策略,就按缺省值:readScope: SAME_DOMAIN、writeScope: SAME_DOMAIN、sharePolicy: PRIVATE。
写入还必须符合 Agent 的数据策略。不做配置时 Agent 拿到的缺省策略不支持匿名化,所以 ANONYMIZED_SHARE 和 KNOWLEDGE_GENE 的写入会被拒绝,直到部署者配置了支持匿名化的策略。retentionPolicy 要求保留的时间超过 Agent 策略允许的上限,也会被拒绝。
每次读、写、删之前都会先检查权限。没有权限的 Gene 会收到 MEMORY_ACCESS_DENIED,这次尝试也会被记录下来。
每个 Agent 有一份记忆配额,按所有键和值的 UTF-8 字节数合计。缺省是 10 MB;创建 Agent 时可以另设:
rotifer agent create my-agent --genes my-gene --memory-quota=50MB--memory-quota 填一个带 KB、MB 或 GB 的数,单位大小写都行(1 KB = 1024 字节),例如 512KB、1.5GB。不带单位、0、负数或者数值过大,都会被拒绝,Agent 不会被创建。
一次写入如果会超出配额,Agent 按下面的顺序腾地方,每一类都从最久没有更新的条目开始清(最近改写过的条目算新的):
CACHEPATTERNCONTEXT
PREFERENCE 不会被这样清掉。清完仍然放不下,这次写入失败,返回 MEMORY_QUOTA_EXCEEDED,什么都不写。
- 单条
value最大 1 MiB(1,048,576 字节)。 - 一次查询最多返回
limit条(最多 1000 条),总大小最多 4 MiB。符合条件的条目超过 4 MiB 时,查询失败,返回MEMORY_INVALID_INPUT:请调小limit或收窄过滤条件。不会悄悄只返回一部分。
条目保留多久
Section titled “条目保留多久”条目的 maxRetentionDays 会真的执行:到 updatedAt 加上这么多天的那一刻(精确到秒),条目就过期,所以重新写入一条记忆会重新计时。条目自己的策略里 maxRetentionDays 为 null 时,这一条永不过期。retentionPolicy 整个为 null 的条目则跟随 Agent 的数据策略;缺省的数据策略不设上限,所以默认情况下这样的条目也永不过期。
过期的条目就等于没了:读返回 null,查询不会列出它,它不再占配额,再写同一个键会新建一条。过期条目会在下一次读、查、写、删这个 Agent 的记忆,或用 rotifer agent memory list 查看时被清除,每清除一条都会在审计日志里记一行。
autoDeleteOnTermination 会被保存和校验,但这一版不会触发:目前还没有终止或删除 Agent 的命令。
哪些 Gene 能用记忆
Section titled “哪些 Gene 能用记忆”- 以 Node.js 方式运行的 TypeScript Gene(Wrapped、Hybrid,或者没编译过的):通过
express()的第二个参数拿到ctx.memory,用法见下面的示例。各种组合方式里都是这样。Par、Cond、Try组合里只要有一个 Gene 没编译,rotifer agent run就会按顺序把组合里的每个 Gene 都跑一遍,所以它们都能读写记忆。 - 自带 WASM 的 Gene(Rust、AssemblyScript、手写 WAT):调用在 WASM 模块里调用里的宿主函数。不管是单独运行、按顺序组合,还是在全部 Gene 都编译过的
Par、Cond、Try组合里,rotifer agent run都会把它们接到所在 Agent 的记忆上。每个 Gene 以自己的 v2 标识读写;没有 v2 标识的 Gene,每次调用都返回-2。 - 编译成 WASM 的 TypeScript Gene 用不了记忆。 编译后的 Gene 调用
express()时只传入输入,没有ctx.memory。需要记忆的 TypeScript Gene,请以 Node.js 方式运行。
Gene 要有 v2 标识才能用记忆;没有 v2 标识的 Gene,每次调用都会返回 MEMORY_ACCESS_DENIED。
只有作为 Agent 的一部分运行时,记忆调用才有效。用 rotifer run 脱离 Agent 单独运行一个 Gene,所有记忆调用都会返回 MEMORY_ACCESS_DENIED(WASM 侧是 -2)。
想试一试用到记忆的 Gene,就跑 rotifer test。它会在测试期间给这个 Gene 一份空的、专用的 Agent 记忆,测完就删掉,不会动到你的 Agent 的记忆。Gene 带测试套件时,套件里的用例共用这一份记忆,后面的用例能读到前面用例写进去的内容。
readMemory(key: string, category?: MemoryCategory): MemoryEntry | null
writeMemory( key: string, value: Uint8Array, category: MemoryCategory, accessPolicy?: MemoryAccessPolicy, retentionPolicy?: RetentionPolicy | null,): MemoryEntry
queryMemory(filter?: MemoryFilter): MemoryEntry[]
deleteMemory(key: string): void
interface MemoryFilter { category?: MemoryCategory; keyPrefix?: string; // 纯前缀,不是匹配模式 since?: string; // RFC 3339,含,比较 updatedAt until?: string; // RFC 3339,不含,比较 updatedAt limit?: number; // 缺省 100,最多 1000}
interface RetentionPolicy { maxRetentionDays: number | null; // 整数天数,1 到 2147483647;null 表示不限 autoDeleteOnTermination: boolean; anonymizationSupported: boolean;}这四个调用都是同步的,直接返回结果,所以 express() 不需要写成 async。
readMemory返回key对应的条目,没有就返回null。传了category,就只匹配该类别的条目。条目存在但这个 Gene 无权读取时,调用失败,返回MEMORY_ACCESS_DENIED,不会假装条目不存在。writeMemory新建条目;键已存在时替换它的值,返回存好的条目。不传accessPolicy就用上面的缺省值,不传retentionPolicy就跟随 Agent 的数据策略。替换已有条目要有这条记忆的写权限,替换后原创建者和原访问策略保持不变;只有创建这条记忆的 Gene 才能修改它的访问策略或保留策略。其他 Gene 如果带了和原来不同的策略,会收到MEMORY_ACCESS_DENIED,什么都不写;不带策略,或者带的和原来完全一样,就照常写入。传retentionPolicy: null表示把条目改回跟随 Agent 的数据策略,这同样只有创建者能做。queryMemory返回这个 Gene 有权读取、且满足所给全部过滤条件的条目,最近更新的排在前面,最多limit条。不传过滤条件就返回这个 Gene 能读的全部条目,同样最多limit条。没权限读的条目直接略过,不会报告。过滤条件里出现未定义的字段,返回MEMORY_INVALID_INPUT。deleteMemory删除条目。只有有权写这条记忆的 Gene 才能删。
| 错误码 | 什么时候出现 |
|---|---|
MEMORY_NOT_FOUND | 条目不存在(比如删除一个从没写过的键) |
MEMORY_ACCESS_DENIED | 条目的访问策略不允许这个 Gene 读、写或删;非创建者想修改条目的策略;或者这次写入不符合 Agent 的数据策略 |
MEMORY_QUOTA_EXCEEDED | 清理之后仍然放不下;什么都没写 |
MEMORY_INVALID_INPUT | 键名不合法(为空、含 NUL 字符,或超过 1024 字节)、类别或范围的值不认识、值不是字节或超过 1 MiB、保留策略不是正好那三个字段、或字段类型不对(null 除外)、或 maxRetentionDays 不是 1 到 2147483647 之间的整数、过滤条件里有未定义的字段,或者查询结果超过 4 MiB |
MEMORY_STORAGE_ERROR | 记忆存储出了故障,或者 CLI 的原生绑定不支持记忆(“native binding unavailable”)。记忆不会退回到别的存储。 |
在 TypeScript 里,调用失败会抛出 MemoryError,它的 code 就是上表中的一个。在 WASM 沙箱里,这些错误和其他宿主调用走同一条错误通道返回。
在 WASM 模块里调用
Section titled “在 WASM 模块里调用”自带 WASM 的 Gene 从 rotifer 模块导入 readMemory、writeMemory、queryMemory、deleteMemory。它们和其他 rotifer 宿主函数是同一种形状:
(req_ptr: i32, req_len: i32, out_ptr: i32, out_cap: i32) -> i32请求和响应都是 JSON,字段名和大写取值与本页一致;value 用 base64。返回值大于等于 0 时,是写进 out 的响应字节数;返回负数表示出错:
| 返回值 | 含义 |
|---|---|
-1 | out 放不下响应 |
-2 | MEMORY_ACCESS_DENIED |
-8 | MEMORY_QUOTA_EXCEEDED |
-10 | MEMORY_INVALID_INPUT |
-12 | MEMORY_NOT_FOUND |
-13 | MEMORY_STORAGE_ERROR |
-4 | 这次运行的截止时间或这次调用的预算已用完 |
除 -4 以外,out 有空间时还会带上 {"__rotifer_error":{"code":"MEMORY_…","message":"…"}}。Gene 把这个对象原样作为自己的输出返回,就等于报告输入被拒绝。对不存在的键调用 readMemory 返回 JSON null,不算错误。
示例:记住用户偏好的 Gene
Section titled “示例:记住用户偏好的 Gene”这个格式化 Gene 会记住用户上次要求的缩进宽度;下次请求没有指定时,就用记住的那个。
import type { GeneMemoryAccess } from "@rotifer/playground/memory";
// reindent() 是这个 Gene 自己的格式化逻辑,这里省略。const KEY = "formatter/indent";
export function express( input: { code: string; indent?: number }, ctx: { memory: GeneMemoryAccess },): { formatted: string; indent: number } { let indent = input.indent;
if (indent === undefined) { const saved = ctx.memory.readMemory(KEY, "PREFERENCE"); indent = saved ? Number(new TextDecoder().decode(saved.value)) : 2; } else { ctx.memory.writeMemory( KEY, new TextEncoder().encode(String(indent)), "PREFERENCE", ); }
return { formatted: reindent(input.code, indent), indent };}这条记忆按缺省访问策略写入,所以同一功能域里的其他格式化 Gene 也能读到这个偏好。把这个 Gene 换成同一功能域里更好的格式化 Gene,新 Gene 会在原处找到之前存下的缩进设置。
记忆的每一次变动都会在 Agent 的审计日志 .rotifer/audit.jsonl 里追加一行:写入、更新、删除、为腾地方被清掉的条目、过期被清除的条目,以及因空间不足被拒绝的写入。这一行在变动保存之前写;写不进去,这次变动就不做。普通的读取不记。被拒绝的访问也会在那里记一行,并在 .rotifer/telemetry.jsonl 里记一条事件;设了 ROTIFER_TELEMETRY=0 或 DO_NOT_TRACK 时不写这条事件,审计那一行照写。两个文件都只留在你的机器上,不会通过网络发送。
不可信输入与记忆投毒
Section titled “不可信输入与记忆投毒”如果 Gene 把来自 Agent 外部的内容(消息、issue、网页、文档)写进记忆,写那段内容的人就能影响记忆里存什么。有大模型参与时,提示注入可以让 Gene 存下攻击者想要的条目,之后的运行会像读 Agent 自己学到的东西一样读到它们。
PREFERENCE 条目永远不会为腾地方被清掉。被污染的偏好会一直留着,直到被删除(部署者用 rotifer agent memory delete,或由有权修改它的 Gene 删除),或者它的 maxRetentionDays 到期。
- 只用可信来源的内容写
PREFERENCE条目。 - 不可信的输入放进
CACHE,或者给它设maxRetentionDays,让它到期自动清除。
部署前要知道的几点
Section titled “部署前要知道的几点”- Gene 升级后标识会变。 旧版本用
CREATOR_GENE_ONLY写下的条目,新版本读不到、改不了、也删不掉。希望跨版本保留的数据,请用缺省的SAME_DOMAIN;留下来的旧条目,用rotifer agent memory delete清理(见下文)。 PREFERENCE条目不会因为空间不够被清掉。 一个 Gene 不停地写偏好,可能把配额写满,之后的写入都会返回MEMORY_QUOTA_EXCEEDED。请给 Agent 设合适的配额,并用rotifer agent memory delete删掉不再需要的偏好。- 清理不看条目是谁写的。 某个 Gene 写入需要空间时,最旧的
CACHE、PATTERN、CONTEXT条目先被清,不管它们是哪个 Gene 创建的。
查看和清理 Agent 的记忆
Section titled “查看和清理 Agent 的记忆”rotifer agent memory list <agent> [--category <类别>]rotifer agent memory delete <agent> (--key <键名> | --category <类别>)list列出每条记忆的键名、类别、大小、创建者和最近更新时间,不显示值。delete按键名删除一条,或者删除某个类别的全部条目。--key和--category必须给且只给一个;删完会显示删了几条。删除不存在的键会返回MEMORY_NOT_FOUND。Agent 归你所有,所以不受条目访问策略的限制;升级后 Gene 再也够不着的条目,也用它清理。每次删除都会以部署者的身份记进审计日志。- 没有修改条目的命令,云端这一版也没有对应的功能。
Agent 迁移
Section titled “Agent 迁移”Agent 迁到别的 binding 时,PREFERENCE、PATTERN、CONTEXT 条目会一起带走,访问策略、保留策略和 createdBy 都不变。CACHE 条目可以留下不带,到新环境再重新算。
这是协议规定的迁移方式。这个版本还没有把 Agent 或它的记忆迁到别的 binding 的命令,记忆也不能上传到 Rotifer Cloud。