Spaces:
Running
agentcache-python β Agent Instructions
What this project is
A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no iii-engine, no Dolt. Agents use it to store observations scoped to (folder_path, agent_id) pairs and global long-term memories. The viewer, MCP tools, and REST API are built around the folder-based memory model β sessions, lessons, slots, and actions are removed.
Project layout
src/
βββ app.py Thin Flask factory β create_app(), WebSocket, CORS hook
βββ cli.py CLI entrypoint (agentcache serve/migrate/export)
βββ connect.py Client connection helper
βββ db.py StateKV β SQLite WAL, per-thread connections, stats()
βββ functions.py All canonical business logic (large; memory/ shims delegate here)
βββ search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
βββ viewer_helpers.py Viewer HTML injection helper
βββ workers.py Background threads β index rebuild, auto-forget, SIGTERM handler
β
βββ routes/ Flask blueprints
β βββ observations.py /observe, /agent/observe, /folders, /folder/observations
β βββ memories.py /remember, /agent/remember, /memories, /forget
β βββ search.py /search, /timeline
β βββ graph.py /graph, /graph/stats, /graph/query, /graph/build
β βββ health.py /livez, /health, /audit, /config/flags
β βββ mcp.py /mcp/tools GET+POST (12 active tools)
β βββ migration.py /migrate
β
βββ memory/ Thin shim package β delegates to functions.py
β βββ observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
β βββ remember.py remember, forget, jaccard_similarity
β βββ context.py context, export_data, rebuild_index
β βββ graph.py folder_graph_build
β βββ timeline.py folder_timeline, folder_search
β βββ health.py health_check, auto_forget
β
βββ storage/ KV scope registry + path/ID utilities
β βββ scopes.py KV class (mirrored from functions.py)
β βββ paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
β βββ images.py save_image_to_disk, delete_image, touch_image
β
βββ viewer/
βββ index.html Single-file HTML dashboard (4 tabs: Folders / Memories / Graph / Timeline)
sync.py HuggingFace dataset backup/restore
Dockerfile HF Space container definition
start.sh Boot: restore DB β start server β start sync loop
requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
pyproject.toml pip-installable package (agentcache==0.9.8, Python β₯3.10)
tests/ pytest suite β unit, integration, and Hypothesis property tests
Running
pip install -r requirements.txt
python src/app.py
# Server on http://localhost:3111
# Viewer at http://localhost:3111/viewer
No build step. No external database. SQLite file lives at ~/.agentcache/agentcache.db.
Architecture
Data Model β Folder-Based Memory
The primary unit of storage is (folder_path, agent_id). Each agent accumulates observations scoped to the folder it is working in. Global long-term memories remain unchanged.
Storage β src/db.py
StateKV wraps a single SQLite file with:
kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))β all data as JSONaudit_log(id, ts, agent_id, message)β write audit trailsync_state_metadata(key, value)β HuggingFace sync watermark
Per-thread persistent connections via threading.local(). WAL checkpoint registered via atexit and on SIGTERM/SIGINT.
Key scopes (defined in functions.py KV class and mirrored in src/storage/scopes.py):
| Scope | Content |
|---|---|
mem:folders |
Index of all (folder_path, agent_id) pairs β key = "{path}:{agent}" |
mem:folder:{path}:{agent} |
Observations for a pair β key = obs_id |
mem:foldermeta:{path}:{agent} |
Metadata for a pair (obsCount, lastUpdated, summary) |
mem:obs_lookup |
O(1) reverse-lookup: obs_id β {folderPath, agentId} |
mem:memories |
Global long-term memories |
mem:index:bm25:* |
Sharded BM25 index (2MB chunks) |
mem:audit |
Audit log entries |
mem:relations |
Knowledge graph edges |
mem:sessions |
Legacy session store (read-only, migration only) |
mem:obs:{session_id} |
Legacy per-session observations (read-only, migration only) |
Business logic β src/functions.py
All canonical implementations live here. src/memory/* are thin shims that re-export from this module (for future decoupling).
Observation pipeline:
raw payload β normalize_folder_path() β validate_agent_id() β strip_private_data()
β build obs dict β kv.set(folder_obs scope) β upsert folder_meta + folders index
β kv.set(obs_lookup) β BM25-indexed β vector-indexed (if key set)
β schedule_save() (debounced 5s) β audit log β WebSocket broadcast
Memory versioning: remember() checks Jaccard similarity against existing memories. If > 0.7 match found, new memory supersedes old (isLatest=False on old, parentId set on new).
Search: folder_search() uses HybridSearch (BM25 + vector, RRF k=60). Falls back to BM25-only when no embedding provider is configured. Results include both folder observations and global memories.
health_check() returns: folderCount, agentCount, pairCount, observationCount, memoryCount, bm25IndexSize, vectorIndexSize, dbPath, plus db.stats() fields.
Search β src/search.py
SearchIndex: BM25 with Porter stemmer and synonym expansion. Dirty-flag (_dirty) prevents unnecessary saves. Persisted in sharded 2MB KV chunks.VectorIndex: cosine similarity over embeddings stored as base64-encoded float32 arrays. Also has_dirtyflag.HybridSearch: fuses BM25 + vector scores with RRF (k=60).
Embedding providers (auto-selected by priority in create_app()):
| Priority | Provider | Env var | Dimensions |
|---|---|---|---|
| 1 | GeminiEmbeddingProvider |
GEMINI_API_KEY / GOOGLE_API_KEY |
768 |
| 2 | OpenAIEmbeddingProvider |
OPENAI_API_KEY |
1536 |
| 3 | SentenceTransformerProvider |
AGENTCACHE_LOCAL_EMBEDDING_MODEL |
variable |
| 4 | BM25-only | β | β |
Server β src/app.py
Boot order:
- Load
~/.agentcache/.envif present - Initialize
StateKV(SQLite) - Auto-select embedding provider (Gemini β OpenAI β SentenceTransformer β BM25-only)
- Initialize
IndexPersistence(load or rebuild) - Backfill
obs_lookupindex if missing - Create Flask app, register blueprints
- Set up WebSocket
/stream/mem-live/viewer - Register CORS
after_requesthook - Start background workers (index rebuild if empty/stale, auto-forget loop)
Auth: all endpoints check AGENTCACHE_SECRET via hmac.compare_digest. /livez is always open.
MCP Tools
The server exposes 12 MCP tools via GET /agentcache/mcp/tools and POST /agentcache/mcp/tools.
| Tool | Description | Status |
|---|---|---|
agent_observe |
Log observation to a (folderPath, agentId) pair |
Working |
agent_remember |
Save to global long-term memory | Working |
memory_recall |
Search folder obs + global memories (BM25+vector) | Working |
memory_smart_search |
Hybrid semantic+keyword search (alias for recall) | Working |
memory_save |
Explicitly save insight to long-term memory | Working |
memory_export |
Export all data as JSON (v2 format) | Working |
memory_forget |
Delete memory or folder pair observations | Working |
memory_diagnose |
Health check (folder/agent/obs/memory counts) | Working |
memory_folders |
List all (folder, agent) pairs |
Working |
memory_folder_observations |
Get observations for a specific pair | Working |
memory_timeline |
Folder activity feed (sorted by time, filterable) | Working |
MCP stdio wrapper: src/mcp_stdio.py reads AGENTCACHE_URL and AGENTCACHE_SECRET from environment variables dynamically.
Consistency rules
When adding a REST endpoint:
- Add the route in the appropriate
src/routes/*.pyblueprint - Update the
API Referencesection inREADME.md - Add the MCP tool in
src/routes/mcp.pyif it should be agent-callable
When adding an MCP tool:
- Add the schema to the
GET /mcp/toolsresponse insrc/routes/mcp.py - Add the handler case to the
POST /mcp/toolsdispatch insrc/routes/mcp.py - Update the tool table in
README.md - Update
AGENTS.mdtool list
When changing data scopes:
- Update the
KVclass insrc/functions.py(canonical) - Mirror the change in
src/storage/scopes.py - Update the scope table in this file
Code patterns
Adding a new KV scope
# In src/functions.py KV class (canonical):
class KV:
your_scope = "mem:your-scope"
@staticmethod
def your_dynamic_scope(id: str) -> str:
return f"mem:your-scope:{id}"
Adding a REST endpoint
# In the appropriate src/routes/*.py blueprint:
@your_bp.route('/agentcache/your-path', methods=['POST'])
def your_endpoint():
auth_err = _check_auth()
if auth_err:
return auth_err
body = request.get_json(force=True) or {}
# validate fields explicitly β never pass raw body to functions
result = functions.your_function(_get_kv(), body.get('field'))
return jsonify(result), 200
Adding an MCP tool schema
In src/routes/mcp.py, GET /agentcache/mcp/tools handler:
{
"name": "memory_your_tool",
"description": "What it does",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "..."}
},
"required": ["query"]
}
}
In src/routes/mcp.py, POST /agentcache/mcp/tools handler:
elif tool_name == 'memory_your_tool':
query = args.get('query', '')
result = functions.your_function(kv, query)
return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
Environment variables
| Variable | Default | Purpose |
|---|---|---|
III_REST_PORT / PORT |
3111 |
Server port |
GEMINI_API_KEY / GOOGLE_API_KEY |
β | Enables Gemini vector search (priority 1) |
OPENAI_API_KEY |
β | Enables OpenAI vector search (priority 2) |
AGENTCACHE_LOCAL_EMBEDDING_MODEL |
β | SentenceTransformer model name (priority 3) |
AGENTCACHE_SECRET |
β | Bearer token auth |
AGENT_ID |
β | Default agent ID |
AGENTCACHE_AGENT_SCOPE=isolated |
β | Filter data to current agent |
AGENTCACHE_CWD |
β | Fallback folder path for legacy clients |
MAX_OBS_PER_FOLDER |
2000 |
Observations hard cap per (folder, agent) pair |
TOKEN_BUDGET |
2000 |
Context compilation cap |
GRAPH_EXTRACTION_ENABLED |
false |
Graph extraction (needs LLM) |
CONSOLIDATION_ENABLED |
false |
Consolidation (needs LLM) |
AGENTCACHE_AUTO_COMPRESS |
false |
LLM compression |
AUTO_FORGET_ENABLED |
β | Auto-forget sweep (set to "false" to disable) |
AGENTCACHE_CORS_ORIGINS |
see app.py | Comma-separated allowed origins |
AGENTCACHE_IMAGE_STORE_MAX_BYTES |
500MB | Image store byte limit |
HF_TOKEN |
β | HuggingFace sync |
AGENTCACHE_DATASET_REPO |
β | HF dataset repo for backup |
Testing
pip install -e ".[dev]"
pytest tests/ -v
Tests live in tests/ β 17 test files covering unit tests, integration tests (Flask test client), and Hypothesis property tests.
Key test files:
tests/test_properties.pyβ 8 correctness properties (pair isolation, obs count consistency, index coverage, privacy, timeline ordering, forget completeness, memory version uniqueness, path normalization idempotency)tests/test_api.pyβ Flask test client integration teststests/test_route_regressions.pyβ regression suite after blueprint split
HuggingFace Space deployment
Data flow: agentcache.db (SQLite) β sync.py β HF dataset repo.
start.sh sequence:
- Restore
agentcache.dbfrom dataset repo - Start
python src/app.pyin background - Run
sync.pyin a loop (backup every ~60s if changed)
Viewer β src/viewer/index.html
Single-file HTML dashboard, served directly by Flask at /viewer. No build step, no bundler.
Tabs: Folders, Memories, Graph, Timeline.
- Folders tab: lists all
(folder, agent)pairs; click a row to drill into observations - Memories tab: global long-term memories with search
- Graph tab: force-directed graph β nodes = folder paths, edges = same-parent / cross-ref / agent-shared
- Timeline tab: all observations sorted by timestamp desc, filterable by folder path and agent ID