agent-memory
by tihon-veliar
README.md
# agent-memory
Standalone memory microservice for agent memory management.
Converts completed agent sessions into long-term memory and exposes
retrieval through an MCP tool interface.
All runnable code must remain inside `sandbox/agent-memory/`.
## Requirements
- **Python** 3.12+
- **Qdrant** (local Docker container)
- **Ollama** (local, with models pulled)
- **Docker** (for Qdrant)
## Quick Install
```
pip install -e ".[dev]"
```
Copy and configure the environment file:
```
copy .env.example .env
```
## Dependencies At A Glance
| Dependency | Default Port | Purpose |
|-------------|---------------|-----------------------------------|
| Qdrant | `7333` | Vector database (via Docker) |
| Ollama | `11434` | LLM extraction + embedding |
| Service | `8000` | agent_memory_service FastAPI |
| MCP Server | stdio | agent_memory_mcp (no TCP port) |
| Sleep CLI | n/a | One-shot operator tool |
## Architecture Boundaries
The system has four separate package roots with clear ownership:
| Package | Entrypoint | Responsibility |
|--------------------------|-------------------------------|-------------------------------------|
| `agent_memory_service` | `python -m agent_memory_service` | HTTP API, persistence, retrieval |
| `agent_memory_mcp` | `python -m agent_memory_mcp` | MCP tool server (long-lived) |
| `agent_memory_sleep` | `python -m agent_memory_sleep` | Interactive operator CLI (one-shot) |
| Config layer | `src/agent_memory_service/` | Typed YAML loading, shared by all |
**Hard boundary rule:** neither `agent_memory_mcp` nor `agent_memory_sleep`
talk to Qdrant or Mem0 directly. All memory operations go through
`agent_memory_service` HTTP endpoints.
---
## Startup Order
Start the stack in this exact order. Each step depends on the previous one.
### 1. Start Qdrant (Docker)
```
docker run --name agent-memory-qdrant -p 7333:6333 -p 7334:6334 -d qdrant/qdrant
```
Verify:
```
curl http://localhost:7333/
```
**Windows note:** host port `6333` is reserved (TCP range 6315-6414),
so `config/memory.yaml` maps host `7333` to container `6333`.
If the container already exists but is stopped:
```
docker start agent-memory-qdrant
```
### 2. Verify Ollama models
```
ollama list
```
Required models (pull if missing):
```
ollama pull qwen2.5:3b
ollama pull qwen2.5:7b
ollama pull nomic-embed-text
```
### 3. Start agent_memory_service
```
python -m agent_memory_service
```
Verify:
```
curl http://localhost:8000/health
curl http://localhost:8000/up-status
```
`/up-status` reports component-by-component status for config loading,
Qdrant, Mem0 OSS, and the provider layer. All four must be `ok` before
proceeding.
### 4. Start agent_memory_mcp (separate terminal)
```
set AGENT_MEMORY_AGENT_ID=default
python -m agent_memory_mcp
```
The MCP server communicates over stdio. It refuses to start if the
service is unreachable at `http://localhost:8000/health`.
### 5. Run agent_memory_sleep (operator-initiated, one-shot)
```
python -m agent_memory_sleep
```
Follow the interactive prompts to:
- Select source runtime (opencode / codex)
- Enter agent ID and session export path
- Choose dream source (inline / profile / file)
- Select mode (dry_run to preview, commit to persist)
The sleep tool is one-shot — it exits after processing one session.
---
## Configuration
**Format:** YAML (frozen baseline — do not substitute JSON or TOML).
**Config directory:** `config/`
**Frozen config filenames:**
| File | Purpose |
|------------------------|----------------------------------|
| `config/agents.yaml` | Agent identity and behaviour |
| `config/providers.yaml`| Model provider and runtime wiring|
| `config/memory.yaml` | Memory store and persistence |
**Typed loading** lives under `src/agent_memory_service/`.
All config is validated at service startup — malformed files prevent boot.
**Environment overrides:**
| Variable | Config Field | Default |
|----------------------------|-----------------------------|----------------------|
| `AGENT_MEMORY_CONFIG_DIR` | Config directory path | `./config` |
| `AGENT_MEMORY_AGENT_ID` | Agent ID for MCP server | `default` (fallback) |
| `AGENT_MEMORY_SERVICE_URL` | Service URL for MCP server | `http://localhost:8000` |
---
## agents_hub Integration
`agents_hub` consumes `agent_memory_mcp` as an MCP server over stdio —
it never calls the service endpoints or Qdrant directly.
### MCP Client Configuration
Add `agent_memory_mcp` to your MCP client's server list as a stdio transport:
```json
{
"mcpServers": {
"agent_memory": {
"command": "python",
"args": ["-m", "agent_memory_mcp"],
"env": {
"AGENT_MEMORY_AGENT_ID": "default",
"AGENT_MEMORY_CONFIG_DIR": "./sandbox/agent-memory/config"
}
}
}
}
```
The MCP server exposes exactly one tool:
| Tool | Arguments | Returns |
|-----------------|--------------------|---------------------------------------|
| `memory_search` | `query` (required) | Formatted memory results with scores |
| | `limit` (optional) | scoped to the configured agent only |
**Blocked arguments:** `agent_id`, `user_id`, `user`, `provider` —
the MCP server rejects these immediately. Agent identity is resolved
from host context, not from tool arguments.
**Integration contract:**
- `agents_hub` must not use `POST /retrieve` or `POST /process` directly
- `agents_hub` must not connect to Qdrant or Mem0
- `agents_hub` must not set `agent_id` in `memory_search` arguments
- The MCP server is the only retrieval surface exposed to external clients
---
## Sleep Operator Guide
`agent_memory_sleep` is an interactive one-shot CLI that processes a
completed agent session into long-term memory.
### Step-by-step walkthrough
1. **Select runtime** — choose `opencode` or `codex` as session source
2. **Enter agent ID** — defaults to `"default"`, must match `config/agents.yaml`
3. **Provide session path** — absolute or relative path to the session export file
4. **Choose dream source:**
- `inline` — paste a JSON dream rule directly
- `profile` — select a named profile from `config/dream_profiles.yaml`
- `file` — provide an absolute path to a YAML/JSON dream file
5. **Select mode:**
- `dry_run` — extract and validate candidates without persistence
- `commit` — persist memory records through Mem0 / Qdrant
### Example session
```
$ python -m agent_memory_sleep
? Select source runtime: opencode
? Agent ID: default
? Path to opencode session export: tests/fixtures/opencode/session_sanitized.json
? Dream source: profile
? Dream profile name: default
? Mode: dry_run
╭────────────────────────────────────────╮
│ Session: ses_124a1da54ffeQ2cp0LkbaiADWt │
│ Agent: default │
│ Source: opencode │
│ Messages: 6 │
╰────────────────────────────────────────╯
Calling http://127.0.0.1:8000/process (mode=dry_run)...
Memory Record Candidates
┌───┬──────────────┬────────────┬──────────┐
│ # │ Content │ Kind │ Evidence │
├───┼──────────────┼────────────┼──────────┤
│ 1 │ ... │ preference │ ... │
│ 2 │ ... │ pattern │ ... │
└───┴──────────────┴────────────┴──────────┘
3 candidate(s)
Extraction: qwen2.5:3b | Embedding: nomic-embed-text
```
### Error handling
- Session load errors: malformed exports, missing IDs, unresolved agents
- Dream input errors: invalid JSON/YAML, missing required sections
- Connection errors: service unreachable at `http://127.0.0.1:8000`
- Service errors: extraction/embedding failures returned with error metadata
---
## Service Endpoints
| Method | Path | Purpose |
|--------|-------------|----------------------------------------------|
| GET | `/health` | Coarse health check (ok / error) |
| GET | `/up-status`| Component-by-component dependency status |
| POST | `/process` | Ingest a session (dry_run or commit) |
| POST | `/retrieve` | Search memory for an agent (internal use) |
`/process` request body:
```json
{
"session": { "... normalized session ..." },
"dream_rule": { "... dream rule ..." },
"mode": "dry_run"
}
```
`/retrieve` request body:
```json
{
"agent_id": "default",
"query": "user preferences",
"limit": 5
}
```
## Processed-Session Registry
**Storage:** local `SQLite`
**Ownership:** `agent_memory_service` — the registry is not accessed directly
by `agent_memory_mcp` or `agent_memory_sleep`; those packages must go through
the service boundary.
**Path source:** `config/memory.yaml` → `registry.path` (default:
`data/processed_sessions.db`, relative to the service working directory unless
absolute).
**Frozen module:** `src/agent_memory_service/registry.py`
**Frozen contract fields:**
| Field | Type | Purpose |
|----------------|------------|------------------------------------|
| `session_id` | `str` | Unique session identifier |
| `agent_id` | `str` | Canonical agent identity |
| `status` | `str` | Processing outcome |
| `processed_at` | `datetime` | Timestamp of processing completion |
The registry enforces idempotency: committing the same `session_id` twice
returns the existing entry without creating duplicate memory records.
---
## Diagnostics
Common operator failure states and how to resolve them.
### Qdrant unavailable
**Symptom:** `/up-status` reports `qdrant: unavailable`
```
docker ps --filter name=agent-memory-qdrant
curl http://localhost:7333/
```
**Fix:** start the Qdrant container — see Startup Order step 1.
### Service unreachable
**Symptom:** MCP server exits with `agent_memory_service unreachable`,
or sleep CLI reports connection errors.
```
curl http://localhost:8000/health
```
**Fix:** start the service — see Startup Order step 3.
### Ollama models missing
**Symptom:** `/up-status` reports `provider_layer: ok` but `/process` fails
with provider execution errors mentioning the model name.
```
ollama list
```
**Fix:** pull missing models — see Startup Order step 2.
### Ollama embeddings not supported (HTTP 501)
**Symptom:** `/process` fails with `This server does not support embeddings`.
```
curl http://localhost:11434/api/embed -d '{"model":"nomic-embed-text","input":"test"}'
```
**Fix:** ensure Ollama is running (`ollama serve`) and the model is pulled.
### Port conflicts (Windows)
**Symptom:** Docker cannot bind host port `6333`.
**Reason:** Windows reserves TCP ports 6315-6414.
**Fix:** already configured — `config/memory.yaml` uses host port `7333`.
### Duplicate session commit
**Symptom:** committing the same session twice reports `status: completed`
but `record_count: 0` on the second commit.
**Expected behaviour:** the registry returns the existing entry without
creating duplicate memory records. This is idempotency, not a bug.
### Memory not retrievable after commit
**Symptom:** sleep CLI reports `status: completed` with `record_count > 0`,
but `/retrieve` returns empty results.
**Check:**
```
curl -X POST http://localhost:8000/retrieve -H "Content-Type: application/json" -d "{\"agent_id\":\"default\",\"query\":\"test\",\"limit\":10}"
```
**Fix:** verify Qdrant collection exists and has the correct dimensions:
```
curl http://localhost:7333/collections
curl http://localhost:7333/collections/agent_memory
```
Check that `embedding_dims: 768` in `config/memory.yaml` matches the
model (`nomic-embed-text` produces 768-dimension vectors). Confirm the
correct `agent_id` is used in retrieval — memories are scoped per agent.
### SQLite registry failure
**Symptom:** `/process` commit mode fails with registry-related errors.
**Check:**
```
dir data\processed_sessions.db
```
**Fix:** ensure the `data/` directory exists relative to the service
working directory, and the service process has write permissions.
If the database is locked, stop any other process accessing it and
delete the file to recreate on next boot.
### Config loading failure
**Symptom:** service exits at startup with config errors.
```
python -c "from agent_memory_service.config import load_memory_config; print(load_memory_config().model_dump())"
```
**Fix:** check `config/memory.yaml`, `config/providers.yaml`, and
`config/agents.yaml` for valid YAML syntax and required fields.
---
## End-to-End Verification
Run this sequence after starting the full stack to verify the operating model
end to end.
### Automated verification (integration tests)
Requires Qdrant, Ollama, and service already running:
```
python -m pytest tests/test_end_to_end.py -v -m integration
```
These tests verify:
1. Service `/health` returns ok
2. `/up-status` reports Qdrant, Mem0, and provider layer as ok
3. `POST /process` (commit) persists memory records
4. `POST /retrieve` returns the persisted memory
5. Idempotency — duplicate commit returns the existing entry
### Manual verification script
1. Verify Qdrant:
```
curl http://localhost:7333/
```
2. Verify service health:
```
curl http://localhost:8000/health
curl http://localhost:8000/up-status
```
Expected: all four components (`config_loading`, `qdrant`, `mem0_oss`,
`provider_layer`) report `"status": "ok"`.
3. Process a session via sleep CLI (commit mode):
```
python -m agent_memory_sleep
```
Select:
- Source: `opencode`
- Agent: `default`
- Session path: `tests/fixtures/opencode/session_sanitized.json`
- Dream: `profile` → `default`
- Mode: `commit`
Expected: `Status: completed` with record count > 0.
4. Verify retrieval through the service endpoint (service-level smoke check):
```
curl -X POST http://localhost:8000/retrieve ^
-H "Content-Type: application/json" ^
-d "{\"agent_id\":\"default\",\"query\":\"user preferences and actions\",\"limit\":5}"
```
Expected: `results` array contains at least one memory with content
from the processed session.
5. Verify retrieval through the MCP server (MCP-client boundary):
First, start the MCP server in one terminal:
```
set AGENT_MEMORY_AGENT_ID=default
python -m agent_memory_mcp
```
Then call `memory_search` through an MCP client. Example verification
script (run from another terminal):
```
python -c "import asyncio, sys, os; from mcp import ClientSession; from mcp.client.stdio import StdioServerParameters, stdio_client; async def main(): server_params = StdioServerParameters(command=sys.executable, args=['-m', 'agent_memory_mcp'], env={**os.environ, 'AGENT_MEMORY_AGENT_ID': 'default'}); async with stdio_client(server_params) as (r, w): async with ClientSession(r, w) as session: await session.initialize(); result = await session.call_tool('memory_search', {'query': 'user preferences', 'limit': 5}); print('\\n'.join(c.text for c in result.content if hasattr(c, 'text'))); asyncio.run(main())"
```
Expected: formatted memory results including content from the processed
session, scoped to the configured agent.
6. Verify idempotency — re-run the sleep CLI with the same session:
```
python -m agent_memory_sleep
```
(Same selections as step 3)
Expected: `Status: completed` with `record_count: 0` — the existing
registry entry is returned without creating duplicates.
### Quick smoke checks
```
python -m pytest tests/ -q
python -c "import agent_memory_service, agent_memory_mcp, agent_memory_sleep; print('imports: ok')"
```
---
## Test
Run all automated tests (integration tests are skipped when services aren't running):
```
python -m pytest
```
Run specific test files:
```
python -m pytest tests/test_end_to_end.py -v
```
Run with real services (Qdrant + service + Ollama must be running):
```
python -m pytest tests/test_end_to_end.py -v -m integration
```
Markers overview:
| Marker | Purpose |
|---------------|------------------------------------------|
| `integration` | Requires live Qdrant, service, and Ollama |
---
## Local Defaults
| Component | Default Value | Config File |
|-------------------|------------------------|---------------------|
| Extraction model | `qwen2.5:3b` | providers.yaml |
| Fallback model | `qwen2.5:7b` | providers.yaml |
| Embedding model | `nomic-embed-text` | providers.yaml |
| Embedding dims | `768` | memory.yaml |
| Qdrant port | `7333` | memory.yaml |
| Ollama URL | `http://localhost:11434` | memory.yaml, providers.yaml |
| Service port | `8000` | (uvicorn default) |
| Registry path | `data/processed_sessions.db` | memory.yaml |
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues