Skip to Content
ExamplesThe memory API
Raw

The memory API

The Aetherfy memory API is a higher-level surface over the vector database, shaped for agent memory. It gives you two containers:

ContainerForWrite shapeStored as
NamespaceStandalone facts and documentstext plus a vectorOne collection per namespace
ThreadAn ordered conversationrole and content plus a vectorRows in ONE collection shared by every thread

That storage difference is not a detail. A namespace counts against your plan’s collection limit; a thread does not, so the number of conversations you can hold is not capped by your plan’s collection count. It is also why a thread has no vector size of its own — see Every thread shares one collection.

You do not install anything extra. In Python the memory layer ships as the aetherfy_memory import package inside the same aetherfy-vectors distribution. In JavaScript it is exported from the aetherfy-vectors package root.

export AETHERFY_API_KEY="afy_live_your_key_here"

Aetherfy memory requires you to supply the vector

This is the constraint that shapes every example on this page: Aetherfy does not generate embeddings. Every write to a namespace or a thread must carry a vector. Omitting it raises EmbeddingNotSupportedError before any request leaves the process — it is not a server error you can retry past.

The examples below define a placeholder embed() so they run as written. Replace it with a real embedding model; see Bring your own embeddings. DEFAULT_VECTOR_SIZE is 384 in both SDKs, so the placeholder produces 384 floats.

Namespaces in Aetherfy memory with Python

Install with pip install aetherfy-vectors (Python >= 3.9).

import hashlib from aetherfy_memory import MemoryClient # Placeholder embedder — deterministic, 384 dimensions, NOT semantic. # Swap in a real model. See /examples/embeddings. def embed(text: str) -> list[float]: h = hashlib.sha256(text.encode()).digest() return [(h[i % len(h)] / 255.0) for i in range(384)] # Reads AETHERFY_API_KEY from the environment; workspace defaults to "auto". memory = MemoryClient() memory.create_namespace("customer-42") ns = memory.namespace("customer-42") # add() is keyword-only and returns the point id. memory_id = ns.add( text="Lives in NYC", vector=embed("Lives in NYC"), metadata={"source": "onboarding-form"}, ) print(memory_id) # a canonical hyphenated UUID string, e.g. generated by the SDK print(type(memory_id).__name__) # str # Several at once, one round trip. ids = ns.add_many([ {"text": "Prefers email over phone", "vector": embed("Prefers email over phone")}, {"text": "Renewal is in March", "vector": embed("Renewal is in March"), "metadata": {"source": "crm"}}, ]) print(len(ids)) # 2 # search() is keyword-only in Python — `vector=` is required. results = ns.search(vector=embed("where does the customer live?"), limit=3) for r in results: print(r.id, r.payload) # <uuid> {'text': 'Lives in NYC', 'metadata': {'source': 'onboarding-form'}} print(ns.count()) # 3

Your metadata is nested under a metadata key in the stored payload, and the text you passed is stored under text. That nesting is what stops your own keys shadowing the reserved ones.

Namespaces in Aetherfy memory with JavaScript

Install with npm install aetherfy-vectors (Node >= 20). Import MemoryClient from the package root — aetherfy-vectors/memory does not resolve.

import { createHash } from 'node:crypto'; import { MemoryClient } from 'aetherfy-vectors'; // Placeholder embedder — deterministic, 384 dimensions, NOT semantic. // Swap in a real model. See /examples/embeddings. function embed(text: string): number[] { const h = createHash('sha256').update(text).digest(); return Array.from({ length: 384 }, (_, i) => h[i % h.length] / 255); } const memory = new MemoryClient(); async function main() { await memory.createNamespace('customer-42'); const ns = await memory.namespace('customer-42'); // add() takes a single options object and resolves to the point id. const memoryId = await ns.add({ text: 'Lives in NYC', vector: embed('Lives in NYC'), metadata: { source: 'onboarding-form' }, }); console.log(memoryId); // a canonical hyphenated UUID string console.log(typeof memoryId); // string const ids = await ns.addMany([ { text: 'Prefers email over phone', vector: embed('Prefers email over phone') }, { text: 'Renewal is in March', vector: embed('Renewal is in March'), metadata: { source: 'crm' } }, ]); console.log(ids.length); // 2 // search() takes the vector POSITIONALLY in JavaScript. const results = await ns.search(embed('where does the customer live?'), { limit: 3 }); for (const r of results) console.log(r.id, r.payload); // <uuid> { text: 'Lives in NYC', metadata: { source: 'onboarding-form' } } console.log(await ns.count()); // 3 } main();

Every thread shares one collection in Aetherfy

Every thread in a workspace is stored in one collection, with the thread id as a payload key, and each per-thread operation is a filtered operation over it. Three consequences you can observe:

Creating a thread consumes no collection slot. Create as many conversations as you like; get_collections() / getCollections() does not grow with them, and neither does your usage against collections_limit.

Every thread shares one vector size and one distance metric, fixed when the shared collection is first created. So create_thread / createThread takes the id and nothing else — there is no per-thread vector_size. Set it on the client instead:

from aetherfy_memory import MemoryClient # Threads default to 384 dimensions. Set this if your model differs. memory = MemoryClient(thread_vector_size=1536) memory.create_thread("conv-99") # no vector_size here
import { MemoryClient } from 'aetherfy-vectors'; const memory = new MemoryClient({ threadVectorSize: 1536 }); await memory.createThread('conv-99'); // no options here

Asking for a size the shared collection does not have raises ThreadVectorSizeMismatchError, naming the size that is actually there, rather than failing later as a dimension error on your first write. create_namespace / createNamespace is unaffected and still takes its own vector_size / vectorSize: a namespace is still one collection.

A thread is a filter, and the memory layer owns it. Every read and write you make through a Thread carries the thread’s own clause, and a filter you pass is combined with it, never substituted for it — a filter of your own can narrow a thread’s results but cannot reach another thread’s messages.

Threads in Aetherfy memory with Python

A thread stores an ordered conversation. Each message carries a role and content — both required — and, as always, a vector.

import hashlib from aetherfy_memory import MemoryClient def embed(text: str) -> list[float]: h = hashlib.sha256(text.encode()).digest() return [(h[i % len(h)] / 255.0) for i in range(384)] memory = MemoryClient() memory.create_thread("conv-99") chat = memory.thread("conv-99") chat.add(role="user", content="Can you reset my password?", vector=embed("Can you reset my password?")) chat.add(role="assistant", content="I've sent a reset link to your email.", vector=embed("I've sent a reset link to your email.")) # Bulk append. Threads use append_many, NOT add_many. chat.append_many([ {"role": "user", "content": "Got it, thanks.", "vector": embed("Got it, thanks.")}, ]) for msg in chat.history(limit=20): print(msg.role, "|", msg.content) # user | Can you reset my password? # assistant | I've sent a reset link to your email. # user | Got it, thanks. # Newest first. for msg in chat.history(limit=1, order="desc"): print(msg.role, "|", msg.content) # user | Got it, thanks. # Stream the whole thread without choosing a limit. for msg in chat.iter_history(order="asc"): print(msg.role) # Threads are searchable like namespaces. hits = chat.search(vector=embed("password"), limit=2) for h in hits: print(h.id)

history(limit=50, *, order="asc") — order is keyword-only. iter_history(*, order="asc") is a generator and takes no limit.

Threads in Aetherfy memory with JavaScript

import { createHash } from 'node:crypto'; import { MemoryClient } from 'aetherfy-vectors'; function embed(text: string): number[] { const h = createHash('sha256').update(text).digest(); return Array.from({ length: 384 }, (_, i) => h[i % h.length] / 255); } const memory = new MemoryClient(); async function main() { await memory.createThread('conv-99'); const chat = await memory.thread('conv-99'); await chat.add({ role: 'user', content: 'Can you reset my password?', vector: embed('Can you reset my password?') }); await chat.add({ role: 'assistant', content: "I've sent a reset link to your email.", vector: embed("I've sent a reset link to your email.") }); // Bulk append. Threads use appendMany, NOT addMany. await chat.appendMany([ { role: 'user', content: 'Got it, thanks.', vector: embed('Got it, thanks.') }, ]); for (const msg of await chat.history({ limit: 20 })) { console.log(msg.role, '|', msg.content); } // user | Can you reset my password? // assistant | I've sent a reset link to your email. // user | Got it, thanks. for (const msg of await chat.history({ limit: 1, order: 'desc' })) { console.log(msg.role, '|', msg.content); } // user | Got it, thanks. for await (const msg of chat.iterHistory({ order: 'asc' })) { console.log(msg.role); } const hits = await chat.search(embed('password'), { limit: 2 }); for (const h of hits) console.log(h.id); } main();

The Aetherfy Message shape

history / iter_history yield messages with these fields.

FieldTypeNotes
rolestringRequired on write
contentstringRequired on write
vectorlist of floats, optionalReturned when you asked for vectors
idstring or int, optionalThe point id; an int you authored stays an int
tsfloat, optionalUnix timestamp; the SDK sets wall-clock time at add unless you pass one
metadataobjectYour own metadata; empty object when none was set

Pass ts explicitly when backfilling historical messages, since history orders by it.

Threads are not namespaces in Aetherfy

Thread is not a subclass of Namespace in either SDK. They share a read/scope base class, but their write APIs are genuinely different and not interchangeable:

NamespaceThread
Single writeadd(text=…, vector=…)add(role=…, content=…, vector=…)
Bulk writeadd_many / addManyappend_many / appendMany
Ordered readnot applicablehistory, iter_history / iterHistory

Do not write a helper typed to accept “a namespace or a thread” and call add on it — the required arguments differ. Calling add_many on a thread raises AttributeError in Python and is a TypeError in JavaScript, because threads only have append_many / appendMany.

The shared Aetherfy scope surface

Both namespaces and threads expose the same read and maintenance methods, inherited from a common base.

OperationPythonJavaScript
Vector searchsearch(*, vector, …)search(vector, options)
Fetch by idretrieve(...)retrieve(...)
Countcount(...)count(...)
Iterate every itemiter(...)async *iter(...)
Delete itemsdelete(...)delete(...)
Drop the containerclear()clear()
Replace metadataset_metadata(...)setMetadata(...)
Merge metadatamerge_metadata(...)mergeMetadata(...)
Drop metadata keysdelete_metadata_keys(...)deleteMetadataKeys(...)
Payload schemaschema methods, namespace onlyschema methods, namespace only

clear() drops the container: after it, the namespace or thread no longer exists and you create it again to reuse the name. On a thread it removes that thread’s rows and leaves every other thread in the workspace untouched.

A payload schema belongs to a collection, and a thread does not have one to itself, so get_schema / set_schema / delete_schema / analyze_schema / refresh_schema / clear_schema_cache (and their JavaScript spellings) are on Namespace only. They do not exist on Thread.

The Aetherfy memory search signature differs by language

This is the most common porting mistake in the memory API. Python is fully keyword-only; JavaScript takes the vector positionally.

# Python — `vector` MUST be a keyword. search(embed("x")) raises TypeError. results = ns.search( vector=embed("query text"), limit=10, offset=0, filter={"must": [{"key": "metadata.source", "match": {"value": "crm"}}]}, with_payload=True, with_vectors=False, score_threshold=None, search_params=None, )
// JavaScript — vector first, everything else in one options object. const results = await ns.search(embed('query text'), { limit: 10, offset: 0, filter: { must: [{ key: 'metadata.source', match: { value: 'crm' } }] }, withPayload: true, withVectors: false, });
Python keywordJavaScript optionDefault
vector (required, keyword-only)first positional argument—
limitlimit10
offsetoffset0
filterfilternone
with_payloadwithPayloadtrue
with_vectorswithVectorsfalse
score_thresholdscoreThresholdnone
search_paramssearchParamsnone

The filter object is the same must / should / must_not vocabulary used everywhere else in Aetherfy. In JavaScript write mustNot; the SDK translates it to must_not on the wire. See Filtered search.

Aetherfy memory container lifecycle

MemoryClient exposes six lifecycle methods for namespaces and the same six for threads.

PurposePython (namespace)Python (thread)JavaScript (namespace)JavaScript (thread)
Createcreate_namespace(name, *, vector_size, distance)create_thread(name)createNamespace(name, options)createThread(name)
Get a handlenamespace(name)thread(name)namespace(name)thread(name)
Existence checknamespace_exists(name)thread_exists(name)namespaceExists(name)threadExists(name)
Read metadataget_namespace(name)get_thread(name)getNamespace(name)getThread(name)
Listlist_namespaces()list_threads()listNamespaces()listThreads()
Deletedelete_namespace(name)delete_thread(name)deleteNamespace(name)deleteThread(name)

Every JavaScript method here is async and must be awaited, including namespace() and thread(). Every Python method here is synchronous.

create_thread / createThread is the one asymmetry: it takes only the id, because every thread shares one collection and therefore one vector size. get_thread / getThread reports that shared collection’s config, with the thread id as name and that thread’s own message count as points_count. A thread with no messages still exists, still lists, and still refuses a second create.

from aetherfy_memory import MemoryClient memory = MemoryClient() if not memory.namespace_exists("customer-42"): memory.create_namespace("customer-42") print([n for n in memory.list_namespaces()]) print(memory.delete_namespace("customer-42"))
import { MemoryClient } from 'aetherfy-vectors'; const memory = new MemoryClient(); async function main() { if (!(await memory.namespaceExists('customer-42'))) { await memory.createNamespace('customer-42'); } console.log(await memory.listNamespaces()); console.log(await memory.deleteNamespace('customer-42')); } main();

Naming rules for Aetherfy namespaces and threads

Names must match this pattern in both SDKs:

^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$
RuleConsequence
First characterMust be a letter or a digit
Remaining charactersLetters, digits, ., _, -
Total length1 to 255 characters
Anything elseInvalidNameError

customer-42, conv-99, tenant.acme_v2 are valid. -leading-dash, has space, emoji-🙂 are not.

Reserved metadata keys in Aetherfy memory

Some payload keys are owned by the memory layer. Using one as your own metadata key raises before any request is made — ValueError in Python, TypeError in JavaScript.

ContainerReserved keys
Namespacetext
Threadrole, content, ts, thread_id, thread_marker

thread_id and thread_marker are ordinary customer payload keys that the memory layer writes and filters on; they are reserved here so your metadata cannot shadow the thing that keeps one thread apart from another. They are not server-owned: the keys the server reserves for itself all begin with __aetherfy_, and these do not.

# Raises ValueError immediately — no request is sent. ns.add(text="hello", vector=embed("hello"), metadata={"text": "shadow"})
// Throws TypeError immediately — no request is sent. await ns.add({ text: 'hello', vector: embed('hello'), metadata: { text: 'shadow' } });

Rename the colliding key: source_text, speaker instead of role, recorded_at instead of ts.

Point ids in Aetherfy memory

add() returns the point id in both SDKs, typed str | int in Python and string | number in JavaScript.

CaseResult
You omit idThe SDK generates a canonical hyphenated UUID string
You pass an integer idIt round-trips as an integer, not as "42"
You pass a UUID stringIt round-trips as a string
You pass a slug such as "memory-001"Rejected with ValidationError

There is no coercion between the two forms. Keep human-readable keys in metadata and let the id be a UUID or an integer.

Aetherfy memory exceptions

ConditionPythonJavaScript
Base classAetherfyMemoryExceptionAetherfyMemoryError
No such namespaceNamespaceNotFoundErrorNamespaceNotFoundError
No such threadThreadNotFoundErrorThreadNotFoundError
Namespace already existsNamespaceAlreadyExistsErrorNamespaceAlreadyExistsError
Thread already existsThreadAlreadyExistsErrorThreadAlreadyExistsError
Threads exist at another vector sizeThreadVectorSizeMismatchErrorThreadVectorSizeMismatchError
A write had no vectorEmbeddingNotSupportedErrorEmbeddingNotSupportedError
Name fails the patternInvalidNameErrorInvalidNameError

Only the base class name differs between the SDKs. EmbeddingNotSupportedError is not transient and retrying will not help — compute a vector and pass it.

from aetherfy_memory import MemoryClient from aetherfy_memory.exceptions import NamespaceAlreadyExistsError memory = MemoryClient() try: memory.create_namespace("customer-42") except NamespaceAlreadyExistsError: pass
import { MemoryClient, NamespaceAlreadyExistsError } from 'aetherfy-vectors'; const memory = new MemoryClient(); try { await memory.createNamespace('customer-42'); } catch (e) { if (!(e instanceof NamespaceAlreadyExistsError)) throw e; }

Related: Bring your own embeddings for the vector you must supply, and An agent that uses memory for running this inside Aetherfy agent compute.

Last updated on