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 |
|---|---|---|
| Namespace | Standalone facts and documents | text plus a vector |
| Thread | An ordered conversation | role 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()) # 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();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(...) |
| Empty 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 | schema methods |
| Analytics | get_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 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) | create_thread(name) | createNamespace(name) | 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.
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 |
# 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 |
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.