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 shape
NamespaceStandalone facts and documentstext plus a vector
ThreadAn ordered conversationrole and content plus a vector

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();

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(...)
Empty the containerclear()clear()
Replace metadataset_metadata(...)setMetadata(...)
Merge metadatamerge_metadata(...)mergeMetadata(...)
Drop metadata keysdelete_metadata_keys(...)deleteMetadataKeys(...)
Payload schemaschema methodsschema methods
Analyticsget_analytics(...)getAnalytics(...)

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)create_thread(name)createNamespace(name)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.

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
# 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
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