Long-term memory for AI agents.
WOS is a memory API. You store a user's memories once, then recall only the relevant ones for each query and pass them to your model's prompt.
Retrieval is purely semantic, with no keyword or BM25 matching, so recall quality is identical across languages. Each query returns a small, bounded context no matter how much you have stored, and no model is ever run over your stored memories.
Core operations
store- save a memory for a user.recall- get the relevant memories for a query. This is the main call.search- raw semantic search over stored memories.supersede- update or replace a memory that is out of date.forget- delete a single memory or an entire user (GDPR).
Three models, one lineage.
WOS models are named for how people have kept knowledge through history - Tablet, Scroll, Codex. Stone, scroll, bound book: each one does more for your agent than the last.
Tablet
LiveA lean, fast, low-cost way to inscribe and recall memory - the foundation every model builds on.
Scroll
LiveAdds a language model to read your question more closely and bring back a fuller context, so scattered evidence comes back together rather than one piece short.
Codex
NextOpens to the right page on its own - choosing the memory and tools each moment needs, and getting sharper the more it's used.
Tablet 1's full benchmark report is on the benchmark page.
Pay us $2. Save many times that on your LLM.
WOS feeds your LLM ~1,200 tokens per query - a bounded, relevant slice - instead of stuffing the full history into every prompt. The gap is enormous, and it grows with your history.
Every $1 spent on WOS saves ~$98 on the LLM. Bigger history or a pricier model → bigger ROI.
Where the savings come from
- Without WOS you stuff the whole history into each prompt -
100K tokens × $2.50/1M = $0.25per query, at GPT-4o input rates (about 2× that on Opus-tier models). - With WOS you ingest once (
$2/1M), then each query is a tiny retrieve ($3/1M × 1,200) plus your LLM on just ~1,200 tokens. - The fewer tokens your LLM reads, the less you pay - and WOS keeps that number flat as memory grows.
Every language, the same accuracy.
Retrieval is pure semantic - embeddings only, zero keyword or BM25 matching. So recall quality is identical whether your users write in 日本語, 中文, Español, or English.
Lexical matching like BM25 is tuned to the shape of a particular language - morphology, spacing, script. In a multilingual store that means retrieval quality varies by language. WOS uses no lexical matching at all, so every language goes through the same path.
One store, three languages at once
You don't pick a language per store - mix them freely. Below, one user's memory holds Japanese, English, and Spanish at the same time, and every question finds the right memory regardless of language. This is a real exchange against the live 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
No translation step, no language detection, no per-language config. Memories and questions are placed by meaning, not by language - if the meaning matches, the language doesn't matter.
Why we banned keywords on purpose
Lexical scoring such as BM25 strengthens retrieval for some languages more than others, which gets in the way when one store holds many languages. So we removed it from the engine entirely and enforce that rule in code review: with any lexical scoring in the path, recall quality would differ by language.
No model runs over your memories.
Storage is verbatim and the engine searches by embeddings - cheap, fast, and deterministic. A model is never run over your stored memories. Tablet uses no model at all; Scroll and Codex add one around the engine for stronger results, but it only ever sees your query, never what you stored.
- Deterministic engine. The engine returns the same memories for the same query, every time - which is why our benchmark variance comes only from the reader model.
- Cheap at scale. No generation cost to store or retrieve, so your bill tracks storage - not model usage - as memory grows.
Your words, untouched
One common design runs a language model at write time to extract and rewrite "facts" from the text. That design trades three things: generation cost on every write, added latency, and storage of a model's paraphrase rather than the original words. WOS makes the opposite trade - it stores what was said, unchanged, and lets your LLM do the interpreting at read time with the original text in hand.
90.7%, measured and reproducible.
90.7% on LongMemEval-S, averaged over 5 independent runs (σ 0.5%, none cherry-picked), graded by the benchmark's canonical GPT-4o judge.
On the same benchmark, scores vary widely with the grading protocol - the judge, the prompt, and what the retrieval layer is allowed to do. We use the benchmark's canonical third-party GPT-4o judge as published, change nothing to fit the test, and publish the scoring code and reader prompt so anyone can reproduce the 90.7% exactly.
The protocol, in one table
| Item | What we do |
|---|---|
| Dataset | LongMemEval-S (cleaned), ~240K tokens of history per question |
| Judge | The benchmark's canonical GPT-4o judge - a third party, not us |
| Runs | 5 independent runs, every score published, mean reported (σ 0.5%) |
| Reader | Fixed reader model and prompt, published verbatim |
What keeps it honest: a third-party judge, the reader prompt published unchanged, purely semantic retrieval, and every run reported - not just the best. The retrieval engine is deterministic - run it again and you get the same memories.
We climb harder benchmarks
We test on the hardest standard benchmark we haven't yet conquered - and the number is the high-water mark across every WOS model, rewritten each time a better one ships. Clear 94%, and we graduate to a harder benchmark.
Two token rates per model,
plus $0.0001 per request.
Per million tokens plus a flat $0.0001 per request, pay as you go. No subscription, no storage rent, no memory caps. You pay when your agent writes or reads - never for what it remembers.
| Model | Input / 1M | Output / 1M | |
|---|---|---|---|
| Tablet 1 | $2 | $3 | Live |
| Scroll 1 | $4 | $8 | Live |
| Codex 1 | - | - | TBD |
- $0.0001 per request. A flat fee on every API call, on top of token usage.
- Storage is free. Ingest pays once; keeping it costs you nothing. No count limit, no retention limit.
- We store it. We never train on it, use it, or look at it. Your agent's memory is yours - we only organize it so you can retrieve it.
- Why Tablet is this cheap: its engine runs no model, so our cost is embeddings and disk - not GPUs. Scroll and Codex add a model, which is what their higher price covers.
Three calls: store, recall, answer.
One API. The recall() call returns short-term, long-term, and surrounding context in a single round-trip, ready to drop into your prompt.
Store
add() your user's facts and turns. Embedded on the way in - no LLM.
Recall
recall() returns short-term + long-term + context in one call - a bounded, fixed-size context.
Answer
Feed that bounded context to your LLM - any provider, your key.
from wontopos import Client mem = Client(api_key="wos-...") mem.add("she prefers tea over coffee", user_id="alice") # one call: short + long + context ctx = mem.recall("what does alice drink?", user_id="alice")
Your first recall in 5 minutes.
One key, one install line, three calls - your agent has memory. Every snippet on this page was actually run; responses are shown verbatim.
Get an API key
Create one in the console. A 155-character key starting with wos-live- is shown once. Keep it in an environment variable - never in code.
Install
pip install wontopos # Python npm install wontopos # TypeScript / JavaScript cargo add wontopos # Rust # curl - nothing to install, just set WOS_API_KEY
Create a store, then store & recall
A store is the user_id you read and write under. Stores are explicit: create one first (the call below), then store and recall under it. Store - embedded on the way in, no LLM call. Recall - short-term + long-term + context in one round-trip.
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?"){"user_id": "alice", "status": "created"}{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}user_id to the client and every call uses it - no need to repeat it; override any single call by passing user_id to it. Stores are explicit: storing into or recalling from a store that doesn't exist returns 404 - create it first. Every account starts with a default store, so with no user_id at all the zero-setup path just works. See Stores to list and manage them.recall() returns four blocks - short_term (recent turns), long_term (relevant memories), context (what surrounded the best match), and an instruction telling the LLM how to use them. Drop the whole thing into your prompt.
Stores - create, list, delete.
A store is the user_id you read and write under - one isolated memory space per end-user, agent, or topic. Stores are explicit: create one before you store into or recall from it, or the call returns 404. Every account starts with a default store, so you can begin without a create call.
mem.create_store("alice") # create (idempotent)
mem.list_stores() # [{"user_id","created_at"}, ...]
mem.delete_store("alice") # delete the store + all its memories{ "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") to keep each person's memory separate, or a single default store for a personal agent. You can also create and browse stores in the console (Memory ids → Issue) without writing code. Deleting a store is permanent - it drops every memory under it.Python - every method, three groups.
Write, read, delete. Every example below was run against the live API on 2026-06-10; responses are verbatim.
pip install wontopos
from wontopos import Client mem = Client(api_key="wos-live-...") # or read from an env var
Choose a model
The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default on the client; override a single call by passing 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="tablet-1") # or pick a model per call
list_models ✓ live-tested
The catalog - the ids you can pass to model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.
mem.list_models()[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"}]The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.
Write
add ✓ live-tested
Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.
mem.add("she prefers tea over coffee", user_id="alice")
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}add_turn ✓ live-tested
Store one conversation turn (user + assistant) into short-term and long-term memory at once.
mem.add_turn("alice", "hi", "hello!")
{"status": "ok"}add_bulk ✓ live-tested
Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.
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 ✓ live-tested
A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.
mem.update("alice", old_memory_id="576700aa-...", new_content="she switched to coffee this year")
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
"old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}Read
search ✓ live-tested
Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.
r = mem.search("what does she drink?", user_id="alice", limit=1)
{"memories": [{
"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-06-10T04:20:39.688276876Z"
}], "search_ms": 315, "total_found": 1}| Field | Meaning |
|---|---|
| similarity | Raw embedding similarity to your query (0–1). |
| is_superseded | True if this fact was replaced by update(). |
| search_ms | Server-side retrieval time. |
recall ✓ live-tested
One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.
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 ✓ live-tested
Recent conversation turns (short-term memory), oldest first.
turns = mem.history("alice")
{"count": 2, "turns": [
{"role": "user", "content": "hi", "timestamp": "2026-06-10T04:20:40.989011337Z"},
{"role": "assistant", "content": "hello!", "timestamp": "2026-06-10T04:20:40.989013416Z"}
], "user_id": "alice"}stats ✓ live-tested
Memory counts for one user.
mem.stats("alice")
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}Delete
delete ✓ live-tested
Delete a single memory by id.
mem.delete("alice", memory_id="576700aa-...")
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}delete_all ✓ live-tested
Erase everything for one user - one call, GDPR-ready.
mem.delete_all("alice")
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}TypeScript - every method, three groups.
Write, read, delete. Every example below was run against the live API on 2026-06-10; responses are verbatim.
npm install wontopos
import { Client } from "wontopos"; const mem = new Client({ apiKey: "wos-live-..." });
Choose a model
The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default in the constructor; override a single call with withModel().
const mem = new Client({ apiKey: "wos-live-...", model: "tablet-1" }); // default mem.recall("...", "alice"); // tablet-1 mem.withModel("tablet-1").recall("...", "alice"); // or pick a model per call
listModels ✓ live-tested
The catalog - the ids you can pass to model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.
await mem.listModels();
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"}]The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.
Write
add ✓ live-tested
Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.
await mem.add("she prefers tea over coffee", "alice");
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}addTurn ✓ live-tested
Store one conversation turn (user + assistant) into short-term and long-term memory at once.
await mem.addTurn("alice", "hi", "hello!");
{"status": "ok"}addBulk ✓ live-tested
Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.
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 ✓ live-tested
A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.
await mem.update("alice", "576700aa-...", "she switched to coffee this year");
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
"old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}Read
search ✓ live-tested
Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.
const r = await mem.search("what does she drink?", "alice", 1);
{"memories": [{
"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-06-10T04:20:39.688276876Z"
}], "search_ms": 315, "total_found": 1}| Field | Meaning |
|---|---|
| similarity | Raw embedding similarity to your query (0–1). |
| is_superseded | True if this fact was replaced by update(). |
| search_ms | Server-side retrieval time. |
recall ✓ live-tested
One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.
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 ✓ live-tested
Recent conversation turns (short-term memory), oldest first.
const turns = await mem.history("alice");
{"count": 2, "turns": [
{"role": "user", "content": "hi", "timestamp": "2026-06-10T04:20:40.989011337Z"},
{"role": "assistant", "content": "hello!", "timestamp": "2026-06-10T04:20:40.989013416Z"}
], "user_id": "alice"}stats ✓ live-tested
Memory counts for one user.
await mem.stats("alice");
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}Delete
delete ✓ live-tested
Delete a single memory by id.
await mem.delete("alice", "576700aa-...");
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}deleteAll ✓ live-tested
Erase everything for one user - one call, GDPR-ready.
await mem.deleteAll("alice");
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}Rust - every method, three groups.
Write, read, delete. Every example below was run against the live API on 2026-06-10; responses are verbatim.
cargo add wontopos
use wontopos::Client; let mem = Client::new("wos-live-...");
Choose a model
The API key picks which memory (your account); the model picks which engine reads it. Every model shares one memory, so you can store with one and recall with another. Set a default with with_model(); chain it again to override a single call.
let mem = Client::new("wos-live-...").with_model("tablet-1"); // default mem.recall("...", "alice").await?; // tablet-1 mem.with_model("tablet-1").recall("...", "alice").await?; // or pick a model per call
list_models ✓ live-tested
The catalog - the ids you can pass to with_model and whether each is live. memory: "shared" models read the same store; "isolated" keeps its own. Needs no API key.
mem.list_models().await?;
[{"id": "tablet-1", "name": "Tablet 1", "available": true, "memory": "shared"}]The catalog above always reflects the models available right now - pass any other id and you get a clear error. New models appear there automatically when they ship.
Write
add ✓ live-tested
Store one memory. Embedded on the way in - no LLM call, you pay embeddings only.
mem.add("she prefers tea over coffee", "alice", json!({})).await?;
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}add_turn ✓ live-tested
Store one conversation turn (user + assistant) into short-term and long-term memory at once.
mem.add_turn("alice", "hi", "hello!").await?;
{"status": "ok"}add_bulk ✓ live-tested
Backfill a large blob of text. Chunked and embedded server-side - ideal for importing existing history.
mem.add_bulk("Alice moved to Brooklyn in March...", "alice", "general").await?;
{"elapsed_secs": 0.154154944, "status": "ok", "stored": 1, "total_chunks": 1}update ✓ live-tested
A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.
mem.update("alice", "576700aa-...", "she switched to coffee this year").await?;
{"new_memory_id": "07e94433-b7cc-4e49-8d8f-f37fc1a392b7",
"old_memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "superseded"}Read
search ✓ live-tested
Semantic search, most relevant first. Pure embedding - no keyword matching, so any language finds any memory. The SDK returns the memories array directly; the raw HTTP body is shown below.
let r = mem.search("what does she drink?", "alice", 1).await?;
{"memories": [{
"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-06-10T04:20:39.688276876Z"
}], "search_ms": 315, "total_found": 1}| Field | Meaning |
|---|---|
| similarity | Raw embedding similarity to your query (0–1). |
| is_superseded | True if this fact was replaced by update(). |
| search_ms | Server-side retrieval time. |
recall ✓ live-tested
One round-trip returns everything your LLM needs - paste the result straight into your prompt: a bounded, fixed-size context no matter how much you've stored.
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 ✓ live-tested
Recent conversation turns (short-term memory), oldest first.
let turns = mem.history("alice").await?;
{"count": 2, "turns": [
{"role": "user", "content": "hi", "timestamp": "2026-06-10T04:20:40.989011337Z"},
{"role": "assistant", "content": "hello!", "timestamp": "2026-06-10T04:20:40.989013416Z"}
], "user_id": "alice"}stats ✓ live-tested
Memory counts for one user.
mem.stats("alice").await?;
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}Delete
delete ✓ live-tested
Delete a single memory by id.
mem.delete("alice", "576700aa-...").await?;
{"memory_id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "deleted"}delete_all ✓ live-tested
Erase everything for one user - one call, GDPR-ready.
mem.delete_all("alice").await?;
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}curl - no install, same methods.
No SDK to install - any HTTP client works. Set your key once and call the same endpoints the SDKs wrap. Base URL https://api.wontopos.com, auth via X-API-Key, JSON in and out.
# set your key once (never hard-code it) export WOS_API_KEY="wos-live-..."
Write
store ✓ live-tested
Store one memory. 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"}'
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}store-turn ✓ live-tested
Store one conversation turn (user + assistant) into short and long-term memory at once.
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"}supersede ✓ live-tested
A fact changed - the old memory is marked superseded, the new one takes its place in recall.
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"}Read
search ✓ live-tested
Semantic search, most relevant first. Pure embedding - any language finds any memory.
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}recall ✓ live-tested
One round-trip returns short-term + long-term + context + an instruction. Paste it straight into 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 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..."}Delete
forget ✓ live-tested
Delete one memory by id, or omit it to delete everything for a user (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"}Engrams
Callable recall tools your model can invoke - each one a different retrieval strategy over the same memory. Use one, or run several at once.
More engrams ship regularly - this list grows.
Memoir & Archive Coming soon
This one is a model mode, not a callable tool. Pick it on the model and every recall, a plain search included, comes back with time written that way. Coming soon.
Time_awareness Coming soon
A model mode, not a per-call option. Pick the model - Time_awareness-memoir or Time_awareness-archive - and every recall comes back rendered that way: a plain search, a recall, or any engram, no extra parameter. A Memoir reads the way a person remembers; an Archive keeps an exact record - the difference shows up most in how each writes time. The Time_awareness- prefix leaves room for more capabilities later.
Memoir
Time_awareness-memoirTells what happened and how one moment led to the next, with the soft sense of time a person recalls - read as experience, not a list.
Archive
Time_awareness-archiveReturns matches as exact records - precise elapsed time and absolute anchors, structured for a model to read straight off.
store / add call under a user_id (that user_id is that person's store). Store first; then any recall - the plain search below included - comes back time-tagged. See Quickstart to store.# plain search - no engram - on a memoir model
r = mem.search("what does Alice drink?", user_id="alice", model="Time_awareness-memoir", tz=9)
# every hit gets a .time field → "a couple weeks ago" (archive model → "2 weeks ago (Jun 09)")tz is the caller's UTC offset in hours - so "this morning" and the 4am day boundary land in their local time. Omit it for UTC; over HTTP it's the X-WOS-Timezone header. Roughly, by region: US East -5, US Central -6, US West -8 · UK / Lisbon 0 · Central Europe +1 · Eastern Europe +2 · India +5.5 · China / Singapore +8 · Korea / Japan +9 · Sydney +10. (Standard time - daylight saving shifts some regions by +1; pass whatever your users are actually on.)
Same search, two models - the memories are identical, only time changes:
{ "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" }
] }{ "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)" }
] }| Elapsed | Memoir | Archive |
|---|---|---|
| 3 min | a few minutes ago | 3 minutes ago |
| 14 min | about 15 minutes ago | 14 minutes ago |
| 30 min | half an hour ago | 30 minutes ago |
| 50 min | about an hour ago | 50 minutes ago |
| 2 hr | a couple hours ago | 2 hours ago, at 13:10 |
| 8 hr | this morning | 8 hours ago, at 07:10 |
| yesterday pm | yesterday afternoon | yesterday at 14:00 |
| last night | last night | 17 hours ago, at 22:00 |
| 2 days | a couple days ago | 2 days ago (Tue 15:10) |
| 6 days | several days ago | 6 days ago (Fri 15:10) |
| 9 days | about a week ago | last week (Jun 16) |
| 16 days | a couple weeks ago | 2 weeks ago (Jun 09) |
| 35 days | about a month ago | last month (May 21) |
| 60 days | a couple months ago | 2 months ago (Apr 2026) |
| 180 days | about half a year ago | 6 months ago (Dec 2025) |
| 380 days | about a year ago | last year (Jun 2025) |
| 800 days | a couple years ago | 2 years ago (Apr 2024) |
| 1500 days | about 4 years ago | 4 years ago (May 2022) |
Every value above is the renderer's real output. Look at the two "yesterday" rows: a Memoir splits afternoon from last night - a day is one sleep - while an Archive writes a single clock time and draws no day or night line.
How each mode reads time
Memoir - the way people actually say it. Recent moments stay fairly sharp (about 15 minutes, half an hour), then the wording widens the further back you go - a couple weeks, about half a year, a couple years - the way memory itself loosens with distance. Inside a day it drops the clock for a landmark: this morning, last night, yesterday afternoon. And a day is one sleep, not a calendar tick: the boundary sits around 4am local time, so a late night still reads as the same evening, not already tomorrow.
Archive - precise, always with an anchor. Every line carries the exact elapsed time plus an absolute reference a model can compute from, and the anchor tightens as it nears: a clock for today (8 hours ago, at 07:10), a weekday and clock this week (2 days ago (Tue 15:10)), a date this month (last week (Jun 16)), a month and year beyond (6 months ago (Dec 2025)). Never vague, never wrong.
deep_recall
Multi-hop recall. Searches your query, then takes the top match and searches again on its content - pulling in linked context a single search would miss. Best when memories reference each other (a person → their projects → details). Returns up to ~12.
out = mem.engram("deep_recall", "what should I know about Alice?", user_id="alice"){ "engram": "deep_recall", "hops": 2, "count": 12,
"memories": [ ... ],
"usage": { "input_tokens": 5, "output_tokens": 61 } }usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.timeline
Time-ordered recall. Returns memories sorted newest-first by when the event happened, not by relevance. For "when did X", history, and sequence questions. Returns up to 15.
events = mem.engram("timeline", "project milestones", user_id="alice"){ "engram": "timeline", "hops": 1, "count": 15,
"memories": [ ... ],
"usage": { "input_tokens": 4, "output_tokens": 88 } }usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.gather
Broad gather. Searches, then expands around the top three matches - a wider net than deep_recall. Use it to pull in everything related to a person, project, or topic in one call. Returns up to ~18.
related = mem.engram("gather", "everything about Project Atlas", user_id="alice"){ "engram": "gather", "hops": 4, "count": 18,
"memories": [ ... ],
"usage": { "input_tokens": 6, "output_tokens": 142 } }usage (input + output), counted by the same tokenizer as the rest of the API; no hidden per-engram fee. Need several at once? Call them concurrently - each engram is an independent request.Every endpoint, one base URL.
No SDK required - any HTTP client works. Base URL https://api.wontopos.com, auth via the X-API-Key header, JSON in and out. Memory ops are POST; managing stores uses POST / GET / DELETE on /collection. A store must exist first (see Stores) or in-store ops return 404.
| Endpoint | Purpose | Body fields |
|---|---|---|
| POST /api/v1/memory/collection | create a store | user_id |
| GET /api/v1/memory/collections | list your stores | (none) |
| DELETE /api/v1/memory/collection | delete a store + its memories | user_id |
| /api/v1/memory/store | store one memory | user_id · content · metadata? |
| /api/v1/memory/store-turn | store a conversation turn | user_id · user_msg · assistant_msg |
| /api/v1/memory/bulk-store | backfill a text blob | user_id · content · category? |
| /api/v1/memory/search | semantic search | user_id · query · max_results? |
| /api/v1/memory/recall | short + long + context | user_id · query |
| /api/v1/memory/history | recent turns | user_id |
| /api/v1/memory/stats | memory counts | user_id |
| /api/v1/memory/supersede | replace a changed fact | user_id · old_memory_id · new_content |
| /api/v1/memory/forget | delete one (or all) | user_id · memory_id? (omit = delete all) |
# 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?"}'
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}Same features for everyone.
Tiers only raise your limits.
Every tier runs the full engine - same recall quality, same languages, every method. Tiers advance automatically through Tier 5 as your cumulative credit purchases grow, with no application or sales call. Enterprise (Tier 6) is the one exception.
Spend limits
Each tier caps how much you can spend per calendar month. You advance immediately when your cumulative credit purchases reach the next threshold.
| Usage tier | Credit purchase | Monthly spend limit |
|---|---|---|
| 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 | Talk to us | No limit |
Rate limits
Rate limits are per account - every API key on an account shares one limit, which scales with your tier. Exceeding it returns a 429 with a retry-after header; back off (1s → 2s → 4s) and retry. Every endpoint is idempotent-friendly, so retries are safe.
| Tier | Requests per minute |
|---|---|
| Tier 1 | 150 |
| Tier 2 | 300 |
| Tier 3 | 600 |
| Tier 4 | 1,500 |
| Tier 5 | 3,000 |
| Tier 6 - Enterprise | Custom |
Enterprise (Tier 6) gets custom rate limits, an SLA, dedicated support, and an optional self-host license - talk to us.
When something goes wrong.
Errors come back as a JSON envelope with a stable type, a human message, and a request_id you can send us when reporting an issue.
{"type": "error", "error": {
"type": "authentication_error",
"message": "Invalid or revoked API key.",
"request_id": "063f8b83-eee2-4383-a5cf-11e4bcd29d7c"
}}| HTTP | Meaning | What to do |
|---|---|---|
| 401 | Invalid or revoked API key | Check the key; issue a new one in the console. |
| 422 | Malformed body (missing/wrong-type field) | The message names the exact field - fix and retry. |
| 429 | Rate limited | Back off exponentially (1s → 2s → 4s) and retry. Safe: all endpoints are idempotent-friendly. |
| 5xx | Server-side problem | Retry with backoff; include request_id if you contact us. |
# SDK error handling (Python) from wontopos import Client, WosError try: mem.search("...", user_id="alice") except WosError as e: if e.status == 401: ... # bad key elif e.status == 429: ... # back off and retry
Rate limits are per account, shared across all your keys, and scale with your tier - see Usage tiers. Your account's usage is shown in the console.
Your servers, your data.
The engine can run inside your own infrastructure - same API, same SDKs. Point the client at your host and nothing else changes.
mem = Client(api_key="...", base_url="https://wos.your-host.com")- Data residency. Memories never leave your network.
- Same surface. The same methods and endpoints work identically.
- Licensing. Self-host packages are arranged per deployment - contact us.
Beyond the context window.
WOS recalls from histories of 1.4M tokens - far larger than any LLM context window - and still hands back a tight ~1,470-token slice.
Your agent's memory isn't capped by what fits in a prompt. It keeps everything and retrieves only what matters, no matter how large the history grows.
Private, and yours.
Your data stays in your store. We never train on it, view it, or reuse it - we only organize it so you can retrieve it.
- BYOK. Your LLM key is sent per request and never stored.
- Isolated. Memories are scoped per account, then per
user_id. - GDPR delete & self-host. One call wipes a user; run the engine in your own environment if you prefer.