Rust SDK

Rust - every method, three groups.

Write, read, delete. Every example below was run against the live API on 2026-08-01; 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("scroll-1").recall("...", "alice").await?;  // or pick a model per call

list_models

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?;
Actual response
[{"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

Confirm connectivity and that your API key works - a one-line setup check.

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

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

Store one memory. No LLM call on the way in - you pay the write rate and nothing else.

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
Actual response
{"id": "576700aa-f0e0-4c26-99a0-10e2d5b0d624", "status": "stored (1 chunks)"}

add_turn

Store one conversation turn (user + assistant) into short-term and long-term memory at once.

mem.add_turn("hi", "hello!", "alice").await?;
Actual response
{"status": "ok"}

speaker

Every memory can carry who said it. Register a person once, then pass their name as the speaker; "me" (the assistant's own words) never needs registration. Search accepts a speaker too, so you can recall one person's words only.

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?;
Speakers are explicit, like stores. Register a person first, then store under their name — a typo can never quietly become a new person. A store registers up to 50 people to start (we plan to raise it), and "me" never needs registration or counts.

add_bulk

Backfill a large blob of text. Split and indexed server-side - ideal for importing existing history.

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

update

A fact changed. The old memory is marked superseded (kept for history); the new one takes its place in recall.

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

Read

search

Semantic search, most relevant first. Any language finds any memory, whichever language it was written in. The SDK returns the memories array directly; the raw HTTP body is shown below. Some models answer with more than one set of results and the SDK hands them back merged, so the array can hold MORE than max_results - size your prompt window on what you receive, not on the number you asked for.

let r = mem.search("what does she drink?", "alice", 1).await?;
Actual response (HTTP body)
[{
   "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"
 }]
FieldMeaning
similarityHow close this memory is to your query (0–1).
is_supersededTrue if this fact was replaced by update().
search_msServer-side retrieval time.

recall

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?;
Actual response (shape - lists shortened)
{"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

Recent conversation turns (short-term memory), oldest first.

let turns = mem.history("alice").await?;
Actual response (HTTP body)
{"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

Memory counts for one user.

mem.stats("alice").await?;
Actual response
{"short_term_turns": 2, "total_memories": 4, "user_id": "alice"}

get

Fetch one memory by id - the id that add or list_memories returned. The original text plus its metadata, and nothing internal. An id from another store, or a deleted or invalidated one, returns 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

List a store's memories - the original text you stored plus its metadata, and nothing internal. Paged by cursor: pass the returned next_cursor back for the next page.

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

Page through every memory with no cursor bookkeeping, or pull the whole store at once.

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

Delete

delete

Delete a single memory by id.

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

delete_all

Erase everything for one user - one call, GDPR-ready.

mem.delete_all("alice").await?;
Actual response
{"memories_deleted": 4, "status": "deleted", "user_id": "alice"}

Errors and reliability

Every failure is a typed error - catch the specific one (rate limit, auth, payment) or all of them with the base 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

Read the quota left after any call and slow down before you hit the limit.

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

search_self

Both lanes from one call on a self-memory model (Scroll 1.2+): what others said, and the agent's OWN words - kept apart so the reader never confuses who said what.

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

Ask the service which engrams and delivery forms the selected model can run, instead of hard-coding names that go stale the moment a new one ships.

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

filters

Narrow the search to part of a store. Applied before ranking, so you get the best matches within the filter - not a filtered top-N.

mem.search_with("what did we decide", "alice", 10, json!({"filters": {
    "categories": ["work"], "event_from": "2026-01-01"   // when it HAPPENED
}})).await?;
Keys: categories · event_from / event_to (when the content HAPPENED - metadata.event_date) · time_from / time_to (when it was written) · min_importance. Unlisted keys are dropped, not rejected, so a typo silently widens the search.

add_idempotent

Makes repeating one write safe. Use it when the retry is yours - a job that died and was re-run, a queue that redelivers.

mem.add_idempotent("she prefers tea", "alice", json!({}), &format!("import:{}", row.id)).await?;
Derive the key from the thing being stored (import:row-42), never a constant: one key reused for two different writes replays the first, and the second is silently lost. Format: 1-128 chars of [A-Za-z0-9._:-].

with_timeout / with_retries / with_deadline

Tune one call site without touching the client you already built: a clone with a longer timeout for a big backfill, or with retries off inside your own retry loop.

timeout bounds one attempt, so a call that retries can outlive it: at the defaults a single call can hold a connection for 30s, back off, try again, and again. deadline bounds the whole call instead - every attempt is capped at what is left, and a backoff is never slept past the budget. Set it when the caller has a real limit, like a request handler with five seconds.

mem.with_timeout(120).add_bulk(big_blob, "alice", "general").await?;
mem.with_retries(0).add("...", "alice", json!({})).await?;
mem.with_deadline(Duration::from_secs(5)).recall("...", "alice").await?;  // 5s for the whole call