---
slug: examples/memory-api
title: The memory API
kind: howto
surface: vectors
summary: Complete Python and JavaScript examples of the Aetherfy memory layer - namespaces for facts, threads for conversations, their shared read surface, reserved metadata keys and the vector requirement.
sources:
  - aetherfy-vectors-python-sdk:aetherfy_memory/client.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/namespace.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/thread.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/scope.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/models.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/exceptions.py
  - aetherfy-vectors-js-sdk:src/memory/client.ts
  - aetherfy-vectors-js-sdk:src/memory/namespace.ts
  - aetherfy-vectors-js-sdk:src/memory/thread.ts
  - aetherfy-vectors-js-sdk:src/memory/scope.ts
  - aetherfy-vectors-js-sdk:src/memory/errors.ts
---

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

```bash
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](/examples/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).

```python
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.

```typescript
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`.

```python
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

```typescript
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
# 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,
)
```

```typescript
// 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](/examples/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.

```python
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"))
```

```typescript
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` |

```python
# Raises ValueError immediately — no request is sent.
ns.add(text="hello", vector=embed("hello"), metadata={"text": "shadow"})
```

```typescript
// 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.

```python
from aetherfy_memory import MemoryClient
from aetherfy_memory.exceptions import NamespaceAlreadyExistsError

memory = MemoryClient()
try:
    memory.create_namespace("customer-42")
except NamespaceAlreadyExistsError:
    pass
```

```typescript
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](/examples/embeddings) for the vector you must supply, and [An agent that uses memory](/examples/agent-with-memory) for running this inside Aetherfy agent compute.
