An agent that uses memory
An Aetherfy agent is code you deploy from a directory containing an aetherfy.yaml. This page builds one that writes a memory and searches it, in Python and in Node, and deploys it with the CLI.
The important property: an agent does not need an API key in its configuration. Every Aetherfy agent machine receives AETHERFY_API_KEY in its environment automatically, and both SDKs read that variable when constructed with no arguments. So inside an agent, MemoryClient() just works.
What an Aetherfy agent is made of
| File | Purpose |
|---|---|
aetherfy.yaml | The agent manifest — name, runtime, type, resources |
| Your entrypoint | main.py, index.js, or whatever your runtime expects |
| A dependency declaration | requirements.txt for Python, package.json for Node |
Deploy the directory with the Aetherfy CLI. The binary is afy:
afy deployThe Aetherfy agent manifest
A minimal aetherfy.yaml:
name: support-bot
runtime: python3.11
type: service
memory_mb: 256| Key | Meaning |
|---|---|
name | The agent’s name |
runtime | Which runtime image to build on. See the table below |
type | service for a long-lived HTTP process, job for one that runs once and exits |
memory_mb | Memory allocated to the agent machine |
Valid runtime values:
| Runtime |
|---|
python3.11 |
python3.12 |
python3.13 |
node20 |
node22 |
node20-ts |
node22-ts |
bun |
dockerfile |
python on its own is not a valid runtime. Neither is node, or a version that is not in the table. Pick an exact value from the list.
The full manifest reference, including everything beyond these four keys, is at aetherfy.yaml reference.
Choosing between an Aetherfy service and a job
type | Behaviour | Use for |
|---|---|---|
service | Long-lived HTTP process, stays up | Something that answers requests |
job | Runs once and exits | Batch work, imports, scheduled work |
The worked example below uses type: job, because a job has an obvious start and end and so demonstrates the whole memory round trip in one run. Everything it does works identically inside a service; only the process lifecycle differs. For the HTTP contract a service must satisfy, see Deploy an agent.
Environment variables every Aetherfy agent receives
These are injected into the agent machine. You do not set them, and you should not hard-code their values.
| Variable | Contains |
|---|---|
AETHERFY_AGENT_ID | The agent’s id |
AETHERFY_AGENT_NAME | The agent’s name, as declared in aetherfy.yaml |
AETHERFY_REGION | The region this machine is running in |
AETHERFY_WORKSPACE | The workspace the agent belongs to |
AETHERFY_API_KEY | An Aetherfy API key — this is why the SDKs need no configuration |
AETHERFY_API_URL | The control API base URL |
AETHERFY_VECTORS_URL | The vector database base URL |
AETHERFY_SPAWN_URL | The spawn endpoint |
AETHERFY_DEPLOYMENT_ID | The id of the deployment this machine came from |
AETHERFY_SPAWN_ID | Present only on ephemeral runs |
Do not test for AETHERFY_SPAWN_ID to decide whether you are running on Aetherfy — it is absent on ordinary deployments. Use AETHERFY_AGENT_ID for that.
Agents reach the vector database in their own region, so a memory read from inside an agent does not cross a region boundary. Note that region topology is plan-scoped: Free and Starter agents run in a single region, and placement across more than one region begins at the plan named Performance. See Regions & replication.
A Python Aetherfy agent that uses memory
Three files in one directory.
aetherfy.yaml
name: memory-demo-py
runtime: python3.11
type: job
memory_mb: 256requirements.txt
aetherfy-vectorsThe single aetherfy-vectors distribution provides both the aetherfy_vectors and aetherfy_memory import packages. There is nothing else to add.
main.py
"""An Aetherfy agent that writes a memory and searches it back.
Runs as a `job`: it executes once and exits. No API key is configured
anywhere — MemoryClient() reads AETHERFY_API_KEY, which the Aetherfy
agent machine provides automatically.
"""
import hashlib
import os
from typing import List
from aetherfy_memory import MemoryClient
from aetherfy_memory.exceptions import NamespaceAlreadyExistsError
def embed(text: str) -> List[float]:
"""REPLACE ME with a real embedding model.
Aetherfy does not generate embeddings — every memory write must carry
a vector you computed. This placeholder is deterministic (so the agent
runs as written) but not semantic. See /examples/embeddings.
"""
h = hashlib.sha256(text.encode()).digest()
return [h[i % len(h)] / 255.0 for i in range(384)]
def main() -> None:
print("agent", os.environ.get("AETHERFY_AGENT_NAME"),
"region", os.environ.get("AETHERFY_REGION"))
# No arguments: the API key comes from AETHERFY_API_KEY.
memory = MemoryClient()
namespace_name = "customer-42"
try:
memory.create_namespace(namespace_name)
print("created namespace", namespace_name)
except NamespaceAlreadyExistsError:
print("namespace already exists", namespace_name)
ns = memory.namespace(namespace_name)
# Write a memory. add() is keyword-only and returns the point id.
memory_id = ns.add(
text="Lives in NYC and prefers email",
vector=embed("Lives in NYC and prefers email"),
metadata={"source": "support-transcript"},
)
print("wrote memory", memory_id)
# Read it back. search() is keyword-only in Python.
results = ns.search(
vector=embed("where is the customer based?"),
limit=3,
with_payload=True,
)
for r in results:
print("hit", r.id, r.payload)
# hit <uuid> {'text': 'Lives in NYC and prefers email',
# 'metadata': {'source': 'support-transcript'}}
print("total memories", ns.count())
if __name__ == "__main__":
main()Deploying the Python Aetherfy agent
From the directory containing those three files:
afy deployA Node Aetherfy agent that uses memory
Three files in one directory.
aetherfy.yaml
name: memory-demo-node
runtime: node22
type: job
memory_mb: 256Use node20/node22 for plain JavaScript, or node20-ts/node22-ts if your entrypoint is TypeScript.
package.json
{
"name": "memory-demo-node",
"private": true,
"type": "module",
"main": "index.js",
"dependencies": {
"aetherfy-vectors": "*"
}
}The npm package is aetherfy-vectors, unscoped. MemoryClient is exported from the package root; there is no aetherfy-vectors/memory subpath.
index.js
/**
* An Aetherfy agent that writes a memory and searches it back.
*
* Runs as a `job`: it executes once and exits. No API key is configured
* anywhere — new MemoryClient() reads AETHERFY_API_KEY, which the Aetherfy
* agent machine provides automatically.
*/
import { createHash } from 'node:crypto';
import { MemoryClient, NamespaceAlreadyExistsError } from 'aetherfy-vectors';
/**
* REPLACE ME with a real embedding model.
*
* Aetherfy does not generate embeddings — every memory write must carry a
* vector you computed. This placeholder is deterministic (so the agent runs
* as written) but not semantic. See /examples/embeddings.
*/
function embed(text) {
const h = createHash('sha256').update(text).digest();
return Array.from({ length: 384 }, (_, i) => h[i % h.length] / 255);
}
async function main() {
console.log('agent', process.env.AETHERFY_AGENT_NAME,
'region', process.env.AETHERFY_REGION);
// No arguments: the API key comes from AETHERFY_API_KEY.
const memory = new MemoryClient();
const namespaceName = 'customer-42';
try {
await memory.createNamespace(namespaceName);
console.log('created namespace', namespaceName);
} catch (e) {
if (!(e instanceof NamespaceAlreadyExistsError)) throw e;
console.log('namespace already exists', namespaceName);
}
const ns = await memory.namespace(namespaceName);
// Write a memory. add() takes one options object and resolves to the id.
const memoryId = await ns.add({
text: 'Lives in NYC and prefers email',
vector: embed('Lives in NYC and prefers email'),
metadata: { source: 'support-transcript' },
});
console.log('wrote memory', memoryId);
// Read it back. search() takes the vector positionally in JavaScript.
const results = await ns.search(embed('where is the customer based?'), {
limit: 3,
withPayload: true,
});
for (const r of results) console.log('hit', r.id, r.payload);
// hit <uuid> { text: 'Lives in NYC and prefers email',
// metadata: { source: 'support-transcript' } }
console.log('total memories', await ns.count());
}
main();Deploying the Node Aetherfy agent
From the directory containing those three files:
afy deployKeeping a conversation in an Aetherfy agent
A namespace holds standalone facts. When the agent is handling a conversation, use a thread instead — it stores ordered messages with a role and content, and reads back in order.
from aetherfy_memory import MemoryClient
from aetherfy_memory.exceptions import ThreadAlreadyExistsError
memory = MemoryClient()
try:
memory.create_thread("conv-99")
except ThreadAlreadyExistsError:
pass
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.",
vector=embed("I've sent a reset link."))
# Rebuild the recent context to hand to your model.
context = [(m.role, m.content) for m in chat.history(limit=20)]
print(context)
# [('user', 'Can you reset my password?'), ('assistant', "I've sent a reset link.")]import { MemoryClient, ThreadAlreadyExistsError } from 'aetherfy-vectors';
const memory = new MemoryClient();
try {
await memory.createThread('conv-99');
} catch (e) {
if (!(e instanceof ThreadAlreadyExistsError)) throw e;
}
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.",
vector: embed("I've sent a reset link.") });
const context = (await chat.history({ limit: 20 })).map((m) => [m.role, m.content]);
console.log(context);
// [ [ 'user', 'Can you reset my password?' ],
// [ 'assistant', "I've sent a reset link." ] ]Threads use append_many / appendMany for bulk writes — they have no add_many / addMany. And a Thread is not a Namespace subclass in either SDK, so code written against one is not reusable against the other for writes. Details in The memory API.
Things that will break an Aetherfy agent using memory
| Mistake | What happens | Fix |
|---|---|---|
runtime: python | The manifest is invalid | Use an exact value such as python3.11 |
| Hard-coding an API key in the agent | Works until the key is rotated | Rely on the injected AETHERFY_API_KEY |
Writing a memory without a vector | EmbeddingNotSupportedError, raised before any request | Compute a vector; see Bring your own embeddings |
A namespace name like -customer or has space | InvalidNameError | Names must match ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$ |
create_namespace on every run without catching | NamespaceAlreadyExistsError on the second run | Catch it, or check namespace_exists first |
Using metadata={"text": …} on a namespace | ValueError in Python, TypeError in JavaScript | text is reserved on a namespace, as are role/content/ts on a thread |
Calling add_many on a thread | Attribute/type error | Threads use append_many / appendMany |
Importing from aetherfy-vectors/memory in Node | Module not found | The package exports only "." — import from the root |
Next steps with Aetherfy agents
- Deployment mechanics and the service HTTP contract: Deploy an agent
- Every manifest key: aetherfy.yaml reference
- The full memory surface: The memory API
- Supplying vectors: Bring your own embeddings