为什么选择 WOS

面向 AI 智能体的长期记忆。

WOS 是一个记忆 API。您只需存储一次用户的记忆,之后每次查询只召回其中相关的部分,并将其传入模型的提示词。

检索完全基于语义,不使用关键词或 BM25 匹配,因此各语言的召回质量完全一致。无论您存储了多少内容,每次查询都只返回一小段大小受限的上下文,并且绝不会在您存储的记忆上运行任何模型。

核心操作

  • store - 为用户保存一条记忆。
  • recall - 获取与查询相关的记忆。这是最主要的调用。
  • search - 对已存储记忆进行原始语义搜索。
  • supersede - 更新或替换已过时的记忆。
  • forget - 删除单条记忆或整个用户的数据(GDPR)。
从左侧选择一个章节 查看各主题的详细说明。
模型

三个模型,一脉相承。

WOS 的模型以人类历史上保存知识的载体命名 - Tablet(石板)、Scroll(卷轴)、Book(册页)。石板、卷轴、装订成册的书:每一代都能为您的智能体做得更多。

Tablet

已上线
铭刻于石 · store 与 recall

一种精简、快速、低成本的记忆写入与召回方式,是所有模型共同的基础。

Scroll

已上线
展开卷轴 · LLM 辅助召回

加入一个语言模型来更仔细地解读您的问题,并带回更完整的上下文,让分散的线索汇聚成整体,而不是总缺一块。

Book

即将推出
装订并编目 · 自主路由

自动翻到正确的一页 - 在每个时刻自行选择所需的记忆与工具,并且越用越敏锐。

Tablet 1 的完整基准测试报告见基准测试页面

成本

付给我们 $2,在您的 LLM 上省下数倍的开销。

WOS 每次查询只向您的 LLM 提供约 1,200 个 token(一段大小受限且高度相关的切片),而不是把完整历史塞进每个提示词。两者差距巨大,且随历史增长而不断扩大。

每 1,000 次查询的 LLM 成本 基于 Tablet 1
用户历史100K
每月查询次数1,000
您的 LLM
45× 更便宜,每月节省 $244
不使用 WOS$250.00
使用 WOS$5.50

在 WOS 上每花 $1,就能在 LLM 上节省 ~$98。历史越大、模型越贵 → 回报越高。

节省从何而来

  • 不使用 WOS 时,您需要把完整历史塞进每个提示词:按 GPT-4o 输入价格计算,每次查询 100K tokens × $2.50/1M = $0.25(Opus 级模型约为其 2 倍)。
  • 使用 WOS 时,您只需一次性写入($2/1M),之后每次查询只是一次极小的检索($3/1M × 1,200),加上您的 LLM 只需读取约 1,200 个 token。
  • 您的 LLM 读取的 token 越少,您付的钱就越少,而 WOS 会让这个数字在记忆增长时保持平稳。
上下文压缩比 = 历史 ÷ 提供的 token 数,而非成本(上方计算器已对每次检索计价)。 25K → 21× · 100K → 83× · 200K → 167×。
多语言

每种语言,同样的准确率。

检索是纯语义的 - 只用向量嵌入,完全不做关键词或 BM25 匹配。因此无论您的用户使用日语、中文、西班牙语还是英语,召回质量都完全相同。

BM25 这类词法匹配是针对特定语言的形态、分词和书写系统调校的。在多语言存储中,这意味着检索质量会因语言而异。WOS 完全不使用词法匹配,因此每种语言都走同一条路径。

一个存储库,同时容纳三种语言

您无需为存储库指定语言 - 可以自由混用。下例中,同一位用户的记忆同时包含日语、英语和西班牙语,而无论提问用什么语言,每个问题都能找到正确的记忆。这是对线上 API 的一次真实交互:

# one user, three languages stored together
mem.add("彼女はコーヒーより紅茶が好き", user_id="alice")                      # Japanese
mem.add("she works at a design studio in Brooklyn", user_id="alice")       # English
mem.add("A ella le encanta hacer senderismo los sábados", user_id="alice")  # Spanish
真实结果 - 每个问题都跨到了另一种语言
"¿Qué bebe ella?"               -> 彼女はコーヒーより紅茶が好き
"what does she do on weekends?" -> A ella le encanta hacer senderismo los sábados
"彼女の仕事は?"                  -> she works at a design studio in Brooklyn

没有翻译步骤,没有语言检测,没有按语言的配置。记忆与问题按语义而非语言归位 - 只要语义匹配,语言无关紧要。

这里只展示三种语言,纯粹是版面所限 - 并不存在什么“支持语言列表”。同一项在线测试同样通过了中文、Русский 和 العربية 记忆的验证,全部针对生产环境 API 完成。

我们为什么刻意禁用关键词

BM25 之类的词法打分会让某些语言的检索得到比其他语言更多的强化,当一个存储库容纳多种语言时就会造成偏差。因此我们把它从引擎中彻底移除,并在代码评审中强制执行这一规则:只要路径中存在任何词法打分,召回质量就会因语言而异。

LongMemEval 只包含英语,因此无法衡量多语言召回。上面的演示就是您直接对线上 API 验证这一点的方式。
架构

没有模型会读取您的记忆。

存储是逐字原样的,引擎通过向量嵌入进行搜索 - 便宜、快速、确定性。我们绝不会在您存储的记忆上运行模型。Tablet 完全不使用模型;Scroll 和 Book 在引擎之外加入了一个模型以获得更强的效果,但它只会看到您的查询,绝不会看到您存储的内容。

  • 确定性引擎。同一查询每次都返回相同的记忆 - 这正是我们基准测试的方差只来自阅读模型的原因。
  • 规模化下依然便宜。存储和检索没有生成成本,因此随着记忆增长,您的账单只与存储量相关,而不是模型用量。

您的原话,一字不改

一种常见设计是在写入时运行语言模型,从文本中提取并改写“事实”。这种设计要付出三重代价:每次写入的生成成本、额外的延迟,以及存下来的是模型的转述而非原话。WOS 做了相反的取舍 - 它原样存储所说的话,让您的 LLM 在读取时拿着原文进行解读。

WOS 不是什么:不是需要您自己运维的向量数据库,也不是需要自己组装的 RAG 框架。我们绝不在您存储的数据上运行模型 - 那条路径是纯向量嵌入。Scroll 和 Book 确实会使用语言模型以获得更强的效果,但它只会看到您的查询,绝不会看到您存储的记忆 - 也绝不会用您的数据训练或收集您的数据。
证明

67.5%,实测且可复现。

在 BEAM 1M 上取得 67.5%,为 5 次独立运行的平均值(σ 0.22%,无任何挑选),由 gpt-4.1-mini 使用该基准自带的评判提示词打分。

即使是同一个基准,分数也会随打分方式大幅变化:评判模型、提示词,以及允许检索层做什么。我们使用作者仓库自带的评判模型打分,原样使用他们的评判提示词,不为迎合测试改动任何东西,并公开测试框架、打分代码与阅读端提示词,任何人都能原样复现 67.5%。

评测协议,一表看全

项目我们的做法
数据集BEAM 1M - 35 段对话、74,630 轮、220 万条记忆、700 道题
评判gpt-4.1-mini,temperature 0,运行 BEAM 自带的评判提示词 - 作者仓库中的默认设置,而非我们挑选的评判模型
运行次数5 次独立运行,公布每一次的分数,报告均值(σ 0.22%)
阅读器固定的阅读模型与提示词,原文公开

诚实的保障:第三方评判、原样公开的阅读器提示词、纯语义检索,以及公布每一次运行的结果 - 而不是只报最好的一次。检索引擎是确定性的 - 再跑一次,返回的记忆完全相同。

我们攀登更难的基准

我们只在尚未攻克的最难的标准基准上测试 - 这个数字是所有 WOS 模型的最高水位线,每当更强的模型发布就会刷新。突破 94%,我们就毕业,转向更难的基准。

BEAM 1M进行中
Tablet67.5%
gpt-4.1-mini 评判 · 五次运行均值达到 94% 即毕业
早期基准测试 LongMemEval-S 已达标
Tablet95.7%
Scroll92.3%
GPT-4o 评判 · 所有 WOS 模型中的最佳成绩达到 94% 即毕业
查看完整报告
定价

每个模型两档 token 费率,
外加每次请求 $0.0001。

按每百万 token 计费,外加每次请求固定 $0.0001,按用量付费。没有订阅,没有存储租金,没有记忆上限。只有当您的智能体写入或读取时才付费 - 绝不为它记住的内容付费。

模型输入 / 1M输出 / 1M
Tablet$2$3已上线
Scroll$4$8已上线
Book--待定
  • 每次请求 $0.0001。在 token 用量之外,每次 API 调用收取固定费用。
  • 存储免费。写入只付一次费,保存不花一分钱。没有条数限制,没有保留期限。
  • 我们负责存储,但绝不用它训练、使用或查看。您智能体的记忆属于您 - 我们只负责组织它,让您能够检索。
  • Tablet 为什么这么便宜:它的引擎不运行任何模型,所以我们的成本只有向量嵌入和磁盘 - 而不是 GPU。Scroll 和 Book 加入了模型,这正是它们更高价格所覆盖的部分。
其他计费模式按存储量按月收费,或按套餐限制记忆条数。WOS 对已存储数据分文不收,无论数据量多大、存了多久。

按用量等级的速率限制 →

面向开发者

三次调用:存储、召回、回答。

一个 API。recall() 调用在一次往返中返回短期记忆、长期记忆和周边上下文,可直接放入您的提示词。

1

存储

add() 保存事实与对话:用户的话、助手自己的话(speaker "me"),或某个具名者的话。写入时完成嵌入,不调用 LLM。

2

召回

recall() 在一次调用中返回短期 + 长期 + 上下文 - 一段大小固定、有界的上下文。

3

回答

把这段有界的上下文交给您的 LLM - 任何提供商,用您自己的密钥。

from wontopos import Client
mem = Client(api_key="wos-...")
mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I suggested the jasmine tea", user_id="alice", speaker="me")  # its own words
# one call: short + long + context
ctx = mem.recall("what does alice drink?", user_id="alice")

记忆带有说话者。默认是用户的话,speaker "me" 存储助手自己说的话,而像 "Bob" 这样的名字会记住是用户身边的谁说的,让回忆可以按人来回答。

说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。
快速上手

5 分钟完成您的第一次召回。

一个密钥、一行安装命令、三次调用 - 您的智能体就有了记忆。本页每个代码片段都实际运行过;响应原样展示。

1

获取 API 密钥

控制台创建。以 wos-live- 开头的 155 字符密钥只显示一次。请保存在环境变量中 - 绝不要写进代码。

2

安装

pip install wontopos        # Python
npm install wontopos        # TypeScript / JavaScript
cargo add wontopos          # Rust
# curl - nothing to install, just set WOS_API_KEY
# latest: SDK v2.2.32 · MCP v1.0.15
3

创建存储库,然后存储与召回

存储库(store)就是您读写所依据的 user_id。存储库是显式的:先创建(下面的调用),再在其下存储和召回。存储 - 写入时即完成向量嵌入,不调用 LLM。召回 - 一次往返返回短期 + 长期 + 上下文。

from wontopos import Client

mem = Client(api_key="wos-live-...", user_id="alice")  # set the store once
mem.create_store()              # create it (stores are explicit)
mem.add("she prefers tea over coffee")  # no user_id needed

# one call → short-term + long-term + context
ctx = mem.recall("what does alice drink?")
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-...", userId: "alice" });  // set the store once
await mem.createStore();            // create it (stores are explicit)
await mem.add("she prefers tea over coffee");  // no userId needed

// one call → short-term + long-term + context
const ctx = await mem.recall("what does alice drink?");
use wontopos::Client;

let mem = Client::new("wos-live-...").with_user("alice");  // set the store once
mem.create_store(None).await?;            // create it (stores are explicit)
mem.add("she prefers tea over coffee", None, json!({})).await?;

// one call → short-term + long-term + context
let ctx = mem.recall("what does alice drink?", None).await?;
# create the store once - stores are explicit
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store - embedded on the way in, no LLM call
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'

# one call → short-term + long-term + context
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
真实响应 - create_store()
{"user_id": "alice", "status": "created"}
真实响应 - add()
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
只需设置一次存储库。user_id 传给客户端后,每个调用都会使用它 - 无需重复;给单个调用传入 user_id 即可覆盖。存储库是显式的:向不存在的存储库写入或从中召回会返回 404 - 请先创建。每个账户自带一个 default 存储库,因此完全不传 user_id 的零配置路径也能直接工作。列出与管理存储库见存储库

recall() 返回四个块 - short_term(最近的对话轮次)、long_term(相关记忆)、context(最佳匹配的前后文),以及一条告诉 LLM 如何使用它们的 instruction。整体放入您的提示词即可。

任何语言都适用。用英语存储,用韩语、日语或中文提问 - 都能召回同一条记忆。这是向量嵌入搜索,不是关键词匹配。

各语言的全部方法 →

同一客户端,不同设置

mem = Client.from_env()                 # reads WONTOPOS_API_KEY
scroll = mem.with_model("scroll-1.2")  # this copy only: another engine
alice  = mem.with_user("alice")       # this copy only: another default store
const alice = mem.withUser("alice");
const scroll = mem.withModel("scroll-1.2");
let alice = mem.with_user("alice");
let scroll = mem.with_model("scroll-1.2");
# curl has no copies — send model and user_id with each request
curl ... -d '{"user_id":"alice","model":"scroll-1.2","query":"…"}'
存储库

存储库 - 创建、列出、删除。

存储库(store)就是您读写所依据的 user_id - 每个最终用户、智能体或主题一个隔离的记忆空间。存储库是显式的:必须先创建,再向其中存储或从中召回,否则调用返回 404。每个账户自带一个 default 存储库,因此无需创建即可开始。

隔离如何嵌套。账户拥有多个工作区;每个工作区隔离自己的记忆、API 密钥和用量(计费在账户层面共享)。存储库位于工作区之内:同一工作区的密钥共享其存储库,不同工作区之间绝不互见彼此的记忆。account → workspace → store (user_id) → memories
mem.create_store("alice")        # create (idempotent)
mem.list_stores()              # [{"user_id","created_at"}, ...]
mem.delete_store("alice")        # delete the store + all its memories
await mem.createStore("alice");
await mem.listStores();          // [{ user_id, created_at }, ...]
await mem.deleteStore("alice");     // store + all its memories
mem.create_store("alice").await?;
let stores = mem.list_stores().await?;
mem.delete_store("alice").await?;
# create
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
# list
curl https://api.wontopos.com/api/v1/memory/collections -H "X-API-Key: $WOS_API_KEY"
# delete (store + all its memories)
curl -X DELETE https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" -d '{"user_id":"alice"}'
真实响应 - 创建
{ "user_id": "alice", "status": "created" }   // "exists" if it already did
真实响应 - 列出
{ "collections": [
  { "user_id": "default", "created_at": "2026-06-26T02:23:14Z" },
  { "user_id": "alice",   "created_at": "2026-06-26T02:24:01Z" }
], "count": 2 }
对不存在的存储库执行召回
{ "error": { "type": "not_found_error",
  "message": "Store 'ghost' does not exist. Create it first with
              POST /api/v1/memory/collection {\"user_id\":\"ghost\"}, then store or recall." } }
为每个最终用户使用一个存储库("alice""user_42"),使每个人的记忆彼此独立;个人智能体则用单个 default 存储库即可。您也可以在控制台(Memory ids → Issue)中创建和浏览存储库,无需写代码。删除存储库是永久性的 - 其下所有记忆都会被删除。 存储 id 在保存前会被折叠:转为小写,且 [a-z0-9_] 之外的字符变为 _,因此 Alice.Smithalice-smith 指向同一个存储。若第二个 id 折叠后与已有 id 相同,会以 409 拒绝,而不是悄悄共享。id 本身还须符合 [A-Za-z0-9][A-Za-z0-9._-]{0,63},所以电子邮件地址或非拉丁字符的名称不能作为存储 id,请改用内部标识符。

列出与删除存储库

mem.list_stores()                # [{"user_id","created_at"}, …]
mem.delete_store("alice")      # the store and every memory in it
await mem.listStores();
await mem.deleteStore("alice");
let stores = mem.list_stores().await?;
mem.delete_store("alice").await?;
curl -X POST   .../api/v1/memory/collections -d '{}'
curl -X DELETE .../api/v1/memory/collection  -d '{"user_id":"alice"}'
召回缓存

重复的召回,只需十分之一的价格。

按请求选择开启后,WOS 会以查询文本为键缓存检索结果,规则与 LLM 提示词缓存相同的前缀方式。缓存有效期间,重复或续写的查询会复用上一次的结果,缓存部分按正常 token 单价的 10% 计费。

仅限 Tablet 和 Scroll。 缓存适用于现在和将来的所有 Tablet、Scroll 模型。Book 不支持:Book 在记忆之上进行推理,并在调用之间学习,同一个问题可能合理地得到不同的答案,缓存的结果在设计上就是错误的。向 Book 发送 cache_control 会明确返回 403。

一段对话,三个回合

当代理持续与记忆对话时,实际发生的事情如下。每个回合都把到目前为止的对话作为查询发送,并开启 cache_control。

write回合 1 - “Alice: 我去年春天搬到了里斯本。”

整个查询被检索并缓存:输入按 2 倍计费(TTL 5 分钟)。

extend回合 2 - 相同文本 + “Bob: 那边天气怎么样?”

只有 Bob 的句子会被嵌入和检索。旧的部分按 0.1 倍,新句子按 2 倍,缓存现在以它结尾。

hit回合 3 - 再次发送完全相同的查询(重试、刷新)

完全不调用引擎。全部按 0.1 倍:这就是 90% 的折扣。

费率

操作Token 计费含义
缓存写入 - TTL 5 分钟第一次请求。结果保留 5 分钟,每次读取都会顺延有效期。
缓存写入 - TTL 1 小时第一次请求,保留整整一小时。
缓存读取 - 命中或前缀命中0.1×写入之后的每次请求:缓存部分按正常 token 单价的十分之一计费。

能省多少

一个具体的例子:你的代理把一段 3,000 token 的对话作为查询发送,并在五分钟内重复或续写 10 次。没有缓存时,是按全价计费的 30,000 输入 token。开启 5 分钟缓存后,第一次写入 6,000(2 倍)加上九次缓存读取约 2,700,共计 8,700 计费 token,节省 71%。对话越长,省得越多。

前缀规则

匹配以查询的开头为准。如果开头保持不变、只是在末尾追加了新文本,缓存部分会被复用,只检索新的部分。如果缓存文本结束之前的任何内容发生变化,则什么都无法复用。

prefix match
cached    [ A B C D E F G ]

○   [ A B C D E F G ] E
✗   [ B C D E F G ] E

命中 - 开头不变,只有 E 是新的部分
未命中 - 开头变了,整个查询将重新检索并重新缓存

要记住的三条规则

  • 续写会连同新尾部一起重新缓存。 在 [A B C D E F G] + E 之后,缓存以 E 结尾:尾部按写入费率计费一次,下一轮可以把 A 到 E 的全部内容再次作为前缀匹配。
  • 每个请求只有一个连续前缀。 一个查询不能拆成两段缓存,只有开头才能匹配。
  • 写入会立即失效缓存。 任何 store、store-turn、bulk-store、forget、supersede 或删除存储的操作都会丢弃该存储的缓存,缓存的答案绝不会返回过时的记忆。

如何开启

hits = mem.search(
    "...the conversation so far...", user_id="alice",
    cache_control={"ttl": "5m"},   # or "1h"
)
const hits = await mem.search(
  "...the conversation so far...", "alice", 10,
  { cache_control: { ttl: "5m" } },   // or "1h"
);
let hits = mem.search_with(
    "...the conversation so far...", "alice", 10,
    serde_json::json!({"cache_control": {"ttl": "5m"}}),   // or "1h"
).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice",
       "query":"...the conversation so far...",
       "cache_control":{"ttl":"5m"}}'   # or "1h"
响应 - cache 对象说明发生了什么
{ "memories": [ ... ],
  "cache": { "status": "hit",              // "write" | "hit" | "extend"
             "ttl": "5m",
             "cache_read_input_tokens": 412,
             "cache_creation_input_tokens": 0 } }

这些功能并不依赖 SDK。缓存只是一次 HTTP 调用上的一个字段,因此任何编程语言都能使用。curl 标签页就是通用配方,Python、TypeScript 和 Rust SDK 只是对同一个调用的便捷封装。

缓存在工作区内按存储、按模型隔离,默认关闭:不发送 cache_control,请求不会有任何变化。
谁说的

知道是谁说的记忆。

人的记忆以人为单位:Bob 承诺了什么,你说过要做什么。给每条记忆打上说话者标签,你的智能体也能做到,在所有 Tablet 和 Scroll 模型上。

说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

一个团队,三条记忆

一个存储库能让多个声音互不混淆。先注册一个人,每句话按说话者保存,之后按人提问。

add注册一次 Bob:POST /speakers,SDK 里是 add_speaker("Bob")。

存储库现在认识 Bob 了。50 人上限只在注册这里计数,store 调用绝不会返回上限错误。

BobBob 说截止日期推迟到周二。用 speaker "Bob" 保存。

这条记忆现在属于 Bob:每次搜索返回它时都会这样标注。

me你的助手承诺周五前交摘要。它自己的话用 speaker "me" 保存。

自己说的话也会被记住,而且 "me" 永远不占说话者上限。

ask之后问:"Bob 关于截止日期说了什么?" 用 speaker "Bob" 搜索。

只返回 Bob 的话。一个人的话绝不会变成另一个人的。

只需记住三条规则

  • "me" 是助手本身。不注册、不计数。保留字且仅小写有效:speaker: "Me""ME" 不会被隐式转换,而是返回 400 invalid_request_error
  • 上限在注册时计数:每库起步 50 人。超限注册返回 400 invalid_request_error,错误体带 speaker_limit: 50。用未注册的名字 store 同样返回 400 且不保存任何内容。用未注册的名字过滤搜索返回 404 not_found_error。请按状态码和字段分支,不要解析消息文本;上限将逐步提高。
  • 标签存在于每一次读取。搜索结果、recall 的长期上下文、engram 结果都带有说话者,模型始终知道手里的话是谁的。搜索时传 speaker 就只返回那个人的话;supersede 保留说话者,forget 一并删除。
  • 名字是 Unicode,任何语言都可以。さくら、Иван、하늘 都是有效的说话者,归属行为在所有语言中一致。匹配在去空格和 Unicode 规范化后完全一致,因此 Bobbob 是两个人。名字上限 80 个字符。
errors - verbatim
# POST /speakers past the limit
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "This store already has 50 registered speakers, ...",
             "speaker_limit": 50 } }

# store with an unregistered name → 400, nothing stored
{ "type": "error",
  "error": { "type": "invalid_request_error",
             "message": "speaker 'Bob' is not registered in this store. Register it first: ...",
             "speaker": "Bob" } }

# search filtered by an unregistered name → 404
{ "type": "error",
  "error": { "type": "not_found_error",
             "message": "speaker 'Bob' is not registered in this store.",
             "speaker": "Bob" } }

两点范围说明。speaker 附加在 add / store 上:add_turn 整体记住一次交流,按人标注和过滤来自显式带 speaker 保存的记忆。另外,会话段落(expand)是多条记忆的合成,因此不带标签;speaker 过滤总是返回原子级、带标签的记忆。 另外,与已存记忆语义足够接近的写入会被丢弃。判定依据是含义而非字面一致。这样的 store 返回 status "duplicate" 和明确的 note,不保存任何内容,也不附加说话者。仅在某个细节上不同于既有记忆的新事实("花生过敏"之后的"甲壳类过敏")同样会被丢弃,因此请读取 status,不要假定写入已生效。

我们用最严格的方式测试它:存入正文里完全没有名字的记忆,再按人回忆。归属来自说话者记录而不是文字匹配,所以在任何语言里表现一致。

如何使用

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")
hits = mem.search("what did Bob say about the deadline?", user_id="alice", speaker="Bob")
mem.list_speakers(user_id="alice")
mem.remove_speaker("Bob", user_id="alice")  # memories stay, the tag goes
await mem.addSpeaker("Bob", "alice");  // once per person; "me" needs no registration
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });
const hits = await mem.search("what did Bob say about the deadline?", "alice", 10, { speaker: "Bob" });
await mem.listSpeakers("alice");
await mem.removeSpeaker("Bob", "alice");  // memories stay, the tag goes
mem.add_speaker("Bob", "alice").await?;  // once per person; "me" needs no registration
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;
let hits = mem.search_with("what did Bob say about the deadline?", "alice", 10, json!({"speaker": "Bob"})).await?;
mem.list_speakers("alice").await?;
mem.remove_speaker("Bob", "alice").await?;  // memories stay, the tag goes
curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about the deadline?","speaker":"Bob"}'

curl "https://api.wontopos.com/api/v1/memory/speakers?user_id=alice" -H "X-API-Key: $WOS_API_KEY"

curl -X DELETE https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # memories stay, the tag goes
response
{ "memories": [
    { "content": "Bob said the deadline moved to Tuesday",
      "speaker": "Bob", ... } ] }
GET /speakers
{ "user_id": "alice",
  "speakers": [ { "speaker": "Bob", "memories": 2, "created_at": "2026-07-10T04:20:39Z" } ],
  "count": 1, "limit": 50 }

列表显示存储库认识的人及各自的记忆数与上限。移除只删除注册:那个人的记忆保留,只有名字标签消失。

读取某个人的记忆

by_speaker 无需查询词即可返回某个人说过的内容,按时间由新到旧。"me" 返回助手自己的话。分页方式与图像相同,采用游标:把 next_beforenext_skip_ids 回传。

page = mem.by_speaker("Bob", limit=50)
page["memories"], page["chunks"]
const page = await mem.bySpeaker("Bob", undefined, { limit: 50 });
let page = mem.by_speaker("Bob", None, 50, None, None).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/by-speaker \
  -H "X-API-Key: $WOS_KEY" \
  -d '{"user_id":"alice","speaker":"Bob","limit":50}'
字段作用
memories记忆列表,按时间由新到旧。结构与检索返回的相同。
chunks这些记忆背后的句子级片段,即一次删除实际会移除的内容。通常大于记忆的条数,建议在确认删除前先展示。同时以 points_to_delete 返回。
next_before下一页的游标,与 next_skip_ids 配合使用。两者都必须传,因为记忆可能共用同一时间戳。
此处的 speaker 是写入时记录的标签,而不是对文本的检索。存储时未指定说话人的记忆可以通过检索找到,但永远无法通过 by_speaker 取得,包括在 "me" 下。

列出、浏览与移除说话人

mem.list_speakers()                    # who is registered
mem.by_speaker("Bob")                 # what Bob said, newest first
mem.remove_speaker("Bob")             # unregister; the memories stay
await mem.listSpeakers();
await mem.bySpeaker("Bob");
await mem.removeSpeaker("Bob");
mem.list_speakers(None).await?;
mem.by_speaker("Bob", None, None, None, None).await?;
mem.remove_speaker("Bob", None).await?;
curl -X GET    .../api/v1/memory/speakers   -d '{"user_id":"alice"}'
curl -X POST   .../api/v1/memory/by-speaker -d '{"user_id":"alice","speaker":"Bob"}'
curl -X DELETE .../api/v1/memory/speakers   -d '{"user_id":"alice","speaker":"Bob"}'
开发者

图像

一条记忆可以携带一张图像。引擎会为图片建立索引,因此即使记录没有说明文字、标题或替代文本,任何语言的文本查询都能匹配到它。

Tablet 2 及更新版本支持。未实现图像功能的引擎会指名说明,而不是仅返回一个 404,因此可以区分功能缺失与记忆缺失。支持 JPEG、PNG、GIF 和 WebP。

存储图像

在普通的 add 调用中传入 image 对象。content 可以为空,此时图像本身即可被检索。

mem.add("at the beach", image={"data": b64})   # caption + image
mem.add("", image={"data": b64})               # the image IS the memory
// the image rides in the 4th argument; the 3rd is metadata
await mem.add("at the beach", undefined, {}, { image: { data: b64 } });
await mem.add("", undefined, {}, { image: { data: b64 } });   // the image IS the memory
let img = json!({"image": {"data": b64}});
mem.add_with("at the beach", None, json!({}), img.clone()).await?;
mem.add_with("", None, json!({}), img).await?;
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"","image":{"data":"<base64>"}}'

data 为必填。data:image/jpeg;base64, 前缀,以及 base64openssl 添加的换行,都会被自动去除。

字段作用
data图片的 Base64 编码。必填。大小上限是服务端配置,而非 SDK 常量,/healthmemory.images.max_bytes 报告该值。
reference你自己保存的原图所在位置。以字符串形式存储,我们不会去获取它。
taken_atRFC3339 格式,通常来自 EXIF。当 event_date 为空时填充该字段,使记忆按拍摄时间而非上传时间排序。

查找图像

没有单独的图像检索接口。searchrecall 会将图像与文本一并返回,并统一排序。

操作已有的图像

data, mime = mem.get_image(memory_id=mid)
page       = mem.list_images(limit=50)      # page["count"] = store total
mem.forget_image(memory_id=mid, preview=True)
const { bytes, contentType } = await mem.getImage(undefined, mid);
const page = await mem.listImages(undefined, { limit: 50 });
await mem.forgetImage(undefined, mid, { preview: true });
let (bytes, mime) = mem.get_image(None, mid).await?;
let page = mem.list_images(None, 50, None, None).await?;
mem.forget_image(None, mid, true).await?;
# original bytes — the one call on this plane that is not JSON
curl -X POST   .../api/v1/memory/image  -d '{"user_id":"alice","memory_id":"m_1"}'
curl -X POST   .../api/v1/memory/images -d '{"user_id":"alice","limit":50}'
curl -X DELETE .../api/v1/memory/image  -d '{"user_id":"alice","memory_id":"m_1","preview":true}'
调用作用
get_image原始字节,形式为 (bytes, content_type)。类型由字节内容嗅探得出,而非取自上传时的文件名。没有图像的记忆会抛出异常,而不是返回空内容。
list_images返回一页结果,按时间由新到旧,并附带 count,即该存储库的总数,而非本页条数。分页采用游标方式:把 next_beforenext_skip_ids 回传。两者都必须传,因为图像可能共用同一时间戳。
forget_image删除图像并保留文本。没有说明文字的图像本身就是这条记忆,这种情况下记忆也会一并删除。

forget_image 传入 preview=True,可在不做任何改动的情况下取得 memory_keptiter_images 会自动翻页。

一张图像的费用

图像与文本使用同一单位的令牌计费。令牌 = 像素面积 / 556.7。长边超过 1,568 px 时按 1,568 px 计数,因此 2,500 px 的图像与 1,568 px 的图像花费相同。

图像计量尺寸Token 数
700 × 700as sent881
1000 × 1000as sent1,797
1568 × 1568as sent4,417
1920 × 10801568 × 8822,485
2500 × 18751568 × 11763,313
2500 × 25001568 × 15684,417

单张图像的上限为 4,417 个令牌。调用前我们会按这个上限从余额中预留,调用后按实测值扣费,实测值绝不会更高。

过大或过小的图像会以 400 拒绝。我们不会替你缩放。两条边都必须 ≥ 700 px,长边 ≤ 2,500 px。低于 700 px 时嵌入模型按固定下限计费,因此更小的图像存储费用相同。请在发送前完成缩放,错误信息会同时给出收到的尺寸和要求的尺寸。

返回数量

默认 1,每次响应最多 5 张图像。五张图像接近 20,000 个 token。

字段作用
max_images0 到 5。单次响应可携带的图像数量。默认 1。0 表示只返回文本。超出范围会被拒绝,而不是截断到边界值。
为图像附加英文说明会提升英文查询的效果,并降低其他语言查询的效果,在十四种语言上平均使 recall@5 下降 11.4 个百分点。如果你的用户使用多种语言检索,存储图像时不要附加说明文字。
开发者

verify

verify 允许一次检索执行额外的检索轮次。每一轮都会排除此前各轮已返回的结果,因此第二轮能够触及第一轮未覆盖的记忆。

Tablet 2 及更新版本支持。向未实现该功能的引擎请求时,调用在发出前即被拒绝,因此不会为一次静默无效的轮次计费。

searchrecall 上的整数,取值 0-3。表示额外轮次的数量,因此 3 允许四次检索。默认 0。

hits = mem.search("what did I eat", verify=3)
# the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
const hits = await mem.search("what did I eat", undefined, 10, { verify: 3 });
// the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
let hits = mem.search_opts("what did I eat", None, 10,
                            &SearchOpts { verify: Some(3), ..Default::default() }).await?;
// the SDKs hand back the memories; `verify_used` is on the HTTP response (curl tab)
curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did I eat","verify":3}'
# → {"memories":[…], "verify_used":1}

该循环中不运行语言模型

每一轮会带上已返回的 id,引擎将其排除并继续向后检索。查询不会被改写,因此同一请求的结果是确定的,也不涉及任何模型凭据。是否再消耗一轮由你的代码决定。

请求字段与响应字段

调用作用
verify0 到 3。允许的额外轮次数量。超出范围会以 400 拒绝,而不是静默截断到边界值。
verify_used实际执行的额外轮次数量。可能低于请求的数量。

当某一轮没有返回新结果时,轮次提前停止,未使用的轮次不计费。若后续某一轮失败,则返回此前已收集到的结果。

当第一轮已经包含答案时,额外轮次可能降低准确率,LongMemEval-S 上的单会话用户类问题下降 4.2 个百分点。收益与单次检索未命中的频率成正比,因此存储库越大收益越高。
附加功能 · Beta

MCP - 给 AI 工具的记忆

WOS 的核心是 API 和 SDK。MCP 服务器是其上的附加功能:把同一份记忆,插进不是你构建的工具里 - Claude Code、Claude Desktop、Cursor。

一行安装,智能体就获得 9 个记忆工具并自行使用。记忆在你的账户里,一个工具写入的,其他所有工具都能召回 - 包括你用 SDK 构建的智能体。

用它能做什么

  • 记得你项目的 Claude Code。决策、bug 修复、偏好 - 下个会话直接召回,无需重新解释。
  • 在 ChatGPT 开始,在 Claude 继续。同一个存储、同一份记忆 - 对话跨工具延续,而不是从头再来。
  • 你自己的智能体也在同一记忆里。Claude Code 学到的,SDK 智能体能召回;智能体存下的,Claude Code 也能召回。

可用于 Claude Code、Claude Desktop、Cursor、Windsurf 及任何 MCP 宿主。ChatGPT 通过 Actions 加 OpenAPI 规范接入同一份记忆。

安装

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp

智能体获得九个工具 - recall · remember · search · update · forget · list_memories · engram · stats · create_store - 每个描述都写明何时使用,它自行判断。

附加功能本身免费,已在 npm 公开 - 只按它发起的 API 调用照常计费。需要 Node 18+ 和在控制台创建的 API 密钥。

打开开发者页面

附加功能

OpenAPI 规范

API 的完整机器可读地图 - 所有端点、请求、响应和错误。

OpenAPI 是用机器可读文件描述 HTTP API 的行业标准格式。

https://api.wontopos.com/openapi.json

用它能做什么

Postman: File → Import → 粘贴 URL,所有端点变成可点击的集合。ChatGPT: 创建 GPT,添加 Action,粘贴同一 URL。代码生成: openapi-generator -i .../openapi.json -g go 生成我们未提供语言的客户端。

导入 Postman、为我们未提供的语言生成客户端、接入 ChatGPT Actions、或在 CI 里做契约校验。测试将它钉在真实路由上,不会漂移。

附加功能

llms.txt

把整个 API 装进 AI 能读的一页文本。

llms.txt 是一种网络惯例:放在站点根目录的一页纯文本,把 AI 需要了解的产品信息全部讲清。

https://wontopos.com/llms.txt

放进 IDE 或编码智能体,它就知道怎么在 WOS 上构建 - 认证、端点、模式、错误。每次发布同步更新。

与 OpenAPI 规范同样的事实,不同的读者:规范是给工具的精确结构,这个文件是 AI(或人)一口气读完的散文。两者都随每次发布更新。

Model Context Protocol · Beta

在每个 AI 工具里的记忆

一条命令,Claude Code、Claude Desktop、Cursor 等任何 MCP 宿主就拥有由你的 WOS 账户支撑的长期记忆。无需集成代码,智能体获得 9 个记忆工具并自行决定何时使用。

MCP 处于测试版。九个工具现已可用且经过测试,但在完善过程中表面仍可能变化。其下的 API 和 SDK 是稳定且有版本管理的。

安装

Claude Code 只需一行(先在控制台创建密钥):

claude mcp add wontopos --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick which store it remembers into (optional): add --env WONTOPOS_USER_ID=my-project
# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
# Claude Desktop and any other MCP host
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →  ·  Add to VS Code →

可选 env:WONTOPOS_USER_ID 指定默认存储,WONTOPOS_MODEL 指定引擎,WONTOPOS_BASE_URL 指定自托管部署。 WONTOPOS_READ_ONLY=1 切换为只读(仅召回/搜索/查看)。

共享同一个存储之前

  • 使用专用密钥。密钥携带其工作区,为 MCP 单独创建的密钥从根上限定了所连工具能触及的范围 - 随时可在控制台轮换,不影响应用的密钥。
  • 只读模式。WONTOPOS_READ_ONLY=1 时完全不注册写入工具:智能体只能召回、搜索、列出记忆、运行 engram、读取统计,无法存储、更新或删除。适合只应查阅记忆、而非拥有记忆的智能体。
  • 保持工具执行确认开启。MCP 宿主默认在运行工具前询问 - 尤其是 forget,因为删除对该存储上的所有工具生效。
  • 凡是存入的,持有密钥的每个工具都能召回。绝不要把机密 - API 密钥、密码 - 存成记忆。
  • 召回的记忆是数据,不是指令。工具描述会明确告诉智能体这一点。即便如此,也不要把不可信的第三方文本存进自主智能体会遵循的存储里。
  • 删除同样是共享的。一个工具里的 forget 或 delete_all,对所有工具都生效。
  • "me" 指的是写入该存储的智能体自己。多个智能体共享一个存储时,"me" 的声音会混在一起。想分开身份,就给每个智能体单独的存储(WONTOPOS_USER_ID)。
  • 一个账户买单。所有接入的工具都消耗同一份余额和速率额度。

然后直接对话

you记住我们每周五发布

智能体调用 remember 工具。持久保存在你的存储里,会话结束也不会丢失。

new session我们什么时候发布?

新会话没有任何聊天记录。智能体调用 recall,凭记忆回答:周五。

可以这样说

  • "这个仓库用 pnpm,记住" → remember 存下来,下个会话就已经知道。
  • "上周我们定的错误格式是什么?" → recall 把那个决定拉回上下文。
  • "其实截止日期改到周五了" → 智能体发现这与它召回的记忆矛盾,调用 update 就地更正那条记忆。
  • "记错了,删掉" → 智能体找到记忆 id 并调用 forget,宿主会先要求确认。
  • "你记得我哪些事?" → list_memories 翻遍所有已存记忆,智能体就能回答或整理。

不需要特殊句式 - 以上都是普通句子,不是命令。智能体读取各工具的描述,自行选择。

九个工具

  • recall - 一次调用取得上下文:最近对话 + 相关长期记忆。工具描述里写明:涉及过去上下文时先调用它。
  • remember - 保存持久的事实或决定。speaker: "me" 标记智能体自己的话;已注册的名字标记说话人。
  • search - 语义搜索。可按人过滤的 speaker,以及按时间或主题收窄范围的 filters(“六月我们定了什么?”)—— 语义本身唯一无法收窄的维度就是时间。
  • update - 用新内容取代事实已变的记忆,保留脉络而不是删除。
  • forget - 按 id 删除一条记忆。
  • list_memories - 分页浏览已存储的全部内容,用于回答“你记得我什么”或做整理。
  • engram - 当一次搜索不够时,运行内置的多跳流程(deep_recall、timeline、gather)。
  • stats - 查看存储中有多少内容 —— 清理前使用,也用于确认写入是否真的落库。
  • create_store - 存储是显式的:每个最终用户、项目或智能体一个。

SDK 还是 MCP?

  • SDK 放进你自己写的应用里。什么时候存、取什么,由你的代码精确决定 - 确定性、有类型、有版本。做产品就用 SDK。
  • MCP 插进不是你写的 AI 工具里。何时使用记忆由智能体根据工具描述判断 - 零代码。适合 Claude Code、Claude Desktop、Cursor,或给现成的助手装上记忆。

底层是同一个 API、同一批存储 - 用 SDK 构建的应用和用 MCP 连接的 Claude Code 会话共享同一份记忆。按场景选择,不是二选一。

一份记忆,贯穿所有工具

记忆属于账户,而不是工具。从 ChatGPT(Actions + OpenAPI 规范)写入的存储,在 Claude Code 和你自己的智能体里同样能召回,反之亦然。在一个工具里开始的对话,在另一个工具里继续。

而且因为是同一个存储,你可以在 Claude Code 上用着,再回到自己的智能体继续对话:同一密钥、同一存储的 SDK 智能体能召回 Claude Code 刚学到的一切;你的智能体存下的,下个会话的 Claude Code 也能召回。

在本地通过 stdio 运行(npx wontopos-mcp):采用这种方式时,密钥只留在你的环境里,不会作为 MCP 会话的一部分发送给我们。它封装了 TypeScript SDK,自动重试、拒绝重定向、密钥脱敏原样生效。
Model Context Protocol · Beta

Claude Code

旗舰路径:终端里一条命令,每个会话都带着记忆开始。

  1. 在控制台创建 API 密钥。密钥携带其工作区,一把密钥 = 一个记忆空间。
  2. 注册服务器。加 --scope user 所有项目可用;不加则只有当前项目可见。
  3. 验证:在 Claude Code 里运行 /mcp,应能看到 wontopos 和 9 个工具。
  4. 自动化技巧:在 CLAUDE.md 里写一行"涉及过去上下文时先调用 wontopos recall",每个会话就会不用吩咐地带着记忆开始。
claude mcp add wontopos --scope user \
  --env WONTOPOS_API_KEY=wos-live-... -- npx -y wontopos-mcp
# pick a store (optional): add --env WONTOPOS_USER_ID=my-project
Model Context Protocol · Beta

Claude Desktop

把下面的块加进 claude_desktop_config.json(设置 → 开发者 → Edit Config),重启应用,九个工具就会出现。注意:网页版和移动版 claude.ai 需要远程 MCP 服务器,WOS 尚未提供 - 桌面应用是受支持的路径。

# claude_desktop_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

Cursor

把下面的块加进 ~/.cursor/mcp.json,或点一键按钮,然后重启 Cursor。智能体就会拿到九个工具。

# ~/.cursor/mcp.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to Cursor →

Model Context Protocol · Beta

VS Code

VS Code(Copilot 智能体模式)从项目的 .vscode/mcp.json 读取 MCP 服务器:加入下面的块,或点一键按钮。

# .vscode/mcp.json
{ "servers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }

Add to VS Code →

Model Context Protocol · Beta

Windsurf

Windsurf(Cascade)读取 ~/.codeium/windsurf/mcp_config.json:加入下面的块并重新加载,同样的九个工具就会出现。

# ~/.codeium/windsurf/mcp_config.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Model Context Protocol · Beta

ChatGPT

ChatGPT 的 MCP 连接器只接受远程服务器,所以目前受支持的路径是自定义 GPT 的 Action:创建 GPT,添加 Action,粘贴下面的 OpenAPI 规范 URL,并把 API 密钥设为认证头。这个 GPT 就会调用与其他工具相同的记忆。

# GPT → Configure → Actions → Import from URL
https://api.wontopos.com/openapi.json
# Authentication: API Key · Header name: X-API-Key

同一个存储、同一份记忆:ChatGPT 通过 Action 存的,Claude Code 通过 MCP 召回,反之亦然。

Model Context Protocol · Beta

Gemini CLI

Gemini CLI 从 ~/.gemini/settings.json 读取 MCP 服务器:加入下面的块并重启 CLI,同样的九个工具也会出现。

# ~/.gemini/settings.json
{ "mcpServers": {
    "wontopos": {
      "command": "npx",
      "args": ["-y", "wontopos-mcp"],
      "env": { "WONTOPOS_API_KEY": "wos-live-...",
               "WONTOPOS_USER_ID": "my-project" }
    } } }
Python SDK

Python - 全部方法,三大类。

写入、读取、删除。以下每个示例都于 2026-08-01 针对线上 API 实际运行;响应为原样展示。

pip install wontopos
from wontopos import Client

mem = Client(api_key="wos-live-...")  # or read from an env var

选择模型

API 密钥决定用哪份记忆(您的账户);模型决定用哪个引擎来读取。所有模型共享同一份记忆,因此可以用一个模型存储、用另一个召回。在客户端上设置默认值;给单个调用传 model= 即可覆盖。

mem = Client(api_key="wos-live-...", model="tablet-1")  # default engine
mem.recall("...", user_id="alice")                  # tablet-1
mem.recall("...", user_id="alice", model="scroll-1")  # or pick a model per call

list_models

模型目录 - 可传给 model 的 id 以及各自是否已上线。memory: "shared" 的模型读取同一存储;"isolated" 则各自独立。无需 API 密钥。

mem.list_models()
真实响应
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

用一行确认连接以及 API 密钥是否有效。

mem.ping()   # True, or raises AuthenticationError / PaymentRequiredError

上面的目录始终反映当前可用的模型 - 传入其他任何 id 都会得到明确的错误。新模型发布后会自动出现在其中。

写入

add

存储一条记忆。写入时即完成向量嵌入 - 不调用 LLM,您只为嵌入付费。

mem.add("she prefers tea over coffee", user_id="alice")
mem.add("I promised the summary by Friday", user_id="alice", speaker="me")  # its own words - no registration needed
真实响应
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

把一轮对话(用户 + 助手)同时写入短期与长期记忆。

mem.add_turn("hi", "hello!", user_id="alice")
真实响应
{"status": "ok"}

speaker

每条记忆都可以记录说话者。先注册一个人,之后把名字作为 speaker 传入;"me"(助手自己的话)无需注册。搜索也接受 speaker,可以只取某个人说过的话。

mem.add_speaker("Bob", user_id="alice")  # once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", user_id="alice", speaker="me")
mem.add("Bob said the deadline moved to Tuesday", user_id="alice", speaker="Bob")
mem.search("what did Bob say about deadlines?", user_id="alice", speaker="Bob")
response
[{"content": "Bob said the deadline moved to Tuesday", "speaker": "Bob", ...}]
说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

add_bulk

回填一大段文本。在服务端分块并嵌入 - 非常适合导入既有历史。

mem.add_bulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", user_id="alice")
真实响应
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

某个事实变了。旧记忆被标记为已取代(保留供追溯);新记忆在召回中取而代之。

mem.update("576700aa-...", "she switched to coffee this year", user_id="alice")
真实响应
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

读取

search

语义搜索,最相关的排在最前。纯向量嵌入 - 没有关键词匹配,因此任何语言都能找到任何记忆。SDK 直接返回 memories 数组;下方展示的是原始 HTTP 响应体。 具备自有通道的模型(Scroll 1.2 及以上)会以两条通道作答,SDK 将两者合并返回,因此数组可能多于 max_results。请以实际收到的数组、而非请求的数量来估算提示词长度。

r = mem.search("what does she drink?", user_id="alice", limit=1)
真实响应(HTTP 响应体)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
字段含义
similarity与您查询的原始嵌入相似度(0–1)。
is_superseded若该事实已被 update() 取代则为 true。
search_ms服务端检索耗时。

recall

一次往返返回您的 LLM 所需的一切 - 结果可直接粘贴进提示词:无论已存储多少内容,上下文大小固定且有界。

ctx = mem.recall("what does she drink?", user_id="alice")
真实响应(结构 - 列表已截短)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

最近的对话轮次(短期记忆),最早的在前。

turns = mem.history("alice")
真实响应(HTTP 响应体)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

单个用户的记忆条数统计。

mem.stats("alice")
真实响应
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

按 id 获取单条记忆 - 即 add 或 list_memories 返回的 id。只返回存储的原文和元数据,绝不返回向量。其他存储空间的 id,或已删除/失效的记忆,返回 404。

m = mem.get("alice", memory_id="576700aa-...")
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
真实响应
{"memory": {"id": "8bd090de-...", "content": "the office moved to the seventh floor in June",
  "category": "general", "created_at": "2026-07-31T18:20:30.531518060+00:00", "event_date": null,
  "is_superseded": false, "superseded_by": null}, "user_id": "docs_livetest"}

list_memories

列出存储中的记忆——只返回你保存的原文与元数据,不含向量。按游标翻页:把返回的 next_cursor 传回以获取下一页。

page = mem.list_memories("alice", limit=100)
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

iter_memories · export_memories

无需管理游标即可遍历全部记忆,或一次性取回整个存储。

for m in mem.iter_memories("alice"):   # every page, no cursor bookkeeping
    print(m["id"], m["content"])
everything = mem.export_memories("alice")   # the whole store as a list

删除

delete

按 id 删除单条记忆。

mem.delete("alice", memory_id="576700aa-...")
真实响应
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all

抹除单个用户的全部数据 - 一次调用,符合 GDPR。

mem.delete_all("alice")
真实响应
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

错误与可靠性

每个失败都是带类型的错误——可按具体情况(限流、认证、付费)分别捕获,或用基类 WosError 统一捕获。

from wontopos import PaymentRequiredError, NotFoundError
try:
    mem.add("...", user_id="alice")
except NotFoundError:
    mem.create_store("alice")   # store didn't exist yet
except PaymentRequiredError:
    top_up()                       # out of credit - don't retry

rate_limit

在任意调用后读取剩余额度,在触达上限前主动放慢。

mem.search("...", user_id="alice")
rl = mem.rate_limit   # {"limit": 150, "remaining": 3, "reset": ...}

search_self

在自我记忆模型(Scroll 1.2+)上一次调用返回两条通道:别人说的话与智能体自己说的话分开返回,读取方不会弄混说话人。

r = mem.search_self("what did I promise?", user_id="alice")
r["memories"]       # what others said / general memories
r["self_memories"]  # the agent's OWN words (speaker "me")

list_engrams

向服务询问当前模型可运行的 engram 与投递形式,而不是硬编码名称 —— 一旦有新 engram 上线,硬编码的代码就再也看不到它。

cat = mem.list_engrams()
[e["name"] for e in cat["engrams"]]   # ask, never hard-code

filters

把搜索收窄到存储的一部分。在排序之前应用,因此得到的是过滤范围内最相关的结果 - 而不是对 top-N 再做过滤。

mem.search("what did we decide", user_id="alice", filters={
    "categories": ["work"],
    "event_from": "2026-01-01",   # when it HAPPENED
})
键:categories · event_from / event_to(内容发生的时间 - metadata.event_date)· time_from / time_to(写入的时间)· min_importance。未列出的键会被丢弃而不是报错,所以拼错会悄悄扩大搜索范围。

idempotency_key

让同一次写入可以安全重复。当重试来自你这边时使用 - 中断后重跑的任务、会重投的队列。

mem.add("she prefers tea", "alice", idempotency_key=f"import:{row.id}")
密钥要从被存储的对象派生(import:row-42),不要用常量:两次不同的写入复用同一密钥会重放第一次的响应,第二次会被悄悄丢弃。格式:1-128 个 [A-Za-z0-9._:-] 字符。

with_timeout / with_retries

不改动已经建好的客户端,只调整单个调用点:大批量回填用超时更长的克隆,自己写重试循环时用关闭重试的克隆。

mem.with_timeout(120).add_bulk(big_blob, "alice")  # this slow call only
mem.with_retries(0).add("...", "alice")              # you retry, not the SDK
TypeScript SDK

TypeScript - 全部方法,三大类。

写入、读取、删除。以下每个示例都于 2026-08-01 针对线上 API 实际运行;响应为原样展示。

npm install wontopos
import { Client } from "wontopos";

const mem = new Client({ apiKey: "wos-live-..." });

选择模型

API 密钥决定用哪份记忆(您的账户);模型决定用哪个引擎来读取。所有模型共享同一份记忆,因此可以用一个模型存储、用另一个召回。在构造函数中设置默认值;用 withModel() 覆盖单个调用。

const mem = new Client({ apiKey: "wos-live-...", model: "tablet-1" });  // default
mem.recall("...", "alice");                          // tablet-1
mem.withModel("scroll-1").recall("...", "alice");  // or pick a model per call

listModels

模型目录 - 可传给 model 的 id 以及各自是否已上线。memory: "shared" 的模型读取同一存储;"isolated" 则各自独立。无需 API 密钥。

await mem.listModels();
真实响应
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

用一行确认连接以及 API 密钥是否有效。

await mem.ping();   // true, or throws AuthenticationError / PaymentRequiredError

上面的目录始终反映当前可用的模型 - 传入其他任何 id 都会得到明确的错误。新模型发布后会自动出现在其中。

写入

add

存储一条记忆。写入时即完成向量嵌入 - 不调用 LLM,您只为嵌入付费。

await mem.add("she prefers tea over coffee", "alice");
await mem.add("I promised the summary by Friday", "alice", { speaker: "me" });  // its own words - no registration needed
真实响应
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

addTurn

把一轮对话(用户 + 助手)同时写入短期与长期记忆。

await mem.addTurn("hi", "hello!", "alice");
真实响应
{"status": "ok"}

speaker

每条记忆都可以记录说话者。先注册一个人,之后把名字作为 speaker 传入;"me"(助手自己的话)无需注册。搜索也接受 speaker,可以只取某个人说过的话。

await mem.addSpeaker("Bob", "alice");  // once per person; "me" needs no registration
await mem.add("I promised to send the report on Friday", "alice", { speaker: "me" });
await mem.add("Bob said the deadline moved to Tuesday", "alice", { speaker: "Bob" });
await mem.search("what did Bob say about deadlines?", "alice", 10, { speaker: "Bob" });
说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

addBulk

回填一大段文本。在服务端分块并嵌入 - 非常适合导入既有历史。

await mem.addBulk("Alice moved to Brooklyn in March. She works at a design studio downtown.", "alice");
真实响应
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

某个事实变了。旧记忆被标记为已取代(保留供追溯);新记忆在召回中取而代之。

await mem.update("576700aa-...", "she switched to coffee this year", "alice");
真实响应
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

读取

search

语义搜索,最相关的排在最前。纯向量嵌入 - 没有关键词匹配,因此任何语言都能找到任何记忆。SDK 直接返回 memories 数组;下方展示的是原始 HTTP 响应体。 具备自有通道的模型(Scroll 1.2 及以上)会以两条通道作答,SDK 将两者合并返回,因此数组可能多于 max_results。请以实际收到的数组、而非请求的数量来估算提示词长度。

const r = await mem.search("what does she drink?", "alice", 1);
真实响应(HTTP 响应体)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
字段含义
similarity与您查询的原始嵌入相似度(0–1)。
is_superseded若该事实已被 update() 取代则为 true。
search_ms服务端检索耗时。

recall

一次往返返回您的 LLM 所需的一切 - 结果可直接粘贴进提示词:无论已存储多少内容,上下文大小固定且有界。

const ctx = await mem.recall("what does she drink?", "alice");
真实响应(结构 - 列表已截短)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

最近的对话轮次(短期记忆),最早的在前。

const turns = await mem.history("alice");
真实响应(HTTP 响应体)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

单个用户的记忆条数统计。

await mem.stats("alice");
真实响应
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

按 id 获取单条记忆 - 即 add 或 list_memories 返回的 id。只返回存储的原文和元数据,绝不返回向量。其他存储空间的 id,或已删除/失效的记忆,返回 404。

const m = await mem.get("alice", "576700aa-...");
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}

listMemories

列出存储中的记忆——只返回你保存的原文与元数据,不含向量。按游标翻页:把返回的 next_cursor 传回以获取下一页。

const page = await mem.listMemories("alice", { limit: 100 });
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

iterMemories · exportMemories

无需管理游标即可遍历全部记忆,或一次性取回整个存储。

for await (const m of mem.iterMemories("alice")) console.log(m.id, m.content);
const everything = await mem.exportMemories("alice");

删除

delete

按 id 删除单条记忆。

await mem.delete("alice", "576700aa-...");
真实响应
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

deleteAll

抹除单个用户的全部数据 - 一次调用,符合 GDPR。

await mem.deleteAll("alice");
真实响应
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

错误与可靠性

每个失败都是带类型的错误——可按具体情况(限流、认证、付费)分别捕获,或用基类 WosError 统一捕获。

import { NotFoundError, PaymentRequiredError } from "wontopos";
try {
  await mem.add("...", "alice");
} catch (e) {
  if (e instanceof NotFoundError) await mem.createStore("alice");
  else if (e instanceof PaymentRequiredError) topUp();   // out of credit
  else throw e;
}

rateLimit

在任意调用后读取剩余额度,在触达上限前主动放慢。

await mem.search("...", "alice");
const rl = mem.rateLimit;   // { limit: 150, remaining: 3, reset: ... }

searchSelf

在自我记忆模型(Scroll 1.2+)上一次调用返回两条通道:别人说的话与智能体自己说的话分开返回,读取方不会弄混说话人。

const { memories, self_memories } = await mem.searchSelf("what did I promise?", "alice");
// memories = what others said · self_memories = the agent's OWN words

listEngrams

向服务询问当前模型可运行的 engram 与投递形式,而不是硬编码名称 —— 一旦有新 engram 上线,硬编码的代码就再也看不到它。

const { engrams, forms } = await mem.listEngrams();  // ask, never hard-code

filters

把搜索收窄到存储的一部分。在排序之前应用,因此得到的是过滤范围内最相关的结果 - 而不是对 top-N 再做过滤。

await mem.search("what did we decide", "alice", 10, {
  filters: { categories: ["work"], event_from: "2026-01-01" },  // when it HAPPENED
});
键:categories · event_from / event_to(内容发生的时间 - metadata.event_date)· time_from / time_to(写入的时间)· min_importance。未列出的键会被丢弃而不是报错,所以拼错会悄悄扩大搜索范围。

idempotencyKey

让同一次写入可以安全重复。当重试来自你这边时使用 - 中断后重跑的任务、会重投的队列。

await mem.add("she prefers tea", "alice", {}, { idempotencyKey: `import:${row.id}` });
密钥要从被存储的对象派生(import:row-42),不要用常量:两次不同的写入复用同一密钥会重放第一次的响应,第二次会被悄悄丢弃。格式:1-128 个 [A-Za-z0-9._:-] 字符。

withTimeout / withRetries

不改动已经建好的客户端,只调整单个调用点:大批量回填用超时更长的克隆,自己写重试循环时用关闭重试的克隆。

await mem.withTimeout(120_000).addBulk(bigBlob, "alice");  // this slow call only
await mem.withRetries(0).add("...", "alice");            // you retry, not the SDK
Rust SDK

Rust - 全部方法,三大类。

写入、读取、删除。以下每个示例都于 2026-08-01 针对线上 API 实际运行;响应为原样展示。

cargo add wontopos
use wontopos::Client;

let mem = Client::new("wos-live-...");

选择模型

API 密钥决定用哪份记忆(您的账户);模型决定用哪个引擎来读取。所有模型共享同一份记忆,因此可以用一个模型存储、用另一个召回。用 with_model() 设置默认值;再次链式调用即可覆盖单个调用。

let mem = Client::new("wos-live-...").with_model("tablet-1");  // default
mem.recall("...", "alice").await?;                       // tablet-1
mem.with_model("scroll-1").recall("...", "alice").await?;  // or pick a model per call

list_models

模型目录 - 可传给 with_model 的 id 以及各自是否已上线。memory: "shared" 的模型读取同一存储;"isolated" 则各自独立。无需 API 密钥。

mem.list_models().await?;
真实响应
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"},
 {"id": "tablet-2", "name": "Tablet 2", "available": true, "memory": "shared"},
 {"id": "scroll-1", "name": "Scroll 1", "available": true, "memory": "shared"},
 {"id": "scroll-1.2", "name": "Scroll 1.2", "available": true, "memory": "shared"}]

ping

用一行确认连接以及 API 密钥是否有效。

mem.ping().await?;   // Ok(true), or Err whose .kind() is Auth / PaymentRequired

上面的目录始终反映当前可用的模型 - 传入其他任何 id 都会得到明确的错误。新模型发布后会自动出现在其中。

写入

add

存储一条记忆。写入时即完成向量嵌入 - 不调用 LLM,您只为嵌入付费。

mem.add("she prefers tea over coffee", "alice", json!({})).await?;
mem.add("I promised the summary by Friday", "alice", json!({"speaker": "me"})).await?;  // its own words - no registration needed
真实响应
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

把一轮对话(用户 + 助手)同时写入短期与长期记忆。

mem.add_turn("hi", "hello!", "alice").await?;
真实响应
{"status": "ok"}

speaker

每条记忆都可以记录说话者。先注册一个人,之后把名字作为 speaker 传入;"me"(助手自己的话)无需注册。搜索也接受 speaker,可以只取某个人说过的话。

mem.add_speaker("Bob", "alice").await?;  // once per person; "me" needs no registration
mem.add("I promised to send the report on Friday", "alice", json!({"speaker": "me"})).await?;
mem.add("Bob said the deadline moved to Tuesday", "alice", json!({"speaker": "Bob"})).await?;
mem.search_with("what did Bob say about deadlines?", "alice", 10, json!({"speaker": "Bob"})).await?;
说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

add_bulk

回填一大段文本。在服务端分块并嵌入 - 非常适合导入既有历史。

mem.add_bulk("Alice moved to Brooklyn in March...", "alice", "general").await?;
真实响应
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}

update

某个事实变了。旧记忆被标记为已取代(保留供追溯);新记忆在召回中取而代之。

mem.update("576700aa-...", "she switched to coffee this year", "alice").await?;
真实响应
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
 "old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}

读取

search

语义搜索,最相关的排在最前。纯向量嵌入 - 没有关键词匹配,因此任何语言都能找到任何记忆。SDK 直接返回 memories 数组;下方展示的是原始 HTTP 响应体。 具备自有通道的模型(Scroll 1.2 及以上)会以两条通道作答,SDK 将两者合并返回,因此数组可能多于 max_results。请以实际收到的数组、而非请求的数量来估算提示词长度。

let r = mem.search("what does she drink?", "alice", 1).await?;
真实响应(HTTP 响应体)
[{
   "id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624",
   "content": "she prefers tea over coffee",
   "category": "general",
   "time_bucket": "2026-06",
   "importance": 0.3,
   "similarity": 0.6316057443618774,
   "is_superseded": false,
   "superseded_by": null,
   "created_at": "2026-07-10T04:20:39.688276876Z"
 }]
字段含义
similarity与您查询的原始嵌入相似度(0–1)。
is_superseded若该事实已被 update() 取代则为 true。
search_ms服务端检索耗时。

recall

一次往返返回您的 LLM 所需的一切 - 结果可直接粘贴进提示词:无论已存储多少内容,上下文大小固定且有界。

let ctx = mem.recall("what does she drink?", "alice").await?;
真实响应(结构 - 列表已截短)
{"short_term":  {"count": 2, "turns": [{"role": "user", "content": "hi", ...}]},
 "long_term":   {"count": 4, "memories": [{"content": "she prefers tea over coffee",
                                           "similarity": 0.63, ...}]},
 "context":     {"count": 4, "around_top_memory": [
                  "[match] she prefers tea over coffee",
                  "[after] Alice moved to Brooklyn in March. ..."]},
 "instruction": "Use short_term for recent context, long_term for relevant
                 past memories, context for surrounding conversation of the
                 most relevant memory."}

history

最近的对话轮次(短期记忆),最早的在前。

let turns = mem.history("alice").await?;
真实响应(HTTP 响应体)
{"count": 2, "turns": [
   {"role": "user",      "content": "hi",     "timestamp": "2026-07-10T04:20:40.989011337Z"},
   {"role": "assistant", "content": "hello!", "timestamp": "2026-07-10T04:20:40.989013416Z"}
 ], "user_id": "alice"}

stats

单个用户的记忆条数统计。

mem.stats("alice").await?;
真实响应
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

按 id 获取单条记忆 - 即 add 或 list_memories 返回的 id。只返回存储的原文和元数据,绝不返回向量。其他存储空间的 id,或已删除/失效的记忆,返回 404。

let m = mem.get("alice", "576700aa-...").await?;
response
{"id": "576700aa-...", "content": "she prefers tea over coffee",
 "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}

list_memories

列出存储中的记忆——只返回你保存的原文与元数据,不含向量。按游标翻页:把返回的 next_cursor 传回以获取下一页。

mem.list_memories("alice", 100, None).await?;
response
{"count": 2, "next_cursor": null, "memories": [
   {"id": "576700aa-...", "content": "she prefers tea over coffee",
    "category": "general", "created_at": "2026-07-10T04:20:39Z", "event_date": null,
 "is_superseded": false, "superseded_by": null}
 ]}

list_all_memories

无需管理游标即可遍历全部记忆,或一次性取回整个存储。

let all = mem.list_all_memories("alice").await?;   // every page, collected

删除

delete

按 id 删除单条记忆。

mem.delete("alice", "576700aa-...").await?;
真实响应
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}

delete_all

抹除单个用户的全部数据 - 一次调用,符合 GDPR。

mem.delete_all("alice").await?;
真实响应
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

错误与可靠性

每个失败都是带类型的错误——可按具体情况(限流、认证、付费)分别捕获,或用基类 WosError 统一捕获。

use wontopos::ErrorKind;
match mem.search("...", "alice", 10).await {
    Ok(hits) => { /* use hits */ }
    Err(e) if e.kind() == ErrorKind::NotFound => { mem.create_store("alice").await?; }
    Err(e) if e.is_rate_limited() => { /* back off */ }
    Err(e) => return Err(e),
}

rate_limit

在任意调用后读取剩余额度,在触达上限前主动放慢。

mem.search("...", "alice", 10).await?;
let rl = mem.rate_limit();   // Some(RateLimit { remaining: Some(3), .. })

search_self

在自我记忆模型(Scroll 1.2+)上一次调用返回两条通道:别人说的话与智能体自己说的话分开返回,读取方不会弄混说话人。

let r = mem.search_self("what did I promise?", "alice", 10).await?;
// r.memories = what others said · r.self_memories = the agent's OWN words

list_engrams

向服务询问当前模型可运行的 engram 与投递形式,而不是硬编码名称 —— 一旦有新 engram 上线,硬编码的代码就再也看不到它。

let cat = mem.list_engrams().await?;  // ask, never hard-code

filters

把搜索收窄到存储的一部分。在排序之前应用,因此得到的是过滤范围内最相关的结果 - 而不是对 top-N 再做过滤。

mem.search_with("what did we decide", "alice", 10, json!({"filters": {
    "categories": ["work"], "event_from": "2026-01-01"   // when it HAPPENED
}})).await?;
键:categories · event_from / event_to(内容发生的时间 - metadata.event_date)· time_from / time_to(写入的时间)· min_importance。未列出的键会被丢弃而不是报错,所以拼错会悄悄扩大搜索范围。

add_idempotent

让同一次写入可以安全重复。当重试来自你这边时使用 - 中断后重跑的任务、会重投的队列。

mem.add_idempotent("she prefers tea", "alice", json!({}), &format!("import:{}", row.id)).await?;
密钥要从被存储的对象派生(import:row-42),不要用常量:两次不同的写入复用同一密钥会重放第一次的响应,第二次会被悄悄丢弃。格式:1-128 个 [A-Za-z0-9._:-] 字符。

with_timeout / with_retries

不改动已经建好的客户端,只调整单个调用点:大批量回填用超时更长的克隆,自己写重试循环时用关闭重试的克隆。

mem.with_timeout(120).add_bulk(big_blob, "alice", "general").await?;
mem.with_retries(0).add("...", "alice", json!({})).await?;
curl

curl - 无需安装,同样的方法。

无需安装 SDK - 任何 HTTP 客户端都可用。设置一次密钥,即可调用 SDK 所封装的相同端点。基础 URL 为 https://api.wontopos.com,通过 X-API-Key 认证,请求与响应均为 JSON。

# set your key once (never hard-code it)
export WOS_API_KEY="wos-live-..."

写入

store

存储一条记忆。写入时即完成向量嵌入 - 不调用 LLM。

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'
真实响应
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

store-turn

把一轮对话(用户 + 助手)同时写入短期与长期记忆。

curl -X POST https://api.wontopos.com/api/v1/memory/store-turn \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","user_msg":"hi","assistant_msg":"hello!"}'
真实响应
{"status": "ok"}

speaker

每条记忆都可以记录说话者。先注册一个人,之后把名字作为 speaker 传入;"me"(助手自己的话)无需注册。搜索也接受 speaker,可以只取某个人说过的话。

curl -X POST https://api.wontopos.com/api/v1/memory/speakers \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","speaker":"Bob"}'   # once per person

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"I promised to send the report on Friday","metadata":{"speaker":"me"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"Bob said the deadline moved to Tuesday","metadata":{"speaker":"Bob"}}'

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did Bob say about deadlines?","speaker":"Bob"}'
说话者和存储库一样是显式的。先注册,再以其名字保存。拼写错误绝不会悄悄变成一个新人。每个存储库起步可注册 50 人(会逐步提高),"me" 永远无需注册也不计数。

supersede

某个事实变了 - 旧记忆被标记为已取代,新记忆在召回中取而代之。

curl -X POST https://api.wontopos.com/api/v1/memory/supersede \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","old_memory_id":"576700aa-...","new_content":"she switched to coffee this year"}'
真实响应
{"new_memory_id": "07e94433-...", "old_memory_id": "576700aa-...", "status": "superseded"}

bulk-store

一次调用回填长历史 - 服务端完成分块与嵌入。

curl -X POST https://api.wontopos.com/api/v1/memory/bulk-store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"...a long history...","category":"general"}'
真实响应
{"elapsed_secs": 0.138589761, "status": "ok", "stored": 1, "total_chunks": 1}

Idempotency-Key

让同一次写入可以安全重复。当重试来自你这边时使用 - 中断后重跑的任务、会重投的队列。

# same key + same body = the FIRST response is replayed, nothing is stored twice
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: import:row-42" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'
密钥要从被存储的对象派生(import:row-42),不要用常量:两次不同的写入复用同一密钥会重放第一次的响应,第二次会被悄悄丢弃。格式:1-128 个 [A-Za-z0-9._:-] 字符。

读取

search

语义搜索,最相关的排在最前。纯向量嵌入 - 任何语言都能找到任何记忆。

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does she drink?","max_results":1}'
真实响应
{"memories": [{"id": "576700aa-...", "content": "she prefers tea over coffee",
   "similarity": 0.63, "is_superseded": false}], "search_ms": 315, "total_found": 1}

search + filters

把搜索收窄到存储的一部分。在排序之前应用,因此得到的是过滤范围内最相关的结果 - 而不是对 top-N 再做过滤。

curl -X POST https://api.wontopos.com/api/v1/memory/search \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what did we decide",
       "filters":{"categories":["work"],"event_from":"2026-01-01","event_to":"2026-06-30"}}'
键:categories · event_from / event_to(内容发生的时间 - metadata.event_date)· time_from / time_to(写入的时间)· min_importance。未列出的键会被丢弃而不是报错,所以拼错会悄悄扩大搜索范围。

get

用 store 或 list 返回的 id 读取单条记忆 - 原文与元数据,不含向量。

curl -X POST https://api.wontopos.com/api/v1/memory/get \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","memory_id":"576700aa-f0e0-4c26-99a0-10e2d5b0d624"}'

list

按游标逐页浏览存储中的全部内容。可用于浏览或导出。

curl -X POST https://api.wontopos.com/api/v1/memory/list \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","limit":100}'   # pass next_cursor back for the next page
真实响应
{"count": 3, "memories": [{"id": "1a1cfc47-...", "content": "...", "category": "general",
   "created_at": "2026-07-31T18:04:51.937117314+00:00", "event_date": null, "is_superseded": false}],
 "next_cursor": "722c08e5-8998-4882-979e-d71995b5b4af", "user_id": "docs_livetest"}

recall

一次往返返回短期 + 长期 + 上下文 + 一条 instruction。直接粘贴进您的提示词即可。

curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does she drink?"}'
真实响应(结构)
{"short_term": {"count": 2, "turns": [...]},
 "long_term":  {"count": 4, "memories": [{"content": "she prefers tea over coffee", "similarity": 0.63}]},
 "context":    {"count": 4, "around_top_memory": ["[match] she prefers tea over coffee"]},
 "instruction": "Use short_term for recent context, long_term for relevant past memories..."}

删除

forget

按 id 删除单条记忆,或省略 id 以删除该用户的全部数据(GDPR)。

curl -X POST https://api.wontopos.com/api/v1/memory/forget \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'  # omit memory_id = delete all
真实响应
{"memories_deleted": 1, "status": "deleted", "user_id": "alice"}

全部端点与请求体字段 →

仅 Rust 的变体方法

Python 和 TypeScript 将这些作为可选参数。稳定版 Rust 没有默认参数和关键字参数,因此每一项都是独立的方法,而不是需要你收尾的构建器。

mem.add_with(text, None, json!({}), extra)   // add + extra body fields
mem.search_opts(q, None, 10, &opts)            // search + verify / max_images
mem.search_with(q, None, 10, extra)            // search + any other field
mem.recall_with(q, None, extra)                // recall + extra
mem.search_self_with(q, None, 10, extra)       // self lane + extra
mem.engram_with(name, q, None, extra)          // engram + extra
mem.update_idempotent(old, new, None, key)     // update + Idempotency-Key
mem.add_turn_idempotent(u, a, None, key)
mem.add_bulk_idempotent(text, None, cat, key)
mem.revisions_page(None, "revised", 20, None, None)
mem.list_all_images(None, None)              // = iter_images, collected

list_all_images 同时以 iter_images 的名称导出,与另外两个 SDK 使用的名称一致,从那些文档过来的读者会先输入这个名称。

Engram

Engrams

可供您的模型调用的召回工具 - 每一个都是作用于同一份记忆的不同检索策略。可单独使用,也可同时运行多个。

现已上线。下方的通用 engram 是不使用 LLM 的检索流水线,因此从 Tablet 1 起的每个层级都能运行。Memoir 与 Archive 是独立的模型模式,在下方单独的章节中介绍。

新的 engram 会持续发布 - 这份列表会不断增长。

Memoir 与 Archive Scroll 1.2+

这是一种交付形式,而不是可调用的工具。在 Scroll 1.2 及以上,按调用选择 form: "memoir"form: "archive",那次召回 - 包括普通搜索 - 返回的时间就会以该方式书写。

全部 engram Engram

Time_awareness Scroll 1.2+

按调用选择的交付形式。在支持形式的模型(Scroll 1.2 及以上)的任意调用中传入 form - memoirarchive - 该次响应就会以对应方式呈现:普通搜索、recall 或任何 engram。SDK 中是与 tz 一样的 form 字段;HTTP 中是 X-WOS-Form 请求头。Memoir 读起来像人的回忆;Archive 保持精确的记录 - 两者的差异最明显地体现在时间的写法上。

Memoir

form: "memoir"
如人所忆 · 一段叙事

讲述发生了什么、一个瞬间如何引向下一个,带着人回忆时那种柔和的时间感 - 读起来是经历,而不是清单。

Archive

form: "archive"
作为记录保存 · 精确时间

以精确记录的形式返回匹配 - 精确的经过时间和绝对时间锚点,结构化到模型可以直接读取。

它只呈现您已存储的记忆 - 不会创造记忆。每条记忆都是在某个 user_id 下的一次 store / add 调用(那个 user_id那个人的存储库)。先存储;之后任何召回 - 包括下面的普通搜索 - 都会带上时间标注。存储方法见快速上手
# the memoir form on a plain search — and on recall, the LLM's one-call context
r   = mem.search("what does Alice drink?", user_id="alice", model="scroll-1.2", form="memoir", tz=9)
ctx = mem.recall("what does Alice drink?", user_id="alice", model="scroll-1.2", form="memoir", tz=9)
# every memory's .time reads "a couple weeks ago" (archive → "2 weeks ago (Jun 09)") — the LLM sees human time
// the memoir form on search — and on recall, the LLM's one-call context
const s = await mem.withModel("scroll-1.2").search("what does Alice drink?", "alice", 10, { form: "memoir", tz: 9 });
const ctx = await mem.withModel("scroll-1.2").recall("what does Alice drink?", "alice", { form: "memoir", tz: 9 });
// form on search AND recall — the _with helpers merge extra fields into the body
let s = mem.with_model("scroll-1.2").search_with("what does Alice drink?", "alice", 10, json!({"form": "memoir", "tz": 9})).await?;
let ctx = mem.with_model("scroll-1.2").recall_with("what does Alice drink?", "alice", json!({"form": "memoir", "tz": 9})).await?;
# same X-WOS-Form header on /search, /recall, or /engram/run
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: wos-live-..." -H "X-WOS-Model: scroll-1.2" -H "X-WOS-Form: memoir" -H "X-WOS-Timezone: 9" \
  -d '{"user_id":"alice","query":"what does Alice drink?"}'
# every memory comes back with a "time" field; use X-WOS-Form: archive for exact time

tz 是调用方的 UTC 偏移小时数 - 这样“今天早上”和凌晨 4 点的日界线都落在用户的本地时间。省略则为 UTC;HTTP 下对应 X-WOS-Timezone 请求头。按地区粗略对照:美国东部 -5、美国中部 -6、美国西部 -8 · 英国 / 里斯本 0 · 中欧 +1 · 东欧 +2 · 印度 +5.5 · 中国 / 新加坡 +8 · 韩国 / 日本 +9 · 悉尼 +10。(均为标准时间 - 夏令时会让部分地区 +1;请传用户实际所处的偏移。)

同一次搜索,两种形式 - 记忆完全相同,只有 time 不同:

结果 · form: memoir
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "a couple weeks ago" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday afternoon" },
  { "content": "Alice moved to Brooklyn",          "time": "about half a year ago" }
] }
结果 · form: archive
{ "count": 3, "memories": [
  { "content": "Alice prefers tea over coffee", "time": "2 weeks ago (Jun 09)" },
  { "content": "met Alice at the cafe downtown",  "time": "yesterday at 14:00" },
  { "content": "Alice moved to Brooklyn",          "time": "6 months ago (Dec 2025)" }
] }
经过时间MemoirArchive
3 分钟a few minutes ago3 minutes ago
14 分钟about 15 minutes ago14 minutes ago
30 分钟half an hour ago30 minutes ago
50 分钟about an hour ago50 minutes ago
2 小时a couple hours ago2 hours ago, at 13:10
8 小时this morning8 hours ago, at 07:10
昨天下午yesterday afternoonyesterday at 14:00
昨晚last night17 hours ago, at 22:00
2 天a couple days ago2 days ago (Tue 15:10)
6 天several days ago6 days ago (Fri 15:10)
9 天about a week agolast week (Jun 16)
16 天a couple weeks ago2 weeks ago (Jun 09)
35 天about a month agolast month (May 21)
60 天a couple months ago2 months ago (Apr 2026)
180 天about half a year ago6 months ago (Dec 2025)
380 天about a year agolast year (Jun 2025)
800 天a couple years ago2 years ago (Apr 2024)
1500 天about 4 years ago4 years ago (May 2022)

上表每个值都是渲染器的真实输出。看看两行“昨天”:Memoir 把昨天下午和昨晚分开 - 一天以一次睡眠为界 - 而 Archive 只写一个钟点,不划分白天与黑夜。

两种模式如何解读时间

Memoir - 人们实际的说法。近期时刻保持相当清晰(大约 15 分钟半小时),越往前措辞越宽泛 - 两周左右大约半年两三年前 - 就像记忆本身随着距离而松弛。在一天之内,它舍弃钟点而使用时间地标:今天早上昨晚昨天下午。而且一天以一次睡眠为界,不按日历跳变:日界线大约在本地时间凌晨 4 点,因此深夜仍算同一个晚上,而不是已经到了明天。

Archive - 精确,且始终带锚点。每一行都带有精确的经过时间和可供模型计算的绝对参照,而且越近锚点越细:今天用钟点(8 小时前,07:10),本周用星期加钟点(2 天前(周二 15:10)),本月用日期(上周(6 月 16 日)),更早则用年月(6 个月前(2025 年 12 月))。绝不含糊,绝不出错。

Memoir 与 Archive 会渲染响应中的每一次召回 - 普通搜索、recall 或 engram 都适用。模型层级(Tablet → Scroll → Book)决定引擎做多少事;形式(memoir / archive)决定时间怎么写。Scroll 1.2 及以上可用。
全部 engram Engram

deep_recall

多跳召回。先搜索您的查询,再取最佳匹配并以它的内容再次搜索 - 带回单次搜索会遗漏的关联上下文。当记忆相互引用时效果最佳(一个人 → 其项目 → 具体细节)。最多返回约 12 条。

out = mem.engram("deep_recall", "what should I know about Alice?", user_id="alice")
const out = await mem.engram("deep_recall", "what should I know about Alice?", "alice");
let out = mem.engram("deep_recall", "what should I know about Alice?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"deep_recall","user_id":"alice","query":"what should I know about Alice?"}'
响应
{ "engram": "deep_recall", "hops": 2, "count": 12,
  "memories": [ ... ],
  "usage": { "input_tokens": 200, "output_tokens": 589 } }
按 token 计量 - 每次调用都返回 usage(输入 + 输出),使用与 API 其他部分相同的分词器统计;没有隐藏的按 engram 收费。需要同时使用多个?并发调用即可 - 每个 engram 都是独立请求。
全部 engram Engram

timeline

按时间排序的召回。事件发生时间由新到旧返回记忆,而非按相关性。适合“X 是什么时候”、历史与顺序类问题。最多返回 15 条。

events = mem.engram("timeline", "project milestones", user_id="alice")
const events = await mem.engram("timeline", "project milestones", "alice");
let events = mem.engram("timeline", "project milestones", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"timeline","user_id":"alice","query":"project milestones"}'
响应
{ "engram": "timeline", "hops": 1, "count": 15,
  "memories": [ ... ],
  "usage": { "input_tokens": 100, "output_tokens": 736 } }
按 token 计量 - 每次调用都返回 usage(输入 + 输出),使用与 API 其他部分相同的分词器统计;没有隐藏的按 engram 收费。需要同时使用多个?并发调用即可 - 每个 engram 都是独立请求。
全部 engram Engram

gather

广域收集。先搜索,再围绕前三个最佳匹配展开 - 比 deep_recall 撒得更宽。用它把与某个人、项目或主题相关的一切一次调用全部带回。最多返回约 18 条。

related = mem.engram("gather", "everything about Project Atlas", user_id="alice")
const related = await mem.engram("gather", "everything about Project Atlas", "alice");
let related = mem.engram("gather", "everything about Project Atlas", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"gather","user_id":"alice","query":"everything about Project Atlas"}'
响应
{ "engram": "gather", "hops": 4, "count": 18,
  "memories": [ ... ],
  "usage": { "input_tokens": 400, "output_tokens": 637 } }
按 token 计量 - 每次调用都返回 usage(输入 + 输出),使用与 API 其他部分相同的分词器统计;没有隐藏的按 engram 收费。需要同时使用多个?并发调用即可 - 每个 engram 都是独立请求。
全部 engram Engram

equilibrium

漂移校正。 语义检索会随着对话变长而收窄:查询携带当前状态,于是只召回同一状态的记忆,下一轮又更偏向那一侧。本引擎沿三个查询无法支配的轴重新展开结果:跨时间的分散、远离查询的联想,以及库中真正有内容的部分。当回复开始重复或变得平淡时使用。若要查具体事实,请用更贴近查询的 deep_recall 或 gather。最多返回 12 条。

wide = mem.engram("equilibrium", "how have things been lately?", user_id="alice")
const wide = await mem.engram("equilibrium", "how have things been lately?", "alice");
let wide = mem.engram("equilibrium", "how have things been lately?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"equilibrium","user_id":"alice","query":"how have things been lately?"}'
响应
{ "engram": "equilibrium", "hops": 3, "count": 12,
  "memories": [ ... ],
  "usage": { "input_tokens": 300, "output_tokens": 293 } }
按 token 计量 - 每次调用都返回 usage(输入 + 输出),使用与 API 其他部分相同的分词器统计;没有隐藏的按 engram 收费。需要同时使用多个?并发调用即可 - 每个 engram 都是独立请求。
全部 engram Engram

tone_stabilizer

自己的声音。 长对话会把助手带离平常的语气:回复变长、变成报告,或染上最近一段的情绪。普通的自我召回只会让情况更糟,因为它匹配当前状态,把最近的发言当成性格返回。本引擎返回的是那一段之前的自身发言。需要以 speaker me 保存过的发言;若没有,则返回空而不做猜测。最多返回 10 条。

# store the assistant's turns as speaker "me", then pull its own register back
mem.add("I keep answers short unless you ask for detail.", user_id="alice", speaker="me")
mine = mem.engram("tone_stabilizer", "how do I usually answer?", user_id="alice")
await mem.add("I keep answers short unless you ask for detail.", "alice", { speaker: "me" });
const mine = await mem.engram("tone_stabilizer", "how do I usually answer?", "alice");
mem.add("I keep answers short unless you ask for detail.", "alice", json!({"speaker": "me"})).await?;
let mine = mem.engram("tone_stabilizer", "how do I usually answer?", "alice").await?;
curl -X POST https://api.wontopos.com/api/v1/engram/run \
  -H "X-API-Key: wos-live-..." -H "Content-Type: application/json" \
  -d '{"name":"tone_stabilizer","user_id":"alice","query":"how do I usually answer?"}'
响应
{ "engram": "tone_stabilizer", "hops": 2, "count": 10,
  "memories": [ { "content": "I keep answers short unless you ask for detail.", "speaker": "me" }, ... ],
  "usage": { "input_tokens": 200, "output_tokens": 442 } }
按 token 计量 - 每次调用都返回 usage(输入 + 输出),使用与 API 其他部分相同的分词器统计;没有隐藏的按 engram 收费。需要同时使用多个?并发调用即可 - 每个 engram 都是独立请求。
HTTP API

所有端点,一个基础 URL。

无需 SDK - 任何 HTTP 客户端都可用。基础 URL 为 https://api.wontopos.com,通过 X-API-Key 请求头认证,请求与响应均为 JSON。记忆操作均为 POST;存储库管理使用 /collection 上的 POST / GET / DELETE。存储库必须先存在(见存储库),否则库内操作返回 404

请求头

请求头作用
X-API-Key每次调用都必需。您的密钥,在控制台签发。
X-WOS-Model可选。选择由哪个引擎作答。省略则使用账户的默认值。GET /api/v1/models 会列出您的密钥可以选择的模型;更早的引擎无法提供的端点会返回 501,并指出是哪个模型。
Idempotency-Key写入时可选。同一密钥配同一请求体时不会重复存储,而是重放首次响应 - 见下面的说明。

端点

端点用途请求体字段
POST /api/v1/memory/collection创建存储库user_id
GET /api/v1/memory/collections列出您的存储库(无)
DELETE /api/v1/memory/collection删除存储库及其全部记忆user_id
/api/v1/memory/store存储一条记忆user_id · content · metadata? (event_date · speaker) · image?
/api/v1/memory/store-turn存储一轮对话user_id · user_msg · assistant_msg
POST /api/v1/memory/speakers注册说话者(显式,最多 50 人)user_id · speaker
GET /api/v1/memory/speakers列出已注册说话者 + 记忆数user_id
DELETE /api/v1/memory/speakers注销说话者(记忆保留)user_id · speaker
/api/v1/memory/by-speaker某个人说过的话,从最新开始(“me” = 智能体自己)user_id · speaker · limit? · before? · skip_ids?
POST /api/v1/memory/image图像记忆的原始字节user_id · memory_id
DELETE /api/v1/memory/image删除图像,保留文字user_id · memory_id · preview?
/api/v1/memory/images某个存储的图像,从最新开始(+ 总数)user_id · limit? · before? · skip_ids?
/api/v1/memory/lineage一条记忆的修改链,从最早开始user_id · memory_id
/api/v1/won/revisions某个存储被改写了多少。免费user_id · include? · limit? · before? · skip_ids?
/api/v1/memory/revisions同一调用在 memory 平面下的名字。免费user_id · include? · limit? · before? · skip_ids?
/api/v1/memory/bulk-store回填一段文本user_id · content · category? · timestamp?
/api/v1/memory/search语义搜索user_id · query · max_results? · speaker? · cache_control? · filters? · verify? · max_images?
/api/v1/memory/recall短期 + 长期 + 上下文user_id · query · limit? · context_limit?
/api/v1/memory/get按 id 取单条记忆user_id · memory_id
/api/v1/memory/list分页浏览存储user_id · limit? · cursor?
/api/v1/memory/history最近的对话轮次user_id
/api/v1/memory/stats记忆条数统计user_id
/api/v1/memory/supersede替换已变更的事实user_id · old_memory_id · new_content
/api/v1/memory/forget删除一条(或全部)user_id · memory_id? (省略 = 全部删除)
GET /api/v1/engram该模型可运行的 engram(无)
POST /api/v1/engram/run运行一个 engramname · user_id · query · form? · tz?
GET /api/v1/models可用模型(无)
写入请求接受 Idempotency-Key 头。同一密钥配同一请求体时不会重复存储,而是重放首次响应(10 分钟);同一密钥配不同请求体则返回 422。仅缓存 2xx,因此失败的调用可以立即重试。
# create the store once (stores are explicit)
curl -X POST https://api.wontopos.com/api/v1/memory/collection \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# store a memory
curl -X POST https://api.wontopos.com/api/v1/memory/store \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","content":"she prefers tea over coffee"}'

# recall - one call, ready for your prompt
curl -X POST https://api.wontopos.com/api/v1/memory/recall \
  -H "X-API-Key: $WOS_API_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","query":"what does alice drink?"}'
真实响应 - store
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}
用量等级

人人功能相同。
等级只提升您的限额。

每个等级都运行完整引擎 - 相同的召回质量、相同的语言支持、全部方法。随着累计信用额度购买的增长,等级会自动提升至 Tier 5,无需申请或联系销售。企业版(Tier 6)是唯一的例外。

消费限额

每个等级限定您每个日历月的消费上限。累计信用额度购买达到下一档门槛时立即升级。

用量等级信用额度购买每月消费限额
Tier 1$5$100
Tier 2$40$500
Tier 3$200$1,000
Tier 4$400$5,000
Tier 5$1,000$25,000
Tier 6 - Enterprise联系我们无限制

速率限制

速率限制按账户计 - 账户下的所有 API 密钥共享同一限额,并随等级提升。超限会返回 429retry-after 响应头;请退避(1s → 2s → 4s)后重试。所有端点都是幂等友好的,重试是安全的。

等级每分钟请求数
Tier 1150
Tier 2300
Tier 3600
Tier 41,500
Tier 53,000
Tier 6 - Enterprise自定义

企业版(Tier 6)可获得自定义速率限制、SLA、专属支持以及可选的自托管许可 - 联系我们

免费调用

有几个端点完全不收费 - 它们汇总在 Won 之下。它们没有价格,取而代之的是两个上限。

  • 每个端点每分钟 10 次请求。每个免费端点各自维护一个配额桶,消耗其中一个不会消耗另一个。
  • 每小时 300 次请求,共享。所有免费端点共用每个账户的同一份小时配额。

常规使用中两者都不会触及,且都不影响上面的付费限额。

定价按用量计费:token 用量加每次请求固定 $0.0001。Tablet 为每 1M 输入 token $2、每 1M 输出 token $3。存储免费且无上限。参见我们为何这样定价
错误与限制

出错时会发生什么。

错误以 JSON 信封返回,包含稳定的 type、面向人的消息,以及报告问题时可提供给我们的 request_id

真实响应 - 无效密钥(HTTP 401)
{"type": "error", "error": {
   "type": "authentication_error",
   "message": "Invalid or revoked API key.",
   "request_id": "063f8b83-eee2-4383-a5cf-11e4bcd29d7c"
 }}
HTTP含义处理方式
400请求体格式错误(字段缺失或类型错误)错误消息会指明具体字段 - 修正后重试。
401API 密钥无效或已吊销检查密钥;在控制台签发新密钥。
402余额不足、未绑定银行卡,或触及等级上限在控制台充值或绑定银行卡。响应中带有 balance_centsfloor_cents,可据此判断是哪一项拦住了您。
404不存在该记忆、存储库或图像检查 id。get_image 在记忆存在但不带图像时同样返回 404。
409该名称已被占用存储库和工作区的名称在账户内唯一 - 请另选一个。
413请求体超过 10MBBase64 比它编码的文件大约大 33%,所以请先把图像缩小再编码。
429触发速率限制SDK 已经替您重试过了,带退避与抖动,并遵守 Retry-After。收到它说明重试已经用尽 - 请降低并发,而不是自己再套一层循环。
501该模型的引擎未实现此端点图像和修订历史需要更新的引擎。GET /api/v1/models 会列出各个模型分别支持什么。
5xx服务端问题退避后重试,但不要盲目重试。此 API 的每一次调用都是 POST,服务端可能已经存下了您的请求,因此 SDK 不会自动重试 5xx。重发时请带上幂等键,让重复请求无法写入两次;联系我们时请附上 request_id

每个错误都是 WosError,同时每个状态还各有自己的类 - BadRequestErrorAuthenticationErrorPaymentRequiredErrorNotFoundErrorConflictErrorRateLimitErrorServerErrorAPIConnectionError。请捕获您真正要处理的那一个,而不是去比较数字。

# SDK error handling (Python)
from wontopos import Client, WosError, RateLimitError, PaymentRequiredError

try:
    mem.search("...", user_id="alice")
except PaymentRequiredError: ...      # 402 - top up
except RateLimitError: ...            # 429 - the SDK already retried; slow down
except WosError as e: ...            # e.status, e.message, e.request_id
except (ValueError, TypeError): ...    # never left the client

有些错误根本到不了我们这里。API 密钥、存储库 id、幂等键和图片都在请求发出之前完成校验,此时抛出的是 ValueErrorTypeError,而不是 WosError。单靠 except WosError 抓不到它们。

密钥安全。密钥仅在创建时显示一次,我们侧只保存其哈希。请保存在环境变量中;如有泄露,在控制台吊销 - 吊销立即生效。

速率限制按账户计,由您的所有密钥共享,并随等级提升 - 见用量等级。账户用量可在控制台查看。

开发者

lineage

一条记忆背后的修改链,按时间由旧到新。revisions 说明一个存储库整体改动了多少,这里说明单个事实发生了什么。

Tablet 2 及更新版本支持。只读。与 revisions 不同,这是一次正常计费的调用,因为它会返回记忆内容。

传入该链中任意一条记忆的 id。被取代的版本会保留而非删除,因此即使检索只返回当前的事实,也仍可回溯。

chain = mem.lineage(memory_id=mid)["chain"]
for step in chain:
    print(step["changed_at"], step["action"], step["content"])
const { chain } = await mem.lineage(undefined, mid);
for (const step of chain) {
  console.log(step.changed_at, step.action, step.content);
}
let r = mem.lineage(None, mid).await?;
for step in r["chain"].as_array().unwrap_or(&vec![]) {
    println!("{} {} {}", step["changed_at"], step["action"], step["content"]);
}
curl -X POST https://api.wontopos.com/api/v1/memory/lineage \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice","memory_id":"m_9"}'
200
{ "memory_id": "m_9", "count": 3, "truncated": false,
  "chain": [
    { "memory_id": "m_3", "content": "lives in Seoul",
      "created_at": "2026-03-02T…", "changed_at": "2026-06-11T…",
      "action": "replaced", "confidence": 0.94,
      "superseded_by": "m_7", "is_current": false },
    { "memory_id": "m_7", "content": "moved to Busan",   … },
    { "memory_id": "m_9", "content": "Haeundae, specifically",
      "changed_at": null, "superseded_by": null, "is_current": true }
  ] }
字段作用
chain各个版本,按时间由旧到新。每个版本包含与记忆相同的字段,另加下面四个。
changed_at该版本被取代的时间(RFC3339),若仍然生效则为 null
action该环节发生了什么,即替代版本与此版本之间的关系。
confidence引擎对该关系的确信程度,取值 0-1。
is_current对仍然生效的那个版本为 True。每条链中恰好有一个。
truncated当链的长度超过服务遍历的范围时为 True。返回的仍然是最早的那些环节。

用途

两种用途。调试:一条记忆为何是今天这个样子。以及让助手查看自身的历史,被修正过三次的事实与只写入过一次的事实属于不同类型,只有修改链能体现这一点。

Won

Won 是为读取记忆的一方准备的。

这个 API 的大部分是记忆作答。Won 则是关于记忆作答:一个存储库被改写了多少,又能信任到什么程度。它面向的是读取记忆的一方,通常是您正在构建的助手,而不是记忆所描述的那个人。

Wontopos 就是 Won + Topos,记忆栖身的那一个地方。Won 是这个地方里报告记忆状况、而不是把记忆返回来的那一部分。这些调用免费、只读,且从不触碰检索:询问不会让您的用户多花一分钱,也不会改变已经记住的内容。

目前提供的内容

目前只有一个调用。

调用作用
POST /won/revisions该存储库自写入以来有多少内容被改动。返回两个数字以及两句说明。

完整示例

使用比例,而非原始计数。40 条中的 3 条与 40 条中的 30 条需要不同的处理方式。

r = mem.revisions()
# {"revised": 3, "total": 40, "counts": "…", "excludes": "…"}

if r["revised"] / r["total"] > 0.1:
    system += "Some of what you remember here has been corrected since."
const r = await mem.revisions();
// { revised: 3, total: 40, counts: "…", excludes: "…" }

if (r.revised / r.total > 0.1) {
  system += "Some of what you remember here has been corrected since.";
}
let r = mem.revisions(None).await?;
let (rev, tot) = (r["revised"].as_f64().unwrap_or(0.0),
                r["total"].as_f64().unwrap_or(1.0));
if rev / tot > 0.1 { /* say so in the system prompt */ }
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" -H "Content-Type: application/json" \
  -d '{"user_id":"alice"}'

# → {"user_id":"alice","revised":3,"total":40,
#     "counts":"memories a transform has touched (supersede, update, retract, image removed)",
#     "excludes":"deletions — a deleted memory leaves nothing to count"}

countsexcludes 以句子而非标志位的形式返回,因为调用方通常是模型。删除不计入统计。

价格与限额

规则
价格无。免费调用会跳过计费环节,不收取 token 费用,不收取按请求计的费用,也不记录用量。
每分钟每分钟 10 次,按账户并且按端点计。消耗某个端点的每分钟配额,不会消耗另一个端点的配额。
每小时每小时 300 次,按账户计,由所有免费调用共享。该限额不区分路径,因此新增免费端点不会提高一个账户可消耗的总量。
与付费流量的关系双向隔离。这些调用不会拖慢你的检索,你的检索也不会耗尽这些配额。同一账户下的多个密钥共用同一批配额桶,因此持有更多密钥不会成倍增加额度。

两个上限都会返回 429,附带以秒为单位的 Retry-After,并在消息中指明触发的是哪一个。

429 rate_limit_error
Retry-After: 41

{ "error": { "type": "rate_limit_error",
    "message": "This endpoint is free and limited to 10 requests per
                minute, counted per endpoint. Retry in 41s." } }
同一调用也可在 /api/v1/memory/revisions 上响应,用于 Won 接口出现之前发布的客户端。它是同一个处理器、同一份预算,而不是第二份额度。新代码应使用 Won 地址。
Won · revisions

一个存储库被改写了多少

revisionsrevisedtotal 作答:一个存储库里的记忆,有多少在写下之后被改动过。在把要紧的事托付给记忆之前值得问一问,或者当召回的事实和用户此刻说的话对不上时问一问。十条事实里有三条被替换过的存储库,理应比无人改动过的那一个更少被信任。

Tablet 2 及以上支持。可通过 HTTP API、Python、TypeScript 和 Rust SDK,以及作为 MCP 工具调用。更早的引擎会返回 501,并指出是哪个模型无法提供。

计数

它数的是变换动过的东西 - 取代、更新、撤回,以及被删除的图像。

mem.revisions()
# {"revised": 3, "unrevised": 37, "total": 40, …}
await mem.revisions();
mem.revisions(None).await?;
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" -d '{"user_id":"alice"}'
字段含义
revised变换动过的记忆数。
unrevised自写入以来未被改动过的记忆。revised + unrevised 始终等于 total,该值是推导得出的,并非单独统计,因此并发写入不会让这三个数字互相矛盾。
total存储库中的记忆数。
counts / excludes以普通句子而非标志位说明这些数字涵盖的范围。调用方通常是模型。

读取列表

传入 include 可获取记忆本身,而不只是数量。省略时只返回计数,这是开销较低的调用。

page = mem.revisions(include="revised", limit=20)
page["memories"], page["matched"], page["has_more"]
const page = await mem.revisions(undefined, { include: "revised", limit: 20 });
let page = mem.revisions_page(None, "revised", 20, None, None).await?;
curl -X POST https://api.wontopos.com/api/v1/won/revisions \
  -H "X-API-Key: $WOS_KEY" \
  -d '{"user_id":"alice","include":"revised","limit":20}'
字段作用
include"revised""unrevised"。其他任何值都会以 400 拒绝,而不是退回为只返回计数,因为静默丢掉列表的拼写错误,看起来与一个空存储库完全一样。
limit5 到 20,默认 20。超出范围或类型错误会被拒绝,而不是截断到边界值。
matched本页背后的总行数,而非本页的条数。
ordered_by服务会声明自身的排序方式:按存储时间由新到旧,而非按最近编辑时间。
next_before下一页的游标,与 next_skip_ids 配合使用。两者都要回传,id 会跨页累积。
列表按记忆的存储时间排序,而非按改动时间。如果调用方假定为“按最近编辑时间排序”,就会读错这一页,因此响应中会说明采用的是哪一种。
删除不计入。被删掉的记忆没有留下可数的东西,所以一个大量清理过的存储库,revised 依然报得很低。这个数字告诉您的是被改写了多少,而不是消失了多少。
规模

超越上下文窗口。

WOS 可从 1.4M token 的历史中召回记忆 - 这远大于任何 LLM 的上下文窗口 - 却依然只交回约 1,470 个 token 的紧凑切片。

您智能体的记忆不受提示词容量的限制。它保留一切,只检索真正重要的部分,无论历史增长到多大。

隐私

私密,且属于您。

您的数据只属于您的存储库。我们绝不用它训练、查看或复用 - 只负责组织它,让您能够检索。

  • BYOK。您的 LLM 密钥随每次请求传递,绝不存储。
  • 隔离。记忆先按账户、再按 user_id 隔离。
  • GDPR 删除与自托管。一次调用即可抹除一个用户;如有需要,也可在您自己的环境中运行引擎。