Skip to main content

Wontopos — long-term memory for AI agents

pip install wontopos

Get an API key in the console. Keys look like wos-live-...; the client also reads WONTOPOS_API_KEY from the environment.

from wontopos import Client

mem = Client(api_key="wos-live-...")

# Each end-user / agent / topic gets its own store — create it once.
# (A "default" store already exists, so you can skip this and omit the id.)
mem.create_store("alice")
mem.add("she prefers tea over coffee", user_id="alice")

# one call → short-term + long-term + context, ready for your LLM prompt
ctx = mem.recall("what does alice drink?", user_id="alice")

Why

  • The same in every language — identical recall whichever language a memory was written in (Korean · Japanese · Chinese · English).
  • No LLM in the loop — store / search / recall never call a language model. You pay retrieval, not generation.
  • Bounded retrieval — recall() returns a small, fixed-size slice regardless of how much you've stored (~1,000 tokens on tablet-2, the default engine). Your LLM bill stops growing with history.

Methods

Method Purpose
add(content, user_id, **metadata) Store one memory
add_turn(user_msg, assistant_msg, user_id?) Store a conversation exchange
add_bulk(content, user_id, category=, timestamp=) Backfill a long history
update(old_memory_id, new_content, user_id?) Supersede an old fact
search(query, user_id, limit=10, **opts) Search stored memories
search_full(query, user_id, limit=10, **opts) The same search with every field kept — images and verify_used included
recall(query, user_id) One-call context (short + long + surrounding)
history(user_id) Recent turns (short-term)
stats(user_id) Counts
get(user_id, memory_id) Fetch one memory by id (the text you stored, and its metadata)
list_memories(user_id, limit=100, cursor=) Browse/export a store's raw memories, paged
delete(user_id, memory_id) Delete one memory
delete_all(user_id) GDPR erase (delete every memory for the user)
add_speaker(speaker, user_id?) Register a person (explicit, up to 50 to start)
list_speakers(user_id?) Registered people + per-person memory counts
remove_speaker(speaker, user_id?) Unregister; memories stay, the tag goes

All methods take a user_id — it names the store: one isolated memory space per end-user, agent, or topic, then per account (your API key). WHO said each memory inside a store is the speaker tag below — storing the assistant's own words never needs a separate id.

Who said it (speakers)

Every memory can carry a speaker: "me" for the assistant's own words, or a person's name. Speakers are explicit, like stores: register a person once, then store under their name — a typo can never silently become a new person. Search accepts a speaker too, so you can recall one person's words only.

mem.add_speaker("Bob", user_id="alice")      # once per person
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")

Results arrive best-first — take the list in the order given. similarity on each memory is a raw closeness score, not the ranking key: what produces the order is internal and is not returned, so sorting by it makes results worse. There is no score field.

A store registers up to 50 people to start (a limit we plan to raise); "me" never needs registration and never counts against it.

Async

Same surface, awaitable — needs the extra:

pip install "wontopos[async]"
from wontopos import AsyncClient

async with AsyncClient(api_key="wos-live-...", user_id="alice") as mem:
    await mem.add("she prefers tea over coffee")
    hits = await mem.search("what does alice drink?")

Every Client method exists on AsyncClient with identical arguments and semantics (retries, redirect refusal, guards). Close with async with or await mem.aclose().

Recall caching

Opt in per search and repeated or extended queries reuse the previous result at 10% of the normal rate (Tablet and Scroll models).

It is not free to turn on: the FIRST call writes the cache and bills the query tokens at 2x for a 5m TTL, 3x for 1h. Only hits inside the TTL bill at 0.1x. So it pays for a query you repeat or extend, and costs more for one you issue once — do not switch it on globally. Any write to the store invalidates its cache at once, so a hit can never predate a new memory.

hits = mem.search("...the conversation so far...", user_id="alice",
                  cache_control={"ttl": "5m"})   # or "1h"

Reliability

Built in, no configuration needed:

  • Automatic retries — 429 always, and 502 / 503 or a connection error only when a retry cannot double-process a write. The writes and the searches are POSTs, and a 502 on one of those may have been returned after the service already stored and billed it, so those get 429 and connect-level failures only. The reads and the deletes that address a whole store — list_stores, list_speakers, delete_store, remove_speaker, forget_image — are GET or DELETE and do retry a 502. Twice, with exponential backoff + jitter, honoring the server's Retry-After. Tune with Client(retries=...); retries=0 disables.
  • Redirects refused — the API key never follows a 3xx to another host.
  • Timeouts — 30s per attempt by default (Client(timeout=...)), and a total budget for the whole call across every retry with Client(deadline=...) / with_deadline(secs). At the defaults one call can hold for 30s + backoff + 30s
    • backoff + 30s, which a request handler with five seconds cannot use.
  • Key never in logs — repr(client) masks the API key.
  • Wipe guard — delete() without a memory_id raises instead of silently meaning "delete everything"; wiping a store is only ever the explicit delete_all(user_id) / delete_store(user_id).

Security

Built in, none of it configurable off:

  • TLS 1.2 floor and certificate verification that cannot be disabled.
  • Redirects refused — a 3xx is an error, so the key never follows one to another host.
  • Response size cap — anything over 64MB is refused instead of buffered.
  • Key hygiene — keys are trimmed (a stray newline from a file otherwise becomes a mystery 401) and inner whitespace is rejected; model names are validated before they reach a header.
  • Client.from_env() reads WONTOPOS_API_KEY (or WOS_API_KEY) — keep keys out of source code.
  • Plain-HTTP base URLs on non-local hosts warn. One dependency (requests, floor >=2.32 for its certificate-verification fix).

Errors

Any non-2xx response raises WosError(status, message). When the server sent a request id it's on e.request_id — include it when contacting support.

from wontopos import Client, WosError

try:
    mem.search("...", user_id="alice")
except WosError as e:
    if e.status == 401:
        print("API key invalid or revoked")
    elif e.status == 429:
        print("Rate limited — back off")   # already retried twice by then
    else:
        print(e.status, e.message, e.request_id)

A different API host

Point the client somewhere other than the default endpoint - a dedicated region, a proxy of your own, or a local test server:

mem = Client(api_key="...", base_url="https://api.example.com")

Links

Reporting a bug

Found something wrong, or something that looks unsafe? Tell us — every report gets read.

Include the SDK version (wontopos.__version__) and the language. If it involves a store id or a memory, describe the shape rather than pasting the contents — we do not need your data to fix it.

Changelog

The three clients release in lockstep — same version, same surface, same day. Patch releases are additive: nothing is removed or reordered within a minor line. Two have bent that, deliberately and named at the top of their entry — 2.2.34 moved the default engine, and 2.2.35 gives search the count range recall has always had. A rule you can bend without saying so is not a rule, so both are stated rather than left for a reader to hit.

  • 2.2.36 — an outside review, read by a different model with no knowledge of why any of this was written. The one that mattered: recall() promised in its own documentation that a count outside 5–20 was refused rather than clamped, and nothing checked — so limit=500 travelled to the service and failed there, for a mistake visible before opening a socket. context_limit (0–20) was unchecked the same way. Both are refused here now, and 0 still passes for context_limit because "attach none" is an answer, not a missing value. An argument mistake now raises ValueError, not WosError. A WosError with status 0 is APIConnectionError — "the request never got a response" — so code branching on it would have retried a typo forever. Every other argument check in this client already raised ValueError. New Client(deadline=…) / with_deadline(secs): a TOTAL budget for one call across every retry. timeout bounds one ATTEMPT, so at the defaults a call could hold for 30s + backoff + 30s + backoff + 30s and a request handler with five seconds had no way to say so. Unset means the previous behaviour. replayed is set only when the service did not send that field itself — these responses are widening, and a client overwriting a server's value is one release away from replacing a real answer with a guess.

  • 2.2.35 — fixes, and two behaviour changes, both listed first because a patch release is not the place to find one by surprise.

    ⚠️ search's count is now 5–20, and out of range is refused rather than adjusted. recall has carried that range from the start; search had no contract anywhere, so the clients sent whatever they were given and the service capped at 50 with no floor. search(q, limit=50) and search(q, limit=3) both worked on 2.2.34 and now raise. The default is still 10, so a call that passes no count is unaffected. Asking for 20 and silently getting 10 reads as "that is all there is", which is why this refuses instead of clamping.

    ⚠️ A Rust limit of 0 is no longer rewritten to 10. It was, and the caller whose prompt budget computed zero was handed ten memories and the bill for them. It is now refused under the range above rather than quietly changed to something nobody asked for.

    The fixes: a null idempotency key no longer becomes the literal key "null", which had made every write on that path share one key so the second onward stored nothing (TypeScript). The async client no longer reads an explicit retries=2 as "the caller said nothing" (Python). The image read is actually streamed, so the 64MB cap bounds a compressed body instead of measuring bytes already in memory (Python async), and that route's ERROR body is capped too (Rust). iter_images stops on a repeated cursor the way iter_memories always did (Python). iterImages no longer throws on the ordinary end of a walk (TypeScript). get_image retries a 429 and a connect-level failure like every other call — it had none at all — in the sync Python client and TypeScript; the async Python client and the Rust crate still make a single attempt there, and say so at the method. An empty or control-character API key is refused at the call site rather than at the network in Python and Rust; TypeScript still refuses only empty and whitespace. And a page walk that reaches its ceiling now raises instead of returning a truncated list that looks complete.

  • 2.2.34 — the default engine is tablet-2: same token price as tablet-1, and the one that serves images and re-ask. Pin the old one with model="tablet-1" for the previous behaviour exactly. (The engine's own default count differs between the two, but these SDKs always send one, so a call through them is unaffected.) New search_full / searchFull keeps every field the answer came with — search was merging away the photos and verify_used, both of which you were billed for. Also: results arrive best-first — take the order as given. similarity is a raw closeness score, and sorting by it makes results worse.

  • 2.2.33 — get_image can return a different FORMAT than you uploaded: the service re-encodes on downscale, so a large PNG comes back as WebP. Take the file extension from the response Content-Type, not from what you sent.

  • 2.2.32 — get_image returns the picture the SERVICE holds, not your original. An image whose long edge is over 1568px is downscaled on the way in, and that smaller picture is what is stored and handed back. Keep your own copy if you need the full file.

  • 2.2.31 — images, by-speaker, lineage and revisions are SDK methods in all three clients; search and recall take verify, max_images and recall's limit / context_limit.

  • 2.2.30 — documentation only: this changelog, which had not been updated in the shipped package since 2.2.10.

  • 2.2.29 — a store id that was PASSED but unusable no longer becomes the default store. 2.2.28 caught a blank string; the value a failed tenant lookup actually produces in Python is an integer primary key. user_id=0 is falsy and fell through to the client default, and any other integer died inside the warn helper as 'int' object has no attribute 'lower'. Anything but a non-blank string now raises, on the destructive calls too. Omitting the argument still means "use the default". Also: delete_store now warns when a store id folds (delete_all already did, and it is the call that removes a whole store), and an idempotency_key ending in a newline is refused here instead of failing inside http.client as Invalid header value.

  • 2.2.28 — audit: warn caches made thread-safe, a whitespace memory_id can no longer read as a whole-store delete, a blank store id raises instead of silently using the default, and a gzip bomb no longer bypasses the response cap on the async client.

  • 2.2.11–2.2.27 — additive fixes and hardening across all three clients.

  • 2.2.10 — Memory fix: the relevance field is similarity (not score); added typed importance, category, is_superseded, superseded_by, created_at, event_date.

  • 2.2.4–2.2.9 — one version across Python, TypeScript and Rust, released in lockstep; retries, redirect refusal, response cap, key masking, speakers.

License: MIT.

Metadata

Release files for wontopos 2.2.36

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wontopos 2.2.36
File Size Uploaded
wontopos-2.2.36.tar.gz 74.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wontopos 2.2.36
File Interpreter ABI Platform
wontopos-2.2.36-py3-none-any.whl Python 3 none any Details

Total release size: 124.3 kB

Release files / wontopos-2.2.36.tar.gz

Download URL wontopos-2.2.36.tar.gz
Size 74.4 kB
Tags Source
SHA-256 checksum
How to use checksums
3704e656ed4ecf550d1b75f551d91a86d07a5ec167eb6b5fdf1a21a78c7e74c1
BLAKE2b-256 checksum
How to use checksums
3344246f996e55c3077fa657e0c9fe0564453a118a9a53dd6035680ad2e26897
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.18

Release files / wontopos-2.2.36-py3-none-any.whl

Download URL wontopos-2.2.36-py3-none-any.whl
Size 49.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3390b5893d49d72411da78f8ffc75ac76d4d42fafcfb51a4939581e5faca568f
BLAKE2b-256 checksum
How to use checksums
2ce89356b5f0452368687ab2eabe07b093d1e3fb49328ae6bcaee88de165aa8e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.18

Release history Release notifications | RSS feed

2.2.42

2 release files

2.2.41

2 release files

2.2.40

2 release files

2.2.39

2 release files

2.2.38

2 release files

2.2.37

2 release files

This release

2.2.36 This release

2 release files

2.2.34

2 release files

2.2.33

2 release files

2.2.32

2 release files

2.2.31

2 release files

2.2.30

2 release files

2.2.29

2 release files

2.2.28

2 release files

2.2.24

2 release files

2.2.23

2 release files

2.2.22

2 release files

2.2.21

2 release files

2.2.20

2 release files

2.2.19

2 release files

2.2.18

2 release files

2.2.17

2 release files

2.2.16

2 release files

2.2.15

2 release files

2.2.14

2 release files

2.2.13

2 release files

2.2.12

2 release files

2.2.11

2 release files

2.2.10

2 release files

2.2.9

2 release files

2.2.8

2 release files

2.2.7

2 release files

2.2.6

2 release files

2.2.5

2 release files

2.2.4

2 release files

2.2.2

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page