---
slug: examples/agent-with-memory
title: An agent that uses memory
kind: howto
surface: agents
summary: A complete Aetherfy agent in Python and Node that writes and searches memories, with its aetherfy.yaml, the afy deploy command, and the environment variables every agent machine receives automatically.
sources:
  - aetherfy-control-plane:models/agent.py
  - aetherfy-control-plane:shared/config_parser.py
  - aetherfy-control-plane:orchestrator/fly_manager.py
  - aetherfy-cli:cmd/deploy.go
  - aetherfy-vectors-python-sdk:aetherfy_memory/client.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/namespace.py
  - aetherfy-vectors-js-sdk:src/memory/client.ts
  - aetherfy-vectors-js-sdk:src/memory/namespace.ts
---

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

```bash
afy deploy
```

## The Aetherfy agent manifest

A minimal `aetherfy.yaml`:

```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](/agents/aetherfy-yaml).

## 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](/agents/quickstart).

## 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](/platform/regions).

## A Python Aetherfy agent that uses memory

Three files in one directory.

### aetherfy.yaml

```yaml
name: memory-demo-py
runtime: python3.11
type: job
memory_mb: 256
```

### requirements.txt

```
aetherfy-vectors
```

The single `aetherfy-vectors` distribution provides both the `aetherfy_vectors` and `aetherfy_memory` import packages. There is nothing else to add.

### main.py

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

```bash
afy deploy
```

## A Node Aetherfy agent that uses memory

Three files in one directory.

### aetherfy.yaml

```yaml
name: memory-demo-node
runtime: node22
type: job
memory_mb: 256
```

Use `node20`/`node22` for plain JavaScript, or `node20-ts`/`node22-ts` if your entrypoint is TypeScript.

### package.json

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

```javascript
/**
 * 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:

```bash
afy deploy
```

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

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

```javascript
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](/examples/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](/examples/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](/agents/quickstart)
- Every manifest key: [aetherfy.yaml reference](/agents/aetherfy-yaml)
- The full memory surface: [The memory API](/examples/memory-api)
- Supplying vectors: [Bring your own embeddings](/examples/embeddings)
