Skip to main content

Anona Memory SDK

PyPI Python npm License: MIT

Official SDKs for Anona Memory, managed memory for AI agents. Record, retrieve and reason over memories scoped per user, agent and space: from a client, a framework adapter, an MCP server, or the anona command.

Documentation — Python SDK · TypeScript SDK · Quickstart · API reference · MCP server · Framework integrations

SDK Package Location
Python anona on PyPI this repository root, documented below
TypeScript @anona-labs/memory on npm typescript/

Contents

Install

pip install anona

With the LiteLLM integration (or mcp for the MCP server):

pip install "anona[litellm]"

Quickstart

from anona import AnonaClient

# base_url defaults to https://api.anonalabs.com — pass it only to override.
client = AnonaClient(api_key="anona_live_...")

# Record a memory
client.record(space_id="space_123", content="User prefers dark mode.")

# Retrieve memories
results = client.retrieve(space_id="space_123", query="UI preferences", limit=5)
for r in results:
    print(r["relevance_score"], r["content"])

# Reason: a synthesized insight across memories
summary = client.reason(space_id="space_123", query="What do we know about this user?")
print(summary)

client.close()

Async ingestion (don't block on a write)

Recording runs fact extraction, so a normal record() takes a moment. In a chat loop or any latency-sensitive path, queue the write with background=True and poll the returned job instead:

import time

job = client.record(
    space_id="space_123",
    content="User prefers dark mode.",
    background=True,        # returns a job_id, doesn't wait
)

while True:
    status = client.get_job(space_id="space_123", job_id=job["job_id"])
    if status["status"] in ("completed", "failed", "cancelled", "not_found"):
        break
    time.sleep(2)

# Backfill many memories at once (always queued, up to 100 per call):
batch = client.record_batch(
    space_id="space_123",
    items=[
        {"content": "User is on the Pro plan."},
        {"content": "Signed up in 2024.", "timestamp": "2024-03-01T00:00:00Z"},
    ],
)
print(batch["accepted"], "queued as job", batch["job_id"])

Async variants (async_record, async_retrieve, async_reason) are available on the same client, or use it as a context manager:

async with AnonaClient(api_key="...") as client:
    await client.async_record(space_id="space_123", content="...")

Automatic space routing

Naming a space is the normal way to work and nothing here changes it. But an agent that files everything it learns has to decide where each thing belongs, and route="auto" hands that decision to Anona, which makes it against what each space declares it is for.

result = client.record(content="Acme is on net-30.", route="auto")
for target in result["routed_to"]:
    print(target["space_id"], target["stage"], target["reason"])

memories = client.retrieve(query="what are Acme's payment terms?", route="auto")
for looked in memories.searched:
    print(looked["space_id"], looked["stage"], looked["reason"])

Pass exactly one of space_id or route="auto". Both, or neither, raises AnonaError with code="route_conflict" — client-side, before the request goes out. "Neither" is an error rather than an implicit auto-route on purpose: a caller who simply forgot space_id should not have their memory filed, or their question answered, somewhere they never named.

Read the decision. A routed write answers with routed_to and a routed read with searched, each a list of {space_id, confidence, stage, reason}, where stage is "rule" (a rule you declared), "model" or "fallback". confidence is null on a rule or a fallback — neither is a judgement with a score behind it. Nothing else tells a misroute apart from a correct call: both succeed.

retrieve still returns a plain list — searched rides on it as an attribute, so every existing call site is untouched — and that attribute is None on an addressed read, because there was no decision to report.

An abstention searches nothing. If no space is judged to answer the question, searched is a single entry whose space_id is None, and the results are empty. That is deliberate rather than a gap: fallback_space_id has no default on a read, because the fallback space is where everything that fitted nowhere ends up — the least topically coherent space you own, and so the worst place to look. Searching it would return irrelevant memories that read exactly like real ones. On a write the fallback does default (to your organization's default space), because a memory has to be stored somewhere.

fallback_space_id names the space either side falls back to.

max_targets means something different on each side. On a write it is how many spaces one memory may reach, and it is capped at 1 server-side today, so 2 or more is a 422 rather than a fan-out — a memory is filed in one place. On a read it accepts 1 to 20, because a question can legitimately be answered out of several spaces: "was Acme charged for a bulk import we ran ourselves?" needs the charge and the account it landed on.

When more than one space is searched the results are ranked across them rather than concatenated, so the leading memory need not come from the leading space in searched.

Routing matches against each space's routing profile — a charter, topics and rules, declared per space on /v1/spaces/{space_id}/routing-settings. That endpoint is REST-only for now; the SDK does not model it yet. examples/space_routing.py shows the whole loop.

User profiles

If you scope writes with user_id, a space accumulates a per-user history with nowhere to address it directly. These two read it back, instead of you reconstructing it from a wide-open search.

profile = client.get_user_profile("support", "alice_123")
print(profile["memory_count"], profile["last_active"])

# Prompt-ready instead of a list
block = client.get_user_profile(
    "support", "alice_123", format="block", context_max_tokens=500
)
messages = [{"role": "system", "content": block["context"]}, ...]

answer = client.ask_about_user(
    "support", "alice_123", "How does she prefer to be contacted?"
)
print(answer["insights"], answer["model"])

user_id has to be the same value your writes are scoped with — a typo on either side looks like an empty profile, not an error.

Three things worth knowing:

  • An unknown user is not a 404. A user_id is a scope tag created by the first write naming it, not a resource you register, so there is no valid set for a typo to fall outside of. A user nobody has recorded under comes back with memory_count of 0 and an empty memories list. An unknown space is still a 404.
  • memory_count can go down. The default view collapses layers: several raw facts become one synthesised note, and the note is what gets counted. A profile read during an import can genuinely go 115 → 67 → 15 while the corpus behind it grows the whole time. Read it as how many distinct things are currently known about this user, never as an ingestion counter, and never as the basis for a progress bar.
  • ask_about_user returns the whole response, not the answer string reason returns, because model reports which LLM actually answered. That is the rate the call was charged at, and it differs from what you asked for whenever you asked for nothing.

Time travel

Three arguments, and the distinction between them is the whole point: one is about when something happened, the other two are about what you knew and when.

# Importing history: date the memory to the event, not to the import run.
client.record(
    space_id="support",
    content="Renewed the enterprise contract",
    timestamp="2025-06-14T10:00:00Z",
)

# What did the space know in June, ignoring everything learned since?
client.retrieve(space_id="support", query="contract status", as_of="2026-06-01T00:00:00Z")

# Same corpus, but score recency and resolve "last June" against a past instant.
client.retrieve(space_id="support", query="what changed last June",
                query_timestamp="2026-01-01T00:00:00Z")

# Everything that HAPPENED in June 2025, however recently it was imported.
client.retrieve(space_id="support", query="contract status",
                occurred_after="2025-06-01T00:00:00Z",
                occurred_before="2025-06-30T23:59:59Z")
  • timestamp on record is when the event occurred. It feeds recency ranking, comes back on a result as timestamp, and is what the event-time window below matches against. It does not change when the memory was recorded, so it has no effect on as_of. It is not copied into occurred_start / occurred_end: those are filled only from a date found in the memory's own text, so a memory whose text names no date has both of them null while carrying a perfectly good timestamp.
  • as_of on retrieve is a hard cutoff: only memories recorded at or before that instant come back. A backdated import is recorded today no matter what timestamp it carries.
  • query_timestamp on retrieve only re-ranks. It moves the "now" that recency and relative dates are measured against, and never removes a result. Reach for as_of when you need the cutoff actually enforced.
  • occurred_after / occurred_before on retrieve bound when the thing happened, which is the one question as_of cannot answer: a year of history imported this morning has one record time and twelve months of event time. Either bound alone is an open-ended window, and the test is an overlap, so an event straddling an edge is inside.

Compound tag filters

tags is a flat list under a single match mode, so it can say "any of these" or "all of these" and nothing else. tag_groups takes a boolean expression instead, for the moment a filter has two clauses that combine differently, or a clause that excludes.

client.retrieve(
    "support",
    "what is left to do",
    tag_groups=[
        {"or": [{"tags": ["project:alpha"]}, {"tags": ["project:beta"]}]},
        {"not": {"tags": ["status:archived"]}},
    ],
)

Groups in the list are AND-ed. Each is either a leaf {"tags": [...], "match": ...} or one of {"and": [...]}, {"or": [...]}, {"not": {...}}, nested as deep as you need. match on a leaf takes the same values as tags_match and defaults to any_strict.

Three things worth knowing:

  • The non-strict modes treat an untagged memory as matching, so a leaf written with any or all widens an expression rather than narrowing it, and both are refused inside a not for that reason. Negating "matches, including untagged" would exclude every untagged memory in the space.
  • The filter is compiled into the search, not applied to its output, so an excluded memory never competes for a slot. You get the best n matches that passed the filter rather than whatever survived it.
  • It composes rather than replaces. tags, user_id, agent_id and session_id are all AND-ed onto your expression, so a scoped query stays scoped when you add a filter.

Limits: 25 leaves and 5 levels of nesting per request; 500 tags in any one leaf and 1,000 across the whole expression. A leaf naming hundreds of tags is a normal shape rather than abuse: one leaf is a single set-containment test, so naming every candidate you might mean and letting the filter decide which of them the space actually has is cheaper than spreading them over separate leaves. reason accepts the same argument, where it narrows what the reasoning agent may look at.

Matching a name you are not sure of

Tags match as exact strings, which is right when your own code wrote them and wrong when it did not. An agent building a filter out of something a user typed has to guess the exact string a memory was stamped with, and one typo means the memory is unreachable.

Set resolve to "fuzzy" on a leaf and its tags become candidates instead of requirements. Each is compared against the tags the space actually holds, and the close ones are added to the filter:

client.retrieve(
    "support",
    "what did we decide about the checkout rewrite",
    tag_groups=[{"tags": ["name:typsecript", "name:chekcout"], "resolve": "fuzzy"}],
)

name:typsecript matches nothing on its own. Resolved, it also matches name:typescript.

Five things worth knowing:

  • It widens, it never narrows. The tags you named are always kept, so a fuzzy leaf matches at least everything the same leaf matched exactly.
  • Resolution stays inside the tag's namespace. The part before the first colon must match exactly and only what follows it is compared, so name:chekcout can reach name:checkout and never status:checkout.
  • Short candidates are matched literally. Below four characters after the namespace nothing is resolved, because similarity on two or three characters is noise: api and apis score 0.5 and they are different things.
  • A word of a longer tag is not a misspelling of it. A candidate has to be about as long as what it matches, so name:pipeline does not resolve to name:functional pipeline.
  • It fixes misspellings, not abbreviations. kubernetes and k8s have almost no letters in common, so nothing resolves one to the other. That is what labels are for: a multi-text group records every name a thing is known by, and the exact filter finds them.

resolve may only be combined with a match of any or any_strict, or left unset: a resolved leaf is a set of alternatives, so all_strict would ask one memory to carry every alternative at once.

A filter is only as good as the tags a memory carries. If you are tagging by hand on every write, labels can have extraction do it for you.

Extraction settings

Recording a memory is not storage. Your text goes through one pass that decides which facts are worth keeping and how they are phrased, and only what survives is stored — so a detail dropped there is not ranked low later, it is not there at all. These settings point that pass at your domain.

client.set_extraction_settings(
    "engineering",
    guidance=(
        "Engineering log. Always capture service names, metric values with "
        "units, and the named owner. Treat incidents as dated events. "
        "Skip standup small talk."
    ),
)
  • guidance is added to the standard rules and applies in every mode. Reach for it first — naming your vocabulary and the fields that always matter is what turns a good guess into a reliable one. Max 4,000 characters.
  • mode is concise (the default), verbose (keep every specific), verbatim (store the text as written, derive only the metadata around it), or custom.
  • custom_prompt replaces the standard rules, and only applies while mode is "custom". Max 8,000 characters.
  • labels is different in kind. The three above steer what a write keeps; labels ask the same pass to classify what it kept, along dimensions you define. See Labels.

Two things worth knowing. set_extraction_settings replaces the record, so anything you leave out is cleared. And settings apply to writes made after the call — memories already stored are never re-extracted, so changing these is safe and never rewrites history. To see the effect, save, record a representative piece of text, and read the memory back.

Unhelpful guidance produces no error; extraction simply keeps different things.

Labels: tag your memories as they are written

Filtering recall is only as good as the tags a memory carries, and by default every one of them has to be supplied by hand on the write that created it. A label taxonomy hands that job to extraction: name a dimension once, and every memory is classified along it as it is written.

client.set_extraction_settings(
    "engineering",
    labels=[{
        "key": "name",
        "type": "multi-text",
        "tag": True,
        "description": (
            "Every name the subject of this memory is known by, including "
            "abbreviations, acronyms and short forms. Write them lowercase, "
            "words separated by single spaces, with no punctuation."
        ),
    }],
)

A group with tag: True is written onto the memory as the tag "<key>:<value>", so retrieve can filter on it through tag_groups. A memory about Kubernetes then carries name:kubernetes, name:k8s and name:kube, and any of the three finds it:

client.retrieve(
    "engineering",
    "what did we decide about k8s",
    tag_groups=[{"tags": ["name:k8s"], "match": "any_strict"}],
)

There are four group types, in two pairs. value and multi-values pick from a values list you supply, one or several. text and multi-text take whatever the memory itself supplies, one value or all of them, for a vocabulary you cannot enumerate in advance. multi-text is the one identity needs: a thing rarely has a single surface form, and text would keep only the first.

Because tags are matched as exact strings, describe the format you want in the group's description and normalise your query the same way.

A few things are refused rather than stored, because each would look saved and do nothing: an enumerated group with no values; a text/multi-text group with values; two groups sharing a key; a key that is not already in tag form (lowercase letters, digits, -, _); and free_form_entities=False with no labels, which would keep no entities at all rather than only labelled ones. Twelve groups maximum, since the taxonomy rides the extraction prompt on every write.

Labels apply to writes made after you save them, so turning a taxonomy on never retags your history.

Rules a space must follow

A rule is a standing instruction the space obeys whenever it answers. It is not a preference and not a tone setting: a rule applies to every answer the space gives, and it overrides what the memories say.

client.create_rule(
    "accounts",
    name="Never call revenue committed",
    content=(
        "Never describe revenue as committed unless a memory records a signed "
        "document. Name the stage the deal is actually at instead."
    ),
    priority=100,
)

priority orders the rules, highest first, with ties broken by whichever was written most recently. name is the label an answer reports back, so write it for whoever reads that answer.

Rules shape the answers the API synthesises for you — reason, ask_about_user, and the memory models a space keeps. They do not change what retrieve returns, and they do not reach a completion you make yourself through the LiteLLM integration: that prompt is yours.

A space may have 25 active rules at once. A rule that is switched off is stored and still editable but never applied, and does not count against the cap — so switching one off is always a way back under it without throwing work away:

for rule in client.list_rules("accounts"):
    state = "active" if rule["is_active"] else "off"
    print(rule["priority"], rule["name"], state)

client.update_rule("accounts", "rl_1", is_active=False)  # frees a slot
client.delete_rule("accounts", "rl_1")                   # gone for good

update_rule changes only the fields you pass, so switching a rule off or lifting its priority never means resending the text of something enforced on every answer.

An answer says which rules shaped it. POST /v1/reason returns rules_applied — the id and name of each rule that applied, and deliberately not its text, which is your own configuration and already in hand. An empty list is the useful finding that no rule applied, which is what separates "the memories are wrong" from "a rule said otherwise". client.reason() returns the synthesised text on its own; read that field from the API response if you need it.

Rules are owner-only, and free — an organisation out of credits must still be able to switch off a rule that is producing bad answers.

Framework adapters

Anona plugs into the Python agent frameworks through optional extras. Every adapter handles recall and storage for you, and scopes memories per end user.

Framework Install Import
LangChain / LangGraph pip install 'anona[langchain]' anona.integrations.langchain
CrewAI pip install 'anona[crewai]' anona.integrations.crewai
LlamaIndex pip install 'anona[llamaindex]' anona.integrations.llamaindex
Google ADK pip install 'anona[adk]' anona.integrations.google_adk
Microsoft Agent Framework pip install 'anona[msagent]' anona.integrations.ms_agent
AWS Strands pip install 'anona[strands]' anona.integrations.strands

All six are built on one MemoryBridge, which owns scope resolution and the failure contract:

from anona.integrations import MemoryBridge
from anona.integrations.langchain import AnonaMemory

bridge = MemoryBridge(
    api_key="anona_live_...",
    space_id="my-space",
    user_id="customer-42",   # optional scope: this user's memories only
)

agent = create_agent(model="gpt-4o-mini", middleware=[AnonaMemory(bridge=bridge)])

Memory failures never raise into your agent. A failed recall or store is logged and the agent runs on without memory, rather than taking your application down.

Each adapter's own module docstring documents its scoping, failure behaviour and per-call cost. Full docs: https://docs.anonalabs.com/integrations/langchain

Runnable end-to-end scripts for all six live in examples/.

LiteLLM integration

Auto-inject relevant memories into every litellm.completion() call, and auto-store the resulting Q&A pair:

from anona.integrations.litellm import AnonaMemory

mem = AnonaMemory(
    api_key="anona_live_...",
    space_id="space_123",
    recall_limit=5,       # how many memories to retrieve per call
    inject_mode="system", # "system" or "user"
    store_after=True,     # auto-store the exchange after each call
)
mem.enable()

# All subsequent litellm.completion() calls now auto-recall + auto-store.
import litellm
litellm.completion(model="gpt-4o", messages=[{"role": "user", "content": "..."}])

The anona command

Installing the package also installs a CLI. It exists so an agent can have memory without an API key in a config file, and so a person can use memory from a terminal.

anona start                                   # a temporary profile, no account
anona record   "The user prefers pnpm."       # store a memory
anona retrieve "package manager preference"   # search them
anona reason   "how does this team build?"    # answer from across them
anona login                                   # approve in a browser, keep it
anona status                                  # what is stored, and until when

anona start needs no signup: it creates a profile that holds memories for 72 hours, and anona login claims it into a real account with everything it holds.

record, retrieve and reason work on a temporary profile only. A login mints a credential scoped to MCP, which the REST API rejects on purpose, so on a signed-in account those three refuse and point at the two paths that do work: anona mcp with a client, or an API key with this SDK.

retrieve lists the memories that match. reason reads across them and answers in prose — ask it when no single memory holds the answer. It is an agent loop rather than a lookup, so it takes tens of seconds and says so on stderr while it works, leaving anona reason "..." > answer.md to capture only the answer.

anona mcp is a stdio MCP server for a client to launch, so the client needs no OAuth and no key of its own:

{ "mcpServers": { "anona": { "command": "anona", "args": ["mcp"] } } }

Full reference: docs.anonalabs.com/agents/cli.

MCP server

The SDK ships an MCP server so any MCP client — Claude Desktop, Claude Code, Cursor — can read and write Anona memory as native tools: record, retrieve, list_spaces, and reason.

Install the extra:

pip install "anona[mcp]"

Claude Desktop / Cursor — add to claude_desktop_config.json (or ~/.cursor/mcp.json), then restart:

{
  "mcpServers": {
    "anona": {
      "command": "uvx",
      "args": [
        "--from",
        "anona[mcp]",
        "anona-mcp"
      ],
      "env": {
        "ANONA_API_KEY": "anona_live_...",
        "ANONA_SPACE_ID": "space_123"
      }
    }
  }
}

Claude Code — one command:

claude mcp add anona \
  --env ANONA_API_KEY=anona_live_... \
  --env ANONA_SPACE_ID=space_123 \
  -- uvx --from "anona[mcp]" anona-mcp

ANONA_SPACE_ID sets the default space so you can just say "remember this" without naming one; override it per call with the space_id argument. The key is personal — the server only reaches spaces you are a member of.

Agent Skills

Two installable Agent Skills that teach a coding agent to use Anona without being told to — recall before a task, record after it — in Claude Code, Codex, Hermes, OpenClaw, OpenCode or Cursor.

curl -fsSL https://raw.githubusercontent.com/anonalabs/Anona-Memory-SDK/main/skills/install.sh | bash

Or npx skills add anonalabs/Anona-Memory-SDK, or the Claude Code plugin marketplace (/plugin marketplace add anonalabs/Anona-Memory-SDK). Source and options: skills/.

API reference

AnonaClient(
    api_key,
    base_url="https://api.anonalabs.com",
    timeout=120.0,
    max_retries=2,
    retry_max_wait=60.0,
)
Parameter Meaning
api_key Your anona_live_... key. Required.
base_url Defaults to the production API; pass it only to override.
timeout Per-request ceiling in seconds. It is a ceiling, not a wait: calls return the instant the server answers, and a loaded reason() can legitimately run 70–115s.
max_retries Retries for a 429, and for a 5xx on idempotent methods. 0 turns retrying off. See Retries.
retry_max_wait Cap, in seconds, on any single retry wait.

Anona is an alias for AnonaClient, matching the TypeScript package's class name. Every method below has an async_ twin (async_record, async_retrieve, …) taking the same arguments. close() / aclose() release the underlying HTTP clients, or use the client as a context manager. Full signatures and parameter docs are in each method's docstring and at docs.anonalabs.com/sdk/python.

Writing and reading memories

Method Description
record(space_id, content, …) Store a memory. background=True queues it and returns a job_id. context adds framing (where it came from, who said it) without becoming the memory's text. timestamp (ISO 8601) is when the event happened, for importing history. user_id / agent_id / session_id scope it inside the space. Pass route="auto" instead of space_id to let Anona choose: see Automatic space routing.
record_batch(space_id, items) Bulk-ingest up to 100 items. Always queued; returns a job_id. Also takes route="auto", which routes the whole batch as one unit.
get_job(space_id, job_id) Poll a queued job. Free. status is pending, processing, completed, failed, cancelled or not_found.
cancel_job(space_id, job_id) Cancel a queued ingestion job.
retrieve(space_id, query, …) Search. limit, mode ("accurate" or "fast"), the scope arguments, memory_type, min_score and top_k narrow it; tags / tags_match and tag_groups filter on tags (Compound tag filters); prefer_observations=False also returns the raw evidence behind a synthesised memory; member_id="me" returns only what you wrote in a shared space. Temporal arguments: Time travel. route="auto" and max_targets: Automatic space routing.
get_context(space_id, query, …) The same search, returned as one prompt-ready string. Takes the same filters as retrieve. max_tokens bounds it; block_order="stable" keeps the block byte-identical between turns so a provider's prompt cache can hit it.
retrieve_receipt(space_id, query, …) retrieve, plus a receipt explaining that search. Returns .memories and .receipt_id.
get_receipt(request_id) The manifest for one earlier search: what was returned, what was considered, what was cut.
explain(request_id, memory_id) Account for one specific memory against an earlier search.
reason(space_id, query, …) A synthesised answer across a space, as a string. depth is "fast" or "thorough"; model picks the LLM; tag_groups narrows what the reasoning agent may look at.
reason_receipt(space_id, query, …) reason, plus what the answer was built from: .sources, .rules_applied and the .model that actually answered. str() of the result is the answer.

Managing memories

Method Description
list_memories(space_id, …) Page through a space's memories. Filters by q, memory_type, state and scope.
get_memory_history(space_id, memory_id) Every recorded version of one memory, newest first.
update_memory(space_id, memory_id, …) Correct one memory in place; only the fields you pass change. state="invalidated" retires a memory reversibly, and reason is kept on its history.
delete_memory(space_id, memory_id) Delete one memory. Irreversible: prefer update_memory(..., state="invalidated").

Spaces and documents

Method Description
create_space(name, description=None, *, space_type=None) Create a space; a space's id is its name. space_type seeds it from a preset (mission, extraction guidance, disposition and a couple of standing questions). The returned space_type is null whenever nothing was applied, including when a type was asked for and applying it failed.
list_space_types() The types create_space accepts. Free; read it rather than hardcoding.
list_spaces() / get_space(space_id) List spaces, or fetch one.
delete_space(space_id) Delete a space and every memory in it. Irreversible.
upload_file(space_id, file, …) Upload a path, bytes or file object so retrieval can draw on it. Ingested asynchronously; returns job_ids. Accepts strategy, tags and the scope arguments. Files over 25 MB are rejected client-side.
list_documents(space_id, …) / get_document(space_id, document_id) List documents, or fetch one with its source and memory count.
delete_document(space_id, document_id) Remove a document and the memories extracted from it.
get_graph(space_id, limit=500, min_count=1) The entity relationship graph: nodes plus co-occurrence edges.
list_entities(space_id, …) / get_entity(space_id, entity_id) Entities, and one entity with its observations.

End users

Method Description
get_user_profile(space_id, user_id, …) Everything the space has learned about one end user. See User profiles.
ask_about_user(space_id, user_id, query, *, model=None) One synthesised answer, drawn from that user's memories only.

Space configuration

Method Description
get_extraction_settings / set_extraction_settings / reset_extraction_settings Steer what a write keeps. A set replaces the record. See Extraction settings.
list_rules / create_rule / update_rule / delete_rule Standing instructions every answer must follow. See Rules.
get_space_profile / set_space_profile / reset_space_profile A space's mission and disposition. Both arguments to the setter are required: the PUT is a full replace, so an omitted one clears that half. Owner-only.
get_reason_settings / set_reason_settings / reset_reason_settings The model and depth reason uses for a space. Both are required on set, for the same full-replace reason. Owner-only.
get_chat_settings / set_chat_settings / reset_chat_settings Per-space defaults for the drop-in proxy endpoints.
list_memory_models / create_memory_model / get_memory_model / update_memory_model / delete_memory_model Standing questions a space keeps an answer to.
refresh_memory_model / clear_memory_model / get_memory_model_history Re-answer a memory model from current memories, wipe its content but keep its definition, or read earlier versions.
list_catalog_models() Every LLM this deployment will answer with, and what each costs.

Webhooks and usage

Method Description
create_webhook(space_id, url, event_types=None, enabled=True) The response carries secret, returned only on create.
list_webhooks / update_webhook / delete_webhook Manage a space's webhooks.
list_webhook_deliveries(space_id, webhook_id, limit=50, cursor=None) Recent attempts, for debugging a receiver.
get_usage() Credits and rate limit for the organization this key belongs to.

Errors, retries and rate limits

Errors

Errors raise AnonaError(status_code, detail). The exception also carries code (the API's stable machine-readable handle, e.g. space_not_found), request_id (what support needs to find the call in the logs) and, on a 429, retry_after in seconds. A 503 with no request_id may be a Cloudflare-mangled 502 or 504: the edge strips the body of those two statuses, so the API rewrites them to 503 before they leave. Report such a failure with a timestamp rather than treating it as a malformed response.

Retries

A 429 is retried for every method, record included: the limiter refuses the request before anything runs, so re-sending cannot double-write. A 5xx is retried only for idempotent methods (GET, PUT, DELETE), because a 5xx on a write may arrive after the work landed, and replaying it would store the memory twice. Uploads are never retried. max_retries=0 turns it off.

A 429 waits as long as the server says to. The rate-limit window is a whole minute, so a client backing off on its own schedule tops out in the low seconds, spends its retries landing on the same full bucket, and fails anyway. The client reads Retry-After, falling back to the window_seconds the rate-limit body carries, and waits that out, capped at retry_max_wait (60s by default). A 5xx with no hint backs off briefly with jitter. AnonaError.retry_after carries the same number if you would rather schedule it yourself.

Staying inside the rate limit

Every metered response reports the budget it left you, so a bulk job can pace itself instead of discovering the ceiling by hitting it. client.rate_limit is updated in place on every call (limit, remaining, window_seconds, credits_remaining, retry_after):

client.retrieve("support", "billing")
rl = client.rate_limit
if rl.remaining is not None and rl.remaining < 5:
    time.sleep(rl.window_seconds or 60)

Fields are None until a metered call has been made. The unmetered routes (spaces, settings, webhooks) report no budget and leave the last reading in place rather than blanking it.

Requirements

  • Python >= 3.10
  • httpx >= 0.24
  • litellm >= 1.0 (optional, only for the LiteLLM integration)
  • mcp >= 1.2 (optional, only for the MCP server)
  • one of langchain, crewai, llama-index-core, google-adk, agent-framework-core, strands-agents (optional, only for the matching framework adapter — see the extras above for the verified version floors)

Documentation

Full guides and the complete API reference live at docs.anonalabs.com:

Page What it covers
Python SDK Every AnonaClient method, with examples
TypeScript SDK The Anona client and its adapters
Quickstart First key to first memory
Concepts Spaces, scoping, observations, credits
API reference The REST API these SDKs wrap
MCP server Local stdio and remote transports
Framework integrations LangChain, CrewAI, LlamaIndex, Google ADK and more

TypeScript

@anona-labs/memory has zero dependencies and runs on Node 18+, Bun, Deno, Cloudflare Workers and the browser, with adapters for the Vercel AI SDK and the OpenAI Agents SDK.

npm install @anona-labs/memory
import { Anona } from "@anona-labs/memory";

const anona = new Anona({ apiKey: process.env.ANONA_API_KEY! });
await anona.record({ spaceId: "support", content: "Alice prefers email" });
const hits = await anona.retrieve({ spaceId: "support", query: "how to contact Alice" });

See typescript/README.md and docs.anonalabs.com/sdk/javascript.

License

MIT

Metadata

Release files for anona 0.20.2

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

Source distribution (sdist)

Source distribution for anona 0.20.2
File Size Uploaded Provenance
anona-0.20.2.tar.gz 225.8 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 354.5 kB

Release files / anona-0.20.2.tar.gz

Download URL anona-0.20.2.tar.gz
Size 225.8 kB
Tags Source
SHA-256 checksum
How to use checksums
8e894d06a81b3c4386e47fd1a7545f21721355394989987a62fc52d905a7f18c
BLAKE2b-256 checksum
How to use checksums
7c91a358aef3ff32e6cd849a30e260146b443a1645b0f9dd355f6ba559eceb30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / anona-0.20.2-py3-none-any.whl

Download URL anona-0.20.2-py3-none-any.whl
Size 128.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8be72cf875f468288c28a8842a61b981d5c79d62b343326380391787e29b4c1d
BLAKE2b-256 checksum
How to use checksums
3f2c19ec8d3c20fe68c88ea88d257cd81face9f31e37719a7b83da1b6b65b9ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for anona 0.20.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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