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:
| Container | For | Write shape | Stored as |
|---|---|---|---|
| Namespace | Standalone facts and documents | text plus a vector | One collection per namespace |
| Thread | An ordered conversation | role and content plus a vector | Rows 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()) # 3Your 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 hereimport { MemoryClient } from 'aetherfy-vectors';
const memory = new MemoryClient({ threadVectorSize: 1536 });
await memory.createThread('conv-99'); // no options hereAsking 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.
| Field | Type | Notes |
|---|---|---|
role | string | Required on write |
content | string | Required on write |
vector | list of floats, optional | Returned when you asked for vectors |
id | string or int, optional | The point id; an int you authored stays an int |
ts | float, optional | Unix timestamp; the SDK sets wall-clock time at add unless you pass one |
metadata | object | Your 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:
| Namespace | Thread | |
|---|---|---|
| Single write | add(text=…, vector=…) | add(role=…, content=…, vector=…) |
| Bulk write | add_many / addMany | append_many / appendMany |
| Ordered read | not applicable | history, 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.
| Operation | Python | JavaScript |
|---|---|---|
| Vector search | search(*, vector, …) | search(vector, options) |
| Fetch by id | retrieve(...) | retrieve(...) |
| Count | count(...) | count(...) |
| Iterate every item | iter(...) | async *iter(...) |
| Delete items | delete(...) | delete(...) |
| Drop the container | clear() | clear() |
| Replace metadata | set_metadata(...) | setMetadata(...) |
| Merge metadata | merge_metadata(...) | mergeMetadata(...) |
| Drop metadata keys | delete_metadata_keys(...) | deleteMetadataKeys(...) |
| Payload schema | schema methods, namespace only | schema 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 keyword | JavaScript option | Default |
|---|---|---|
vector (required, keyword-only) | first positional argument | — |
limit | limit | 10 |
offset | offset | 0 |
filter | filter | none |
with_payload | withPayload | true |
with_vectors | withVectors | false |
score_threshold | scoreThreshold | none |
search_params | searchParams | none |
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.
| Purpose | Python (namespace) | Python (thread) | JavaScript (namespace) | JavaScript (thread) |
|---|---|---|---|---|
| Create | create_namespace(name, *, vector_size, distance) | create_thread(name) | createNamespace(name, options) | createThread(name) |
| Get a handle | namespace(name) | thread(name) | namespace(name) | thread(name) |
| Existence check | namespace_exists(name) | thread_exists(name) | namespaceExists(name) | threadExists(name) |
| Read metadata | get_namespace(name) | get_thread(name) | getNamespace(name) | getThread(name) |
| List | list_namespaces() | list_threads() | listNamespaces() | listThreads() |
| Delete | delete_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}$| Rule | Consequence |
|---|---|
| First character | Must be a letter or a digit |
| Remaining characters | Letters, digits, ., _, - |
| Total length | 1 to 255 characters |
| Anything else | InvalidNameError |
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.
| Container | Reserved keys |
|---|---|
| Namespace | text |
| Thread | role, 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.
| Case | Result |
|---|---|
You omit id | The SDK generates a canonical hyphenated UUID string |
| You pass an integer id | It round-trips as an integer, not as "42" |
| You pass a UUID string | It 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
| Condition | Python | JavaScript |
|---|---|---|
| Base class | AetherfyMemoryException | AetherfyMemoryError |
| No such namespace | NamespaceNotFoundError | NamespaceNotFoundError |
| No such thread | ThreadNotFoundError | ThreadNotFoundError |
| Namespace already exists | NamespaceAlreadyExistsError | NamespaceAlreadyExistsError |
| Thread already exists | ThreadAlreadyExistsError | ThreadAlreadyExistsError |
| Threads exist at another vector size | ThreadVectorSizeMismatchError | ThreadVectorSizeMismatchError |
A write had no vector | EmbeddingNotSupportedError | EmbeddingNotSupportedError |
| Name fails the pattern | InvalidNameError | InvalidNameError |
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:
passimport { 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.