Spaces:
Sleeping
Sleeping
feat: migrate agentmemory to agentcache namespace, endpoints, and tools
Browse files- .clineskills +8 -9
- .cursorrules +14 -11
- .env.example +31 -31
- .kiro/specs/folder-based-memory/design.md +150 -54
- .kiro/specs/folder-based-memory/tasks.md +116 -297
- .windsurfrules +14 -11
- AGENTS.md +153 -106
- ARCHITECTURE.md +6 -6
- CLAUDE.md +77 -71
- Caddyfile +3 -3
- INSTALL_FOR_AGENTS.md +43 -43
- README.md +39 -39
- agentmemory.md → agentcache.md +2 -2
- docker-compose.yml +2 -2
- pyproject.toml +3 -3
- src/app.py +11 -8
- src/{memory → cache}/__init__.py +1 -1
- src/{memory → cache}/context.py +0 -0
- src/{memory → cache}/graph.py +0 -0
- src/{memory → cache}/health.py +0 -0
- src/{memory → cache}/observe.py +0 -0
- src/{memory → cache}/remember.py +0 -0
- src/{memory → cache}/timeline.py +0 -0
- src/cli.py +8 -8
- src/connect.py +42 -42
- src/db.py +1 -1
- src/functions.py +11 -11
- src/import_data.py +3 -3
- src/mcp_stdio.py +5 -5
- src/routes/graph.py +9 -5
- src/routes/health.py +12 -8
- src/routes/mcp.py +28 -25
- src/routes/memories.py +5 -1
- src/routes/migration.py +3 -2
- src/routes/observations.py +10 -1
- src/routes/search.py +5 -3
- src/storage/scopes.py +1 -1
- src/viewer/index.html +38 -38
- src/viewer_helpers.py +4 -4
- src/workers.py +1 -1
- start.sh +15 -14
- sync.py +6 -6
- tests/test_api.py +39 -36
- tests/test_context.py +4 -4
- tests/test_folder_observe.py +5 -5
- tests/test_properties.py +1 -1
- tests/test_remember.py +1 -1
- tests/test_route_regressions.py +138 -135
- tests/test_timeline.py +2 -2
.clineskills
CHANGED
|
@@ -1,20 +1,19 @@
|
|
| 1 |
-
# Agent
|
| 2 |
|
| 3 |
-
This workspace is integrated with long-term semantic memory via `
|
| 4 |
-
You must act as your own memory manager by calling the
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
-
At the start of every session or new task, immediately call `
|
| 10 |
-
- Example: `
|
| 11 |
|
| 12 |
2. **Lessons & Insights Capture**:
|
| 13 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 14 |
-
- Call `
|
| 15 |
-
- Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
| 16 |
|
| 17 |
3. **Checklist Before Ending**:
|
| 18 |
Before stating a task is complete:
|
| 19 |
-
- Reflect on whether any lessons learned should be saved.
|
| 20 |
-
- Call `
|
|
|
|
| 1 |
+
# Agent Cache Rules
|
| 2 |
|
| 3 |
+
This workspace is integrated with long-term semantic memory via `agentcache-python`.
|
| 4 |
+
You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
+
At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
|
| 10 |
+
- Example: `cache_smart_search(query="jwt token rotation logic")`
|
| 11 |
|
| 12 |
2. **Lessons & Insights Capture**:
|
| 13 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 14 |
+
- Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
|
|
|
| 15 |
|
| 16 |
3. **Checklist Before Ending**:
|
| 17 |
Before stating a task is complete:
|
| 18 |
+
- Reflect on whether any lessons learned or insights should be saved.
|
| 19 |
+
- Call `cache_dedup` to deduplicate any observations if needed.
|
.cursorrules
CHANGED
|
@@ -1,20 +1,23 @@
|
|
| 1 |
-
# Agent
|
| 2 |
|
| 3 |
-
This workspace is integrated with long-term semantic memory via `
|
| 4 |
-
You must act as your own memory manager by calling the
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
-
At the start of every session or new task, immediately call `
|
| 10 |
-
- Example: `
|
| 11 |
|
| 12 |
-
2. **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 14 |
-
- Call `
|
| 15 |
-
- Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
| 16 |
|
| 17 |
-
|
| 18 |
Before stating a task is complete:
|
| 19 |
-
- Reflect on whether any
|
| 20 |
-
-
|
|
|
|
| 1 |
+
# Agent Cache Rules
|
| 2 |
|
| 3 |
+
This workspace is integrated with long-term semantic memory via `agentcache-python`.
|
| 4 |
+
You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
+
At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, and past work.
|
| 10 |
+
- Example: `cache_smart_search(query="jwt token rotation logic")`
|
| 11 |
|
| 12 |
+
2. **Log Observations**:
|
| 13 |
+
As you work, call `agent_observe` to record what you are doing. Include `folderPath` (the working directory), `agentId` (your agent name), and `text` (what happened).
|
| 14 |
+
- Example: `agent_observe(folderPath="/your/project/path", agentId="cursor", text="Refactored auth middleware to use JWT rotation")`
|
| 15 |
+
|
| 16 |
+
3. **Save Long-Term Memories**:
|
| 17 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 18 |
+
- Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
|
|
|
| 19 |
|
| 20 |
+
4. **Checklist Before Ending**:
|
| 21 |
Before stating a task is complete:
|
| 22 |
+
- Reflect on whether any insights should be saved via `cache_save`.
|
| 23 |
+
- Review the folder activity feed with `cache_timeline` to confirm all significant work was captured.
|
.env.example
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
# =============================================================================
|
| 2 |
-
#
|
| 3 |
# =============================================================================
|
| 4 |
#
|
| 5 |
-
# Copy this file to `~/.
|
| 6 |
# prefer scoped config) and uncomment the lines you want to override.
|
| 7 |
#
|
| 8 |
-
# Every line is OFF by default — `
|
| 9 |
# LLM key, no embedding key, and no API auth. Set keys here only when you
|
| 10 |
# want to enable the corresponding feature.
|
| 11 |
#
|
| 12 |
-
# Run `npx @
|
| 13 |
-
# automatically. Run `npx @
|
| 14 |
# the daemon reads the env you expect.
|
| 15 |
#
|
| 16 |
# Defaults shown in comments. Listed in priority order — the first key
|
|
@@ -20,7 +20,7 @@
|
|
| 20 |
# 1. LLM provider — pick ONE
|
| 21 |
# -----------------------------------------------------------------------------
|
| 22 |
#
|
| 23 |
-
# Without a provider key,
|
| 24 |
# indexed via zero-LLM synthetic compression, hybrid search still works,
|
| 25 |
# but LLM-backed summarisation / reflection / consolidation are disabled.
|
| 26 |
# The detection order is OPENAI_API_KEY → MINIMAX_API_KEY → ANTHROPIC_API_KEY
|
|
@@ -49,12 +49,12 @@
|
|
| 49 |
# (Gemini, OpenRouter, MiniMax, OpenAI LLM, and OpenAI/Cohere/Voyage/OpenRouter
|
| 50 |
# embedding). The OpenAI LLM path also honours the OpenAI-scoped
|
| 51 |
# OPENAI_TIMEOUT_MS alias for back-compat with v0.9.17 (precedence).
|
| 52 |
-
#
|
| 53 |
|
| 54 |
# Opt-in Claude-subscription fallback (spawns @anthropic-ai/claude-agent-sdk
|
| 55 |
# child sessions). Off by default — the agent-sdk fallback can trigger
|
| 56 |
# Stop-hook recursion (#149 follow-up) when invoked from inside Claude Code.
|
| 57 |
-
#
|
| 58 |
|
| 59 |
# FALLBACK_PROVIDERS=anthropic,gemini # Comma-separated chain tried after the primary provider returns an error (e.g. rate limit)
|
| 60 |
|
|
@@ -62,7 +62,7 @@
|
|
| 62 |
# 2. Embedding provider — auto-detected, override via EMBEDDING_PROVIDER
|
| 63 |
# -----------------------------------------------------------------------------
|
| 64 |
#
|
| 65 |
-
# Without an embedding key,
|
| 66 |
# search. Detection order: EMBEDDING_PROVIDER override → GEMINI_API_KEY →
|
| 67 |
# OPENAI_API_KEY → VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
|
| 68 |
# local (Xenova/all-MiniLM-L6-v2, 384-dim).
|
|
@@ -87,7 +87,7 @@
|
|
| 87 |
# Without a secret, REST endpoints are open on loopback. Set this when
|
| 88 |
# you expose the daemon beyond loopback or run behind a reverse proxy.
|
| 89 |
|
| 90 |
-
#
|
| 91 |
|
| 92 |
# -----------------------------------------------------------------------------
|
| 93 |
# 4. Search tuning
|
|
@@ -95,7 +95,7 @@
|
|
| 95 |
|
| 96 |
# BM25_WEIGHT=0.4 # Hybrid search weight for BM25 leg
|
| 97 |
# VECTOR_WEIGHT=0.6 # Hybrid search weight for vector leg
|
| 98 |
-
#
|
| 99 |
# TOKEN_BUDGET=2000 # Max tokens injected via mem::context per session
|
| 100 |
# MAX_OBS_PER_SESSION=500 # Per-session observation cap before consolidation kicks in
|
| 101 |
# SUMMARIZE_CHUNK_SIZE=400 # When mem::summarize sees a session larger than this, it chunks observations and map-reduces (chunk-summarize → reduce-merge) to stay within the LLM's context window. Default 400 ≈ 50k tokens per chunk at ~110 tok/obs. Native sessions are capped by MAX_OBS_PER_SESSION; chunking primarily matters for bulk-imported jsonl sessions, which bypass that cap.
|
|
@@ -105,35 +105,35 @@
|
|
| 105 |
# 5. Behaviour flags
|
| 106 |
# -----------------------------------------------------------------------------
|
| 107 |
|
| 108 |
-
#
|
| 109 |
-
#
|
| 110 |
# CONSOLIDATION_ENABLED=true # Run the 4-tier consolidation pipeline (memories → semantic → procedural). Default off — opt in once you've measured the LLM cost.
|
| 111 |
# CONSOLIDATION_DECAY_DAYS=30 # Age (days) after which non-reinforced memories decay during consolidation
|
| 112 |
# GRAPH_EXTRACTION_ENABLED=true # Extract concept-graph edges on remember; powers the graph-traversal recall path
|
| 113 |
# GRAPH_EXTRACTION_BATCH_SIZE=8 # Memories per graph-extraction batch
|
| 114 |
-
#
|
| 115 |
-
#
|
| 116 |
-
#
|
| 117 |
|
| 118 |
# -----------------------------------------------------------------------------
|
| 119 |
# 6. CLI / runtime knobs
|
| 120 |
# -----------------------------------------------------------------------------
|
| 121 |
|
| 122 |
-
#
|
| 123 |
-
#
|
| 124 |
-
#
|
| 125 |
-
#
|
| 126 |
-
#
|
| 127 |
-
#
|
| 128 |
-
#
|
| 129 |
-
#
|
| 130 |
|
| 131 |
-
# STANDALONE_MCP=1 # MCP shim only — bypass the worker and run @
|
| 132 |
-
# STANDALONE_PERSIST_PATH=~/.
|
| 133 |
|
| 134 |
# Snapshot exporter — periodic snapshots of state_store + stream_store.
|
| 135 |
# SNAPSHOT_ENABLED=true
|
| 136 |
-
# SNAPSHOT_DIR=~/.
|
| 137 |
# SNAPSHOT_INTERVAL=3600 # Seconds between snapshots
|
| 138 |
|
| 139 |
# Team sharing — when set, memories are scoped to (TEAM_ID, USER_ID) tuples.
|
|
@@ -153,12 +153,12 @@
|
|
| 153 |
# 8. iii engine pin
|
| 154 |
# -----------------------------------------------------------------------------
|
| 155 |
#
|
| 156 |
-
#
|
| 157 |
-
# new sandbox-everything-via-`iii worker add` model that
|
| 158 |
-
# hasn't been refactored for yet. Override with
|
| 159 |
# only after migrating to the sandbox model manually.
|
| 160 |
|
| 161 |
-
#
|
| 162 |
|
| 163 |
# -----------------------------------------------------------------------------
|
| 164 |
# 9. Claude Code bridge (opt-in)
|
|
|
|
| 1 |
# =============================================================================
|
| 2 |
+
# agentcache configuration
|
| 3 |
# =============================================================================
|
| 4 |
#
|
| 5 |
+
# Copy this file to `~/.agentcache/.env` (or to your project root if you
|
| 6 |
# prefer scoped config) and uncomment the lines you want to override.
|
| 7 |
#
|
| 8 |
+
# Every line is OFF by default — `agentcache` runs out of the box with no
|
| 9 |
# LLM key, no embedding key, and no API auth. Set keys here only when you
|
| 10 |
# want to enable the corresponding feature.
|
| 11 |
#
|
| 12 |
+
# Run `npx @agentcache/agentcache init` to copy this file into place
|
| 13 |
+
# automatically. Run `npx @agentcache/agentcache doctor` to verify that
|
| 14 |
# the daemon reads the env you expect.
|
| 15 |
#
|
| 16 |
# Defaults shown in comments. Listed in priority order — the first key
|
|
|
|
| 20 |
# 1. LLM provider — pick ONE
|
| 21 |
# -----------------------------------------------------------------------------
|
| 22 |
#
|
| 23 |
+
# Without a provider key, agentcache runs in noop mode: observations are
|
| 24 |
# indexed via zero-LLM synthetic compression, hybrid search still works,
|
| 25 |
# but LLM-backed summarisation / reflection / consolidation are disabled.
|
| 26 |
# The detection order is OPENAI_API_KEY → MINIMAX_API_KEY → ANTHROPIC_API_KEY
|
|
|
|
| 49 |
# (Gemini, OpenRouter, MiniMax, OpenAI LLM, and OpenAI/Cohere/Voyage/OpenRouter
|
| 50 |
# embedding). The OpenAI LLM path also honours the OpenAI-scoped
|
| 51 |
# OPENAI_TIMEOUT_MS alias for back-compat with v0.9.17 (precedence).
|
| 52 |
+
# AGENTCACHE_LLM_TIMEOUT_MS=60000 # Default: 60 000 ms (60 s)
|
| 53 |
|
| 54 |
# Opt-in Claude-subscription fallback (spawns @anthropic-ai/claude-agent-sdk
|
| 55 |
# child sessions). Off by default — the agent-sdk fallback can trigger
|
| 56 |
# Stop-hook recursion (#149 follow-up) when invoked from inside Claude Code.
|
| 57 |
+
# AGENTCACHE_ALLOW_AGENT_SDK=true
|
| 58 |
|
| 59 |
# FALLBACK_PROVIDERS=anthropic,gemini # Comma-separated chain tried after the primary provider returns an error (e.g. rate limit)
|
| 60 |
|
|
|
|
| 62 |
# 2. Embedding provider — auto-detected, override via EMBEDDING_PROVIDER
|
| 63 |
# -----------------------------------------------------------------------------
|
| 64 |
#
|
| 65 |
+
# Without an embedding key, agentcache runs in BM25-only mode for hybrid
|
| 66 |
# search. Detection order: EMBEDDING_PROVIDER override → GEMINI_API_KEY →
|
| 67 |
# OPENAI_API_KEY → VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
|
| 68 |
# local (Xenova/all-MiniLM-L6-v2, 384-dim).
|
|
|
|
| 87 |
# Without a secret, REST endpoints are open on loopback. Set this when
|
| 88 |
# you expose the daemon beyond loopback or run behind a reverse proxy.
|
| 89 |
|
| 90 |
+
# AGENTCACHE_SECRET=your-secret-here
|
| 91 |
|
| 92 |
# -----------------------------------------------------------------------------
|
| 93 |
# 4. Search tuning
|
|
|
|
| 95 |
|
| 96 |
# BM25_WEIGHT=0.4 # Hybrid search weight for BM25 leg
|
| 97 |
# VECTOR_WEIGHT=0.6 # Hybrid search weight for vector leg
|
| 98 |
+
# AGENTCACHE_GRAPH_WEIGHT=0.2 # Graph traversal bonus on smart-search ranking
|
| 99 |
# TOKEN_BUDGET=2000 # Max tokens injected via mem::context per session
|
| 100 |
# MAX_OBS_PER_SESSION=500 # Per-session observation cap before consolidation kicks in
|
| 101 |
# SUMMARIZE_CHUNK_SIZE=400 # When mem::summarize sees a session larger than this, it chunks observations and map-reduces (chunk-summarize → reduce-merge) to stay within the LLM's context window. Default 400 ≈ 50k tokens per chunk at ~110 tok/obs. Native sessions are capped by MAX_OBS_PER_SESSION; chunking primarily matters for bulk-imported jsonl sessions, which bypass that cap.
|
|
|
|
| 105 |
# 5. Behaviour flags
|
| 106 |
# -----------------------------------------------------------------------------
|
| 107 |
|
| 108 |
+
# AGENTCACHE_AUTO_COMPRESS=true # Run LLM compression on every observation batch (requires a provider key). Default off — synthetic compression handles most cases.
|
| 109 |
+
# AGENTCACHE_INJECT_CONTEXT=true # Inject recalled memories back into agent prompts (#143). Default off — hooks capture observations but do not modify conversation.
|
| 110 |
# CONSOLIDATION_ENABLED=true # Run the 4-tier consolidation pipeline (memories → semantic → procedural). Default off — opt in once you've measured the LLM cost.
|
| 111 |
# CONSOLIDATION_DECAY_DAYS=30 # Age (days) after which non-reinforced memories decay during consolidation
|
| 112 |
# GRAPH_EXTRACTION_ENABLED=true # Extract concept-graph edges on remember; powers the graph-traversal recall path
|
| 113 |
# GRAPH_EXTRACTION_BATCH_SIZE=8 # Memories per graph-extraction batch
|
| 114 |
+
# AGENTCACHE_REFLECT=true # Periodically auto-synthesize lessons from memories
|
| 115 |
+
# AGENTCACHE_DROP_STALE_INDEX=true # Drop on-disk BM25 / vector index on startup if dim guard fires (#248). Recovery toggle for stuck-state debugging.
|
| 116 |
+
# AGENTCACHE_IMAGE_EMBEDDINGS=true # Enable image embeddings when an image provider is present (experimental).
|
| 117 |
|
| 118 |
# -----------------------------------------------------------------------------
|
| 119 |
# 6. CLI / runtime knobs
|
| 120 |
# -----------------------------------------------------------------------------
|
| 121 |
|
| 122 |
+
# AGENTCACHE_TOOLS=all # core (7 tools, default) | all (51 tools) — surface exposed to MCP clients
|
| 123 |
+
# AGENTCACHE_SLOTS=memory # Comma-separated plugin slot names the CLI should claim
|
| 124 |
+
# AGENTCACHE_DEBUG=1 # Trace MCP shim probe + standalone fallback decisions to stderr
|
| 125 |
+
# AGENTCACHE_FORCE_PROXY=1 # Skip the MCP shim livez probe and trust AGENTCACHE_URL (for sandboxed MCP clients that can't reach localhost)
|
| 126 |
+
# AGENTCACHE_PROBE_TIMEOUT_MS=2000 # MCP shim livez probe timeout
|
| 127 |
+
# AGENTCACHE_URL=http://localhost:3111 # REST base URL — honored by status, doctor, MCP shim
|
| 128 |
+
# AGENTCACHE_VIEWER_URL=http://localhost:3113 # Override the viewer URL printed by `agentcache status`
|
| 129 |
+
# AGENTCACHE_EXPORT_ROOT=~/agentcache-backup # Default destination for `agentcache export`
|
| 130 |
|
| 131 |
+
# STANDALONE_MCP=1 # MCP shim only — bypass the worker and run @agentcache/mcp in-process
|
| 132 |
+
# STANDALONE_PERSIST_PATH=~/.agentcache/local.db # Path used by the standalone MCP shim's local fallback store
|
| 133 |
|
| 134 |
# Snapshot exporter — periodic snapshots of state_store + stream_store.
|
| 135 |
# SNAPSHOT_ENABLED=true
|
| 136 |
+
# SNAPSHOT_DIR=~/.agentcache/snapshots
|
| 137 |
# SNAPSHOT_INTERVAL=3600 # Seconds between snapshots
|
| 138 |
|
| 139 |
# Team sharing — when set, memories are scoped to (TEAM_ID, USER_ID) tuples.
|
|
|
|
| 153 |
# 8. iii engine pin
|
| 154 |
# -----------------------------------------------------------------------------
|
| 155 |
#
|
| 156 |
+
# agentcache currently pins iii-engine to v0.11.2 — v0.11.6 introduces a
|
| 157 |
+
# new sandbox-everything-via-`iii worker add` model that agentcache
|
| 158 |
+
# hasn't been refactored for yet. Override with AGENTCACHE_III_VERSION
|
| 159 |
# only after migrating to the sandbox model manually.
|
| 160 |
|
| 161 |
+
# AGENTCACHE_III_VERSION=0.11.2
|
| 162 |
|
| 163 |
# -----------------------------------------------------------------------------
|
| 164 |
# 9. Claude Code bridge (opt-in)
|
.kiro/specs/folder-based-memory/design.md
CHANGED
|
@@ -10,6 +10,45 @@ The redesign eliminates sessions, lessons, slots, actions, crystals, and artefac
|
|
| 10 |
|
| 11 |
## Architecture
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
### High-Level System Diagram
|
| 14 |
|
| 15 |
```mermaid
|
|
@@ -20,7 +59,7 @@ graph TD
|
|
| 20 |
A3[Cursor]
|
| 21 |
end
|
| 22 |
|
| 23 |
-
subgraph
|
| 24 |
T1[agent_observe]
|
| 25 |
T2[memory_recall]
|
| 26 |
T3[memory_save / agent_remember]
|
|
@@ -28,62 +67,70 @@ graph TD
|
|
| 28 |
T5[memory_export]
|
| 29 |
T6[memory_forget]
|
| 30 |
T7[memory_diagnose]
|
|
|
|
|
|
|
|
|
|
| 31 |
end
|
| 32 |
|
| 33 |
-
subgraph
|
| 34 |
F1[folder_observe]
|
| 35 |
F2[folder_search]
|
| 36 |
F3[remember / forget]
|
| 37 |
F4[folder_timeline]
|
| 38 |
F5[folder_graph_build]
|
| 39 |
-
F6[health_check /
|
| 40 |
F7[strip_private_data]
|
| 41 |
-
F8[IndexPersistence]
|
| 42 |
end
|
| 43 |
|
| 44 |
-
subgraph
|
| 45 |
S1["mem:folders (index)"]
|
| 46 |
-
S2["mem:folder:{path}:{agent} (observations
|
| 47 |
-
S3[mem:
|
| 48 |
-
S4[
|
| 49 |
-
S5[
|
|
|
|
|
|
|
| 50 |
end
|
| 51 |
|
| 52 |
-
subgraph
|
| 53 |
-
SR1[SearchIndex BM25]
|
| 54 |
-
SR2[VectorIndex cosine]
|
| 55 |
-
SR3[HybridSearch RRF]
|
|
|
|
|
|
|
|
|
|
| 56 |
end
|
| 57 |
|
| 58 |
-
subgraph
|
| 59 |
V1[Folders tab]
|
| 60 |
V2[Memories tab]
|
| 61 |
V3[Graph tab]
|
| 62 |
V4[Timeline tab]
|
| 63 |
end
|
| 64 |
|
| 65 |
-
A1 & A2 & A3 --> T1 & T2 & T3 & T4 & T5 & T6 & T7
|
| 66 |
T1 --> F1
|
| 67 |
T2 & T4 --> F2
|
| 68 |
T3 --> F3
|
| 69 |
T5 --> F6
|
| 70 |
T6 --> F3
|
| 71 |
T7 --> F6
|
|
|
|
|
|
|
| 72 |
|
| 73 |
F1 --> F7
|
| 74 |
-
F1 --> S2
|
| 75 |
F1 --> SR1 & SR2
|
| 76 |
-
|
| 77 |
-
F3 --> S3
|
| 78 |
F2 --> SR3
|
| 79 |
SR1 & SR2 --> SR3
|
| 80 |
-
|
|
|
|
| 81 |
SR1 & SR2 --> F8
|
| 82 |
|
| 83 |
-
F4 --> S2
|
| 84 |
-
F5 --> S1 & S2 &
|
| 85 |
-
|
| 86 |
-
S2 & S3 & S4 & S5 --> Storage
|
| 87 |
|
| 88 |
Viewer --> T2 & T4
|
| 89 |
F1 -.->|WebSocket broadcast| Viewer
|
|
@@ -150,26 +197,28 @@ sequenceDiagram
|
|
| 150 |
|
| 151 |
## Components and Interfaces
|
| 152 |
|
| 153 |
-
### Component 1: KV Scope Registry (src/functions.py — `KV` class)
|
| 154 |
|
| 155 |
-
**Purpose**: Single source of truth for all KV scope names.
|
| 156 |
|
| 157 |
-
**
|
| 158 |
|
| 159 |
```python
|
| 160 |
class KV:
|
| 161 |
-
# Folder memory index —
|
| 162 |
folders = "mem:folders"
|
| 163 |
|
| 164 |
-
#
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
@staticmethod
|
| 167 |
def folder_obs(folder_path: str, agent_id: str) -> str:
|
| 168 |
safe_path = folder_path.replace("\\", "/").strip("/")
|
| 169 |
safe_agent = agent_id.strip()
|
| 170 |
return f"mem:folder:{safe_path}:{safe_agent}"
|
| 171 |
|
| 172 |
-
# Per-(folder, agent) metadata
|
| 173 |
@staticmethod
|
| 174 |
def folder_meta(folder_path: str, agent_id: str) -> str:
|
| 175 |
safe_path = folder_path.replace("\\", "/").strip("/")
|
|
@@ -179,16 +228,33 @@ class KV:
|
|
| 179 |
# Global long-term memories (unchanged)
|
| 180 |
memories = "mem:memories"
|
| 181 |
|
| 182 |
-
# BM25 index shards (
|
| 183 |
bm25Index = "mem:index:bm25"
|
| 184 |
|
| 185 |
-
# Audit
|
| 186 |
audit = "mem:audit"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
```
|
| 188 |
|
| 189 |
-
**
|
| 190 |
|
| 191 |
-
**Scopes
|
| 192 |
|
| 193 |
---
|
| 194 |
|
|
@@ -797,32 +863,62 @@ END
|
|
| 797 |
|
| 798 |
---
|
| 799 |
|
| 800 |
-
## MCP Tools:
|
|
|
|
|
|
|
| 801 |
|
| 802 |
-
###
|
| 803 |
|
| 804 |
-
| Tool |
|
| 805 |
-
|------|-------------|
|
| 806 |
-
| `agent_observe` | Required: `folderPath`, `agentId`, `text`.
|
| 807 |
-
| `
|
| 808 |
-
| `
|
| 809 |
-
| `
|
| 810 |
-
| `
|
| 811 |
-
| `memory_export` |
|
| 812 |
-
| `memory_forget` | Accepts `memoryId`
|
| 813 |
-
| `memory_diagnose` |
|
|
|
|
|
|
|
|
|
|
| 814 |
|
| 815 |
-
###
|
| 816 |
|
| 817 |
-
|
| 818 |
-
|------|-------------|
|
| 819 |
-
| `memory_folders` | List all (folder, agent) pairs with obsCount and lastUpdated |
|
| 820 |
-
| `memory_folder_observations` | Get observations for a specific `(folderPath, agentId)` pair |
|
| 821 |
-
| `memory_timeline` | Folder activity feed — observations sorted by time, filterable by folderPath/agentId |
|
| 822 |
|
| 823 |
-
|
| 824 |
|
| 825 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
|
| 827 |
---
|
| 828 |
|
|
|
|
| 10 |
|
| 11 |
## Architecture
|
| 12 |
|
| 13 |
+
### Implemented Module Layout
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
src/
|
| 17 |
+
├── app.py Thin Flask factory — create_app(), WebSocket, CORS
|
| 18 |
+
├── workers.py Background threads — index rebuild, auto-forget, SIGTERM handler
|
| 19 |
+
├── cli.py CLI entrypoint (agentmemory serve/migrate/export)
|
| 20 |
+
├── connect.py Client connection helper
|
| 21 |
+
├── db.py StateKV — SQLite WAL, per-thread connections, stats()
|
| 22 |
+
├── functions.py All canonical business logic (large; split planned)
|
| 23 |
+
├── search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
|
| 24 |
+
├── viewer_helpers.py Viewer HTML injection helper
|
| 25 |
+
│
|
| 26 |
+
├── routes/ Flask blueprints (A1 complete)
|
| 27 |
+
│ ├── observations.py /observe, /agent/observe, /folders, /folder/observations
|
| 28 |
+
│ ├── memories.py /remember, /agent/remember, /memories, /forget
|
| 29 |
+
│ ├── search.py /search, /timeline
|
| 30 |
+
│ ├── graph.py /graph, /graph/stats, /graph/query, /graph/build
|
| 31 |
+
│ ├── health.py /livez, /health, /audit, /config/flags
|
| 32 |
+
│ ├── mcp.py /mcp/tools GET+POST (12 active tools)
|
| 33 |
+
│ └── migration.py /migrate
|
| 34 |
+
│
|
| 35 |
+
├── memory/ Thin shim package re-exporting from functions.py (A2.1)
|
| 36 |
+
│ ├── observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
|
| 37 |
+
│ ├── remember.py remember, forget, jaccard_similarity
|
| 38 |
+
│ ├── context.py context, export_data, rebuild_index
|
| 39 |
+
│ ├── graph.py folder_graph_build
|
| 40 |
+
│ ├── timeline.py folder_timeline, folder_search
|
| 41 |
+
│ └── health.py health_check, auto_forget
|
| 42 |
+
│
|
| 43 |
+
├── storage/ KV scope registry + path utilities (A2.3)
|
| 44 |
+
│ ├── scopes.py KV class (copy; canonical in functions.py)
|
| 45 |
+
│ ├── paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
|
| 46 |
+
│ └── images.py save_image_to_disk, delete_image, touch_image
|
| 47 |
+
│
|
| 48 |
+
└── viewer/
|
| 49 |
+
└── index.html Single-file HTML dashboard (4 tabs: Folders/Memories/Graph/Timeline)
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
### High-Level System Diagram
|
| 53 |
|
| 54 |
```mermaid
|
|
|
|
| 59 |
A3[Cursor]
|
| 60 |
end
|
| 61 |
|
| 62 |
+
subgraph "MCP / REST API (src/routes/)"
|
| 63 |
T1[agent_observe]
|
| 64 |
T2[memory_recall]
|
| 65 |
T3[memory_save / agent_remember]
|
|
|
|
| 67 |
T5[memory_export]
|
| 68 |
T6[memory_forget]
|
| 69 |
T7[memory_diagnose]
|
| 70 |
+
T8[memory_folders]
|
| 71 |
+
T9[memory_folder_observations]
|
| 72 |
+
T10[memory_timeline]
|
| 73 |
end
|
| 74 |
|
| 75 |
+
subgraph "Business Logic (src/functions.py)"
|
| 76 |
F1[folder_observe]
|
| 77 |
F2[folder_search]
|
| 78 |
F3[remember / forget]
|
| 79 |
F4[folder_timeline]
|
| 80 |
F5[folder_graph_build]
|
| 81 |
+
F6[health_check / export_data]
|
| 82 |
F7[strip_private_data]
|
| 83 |
+
F8[IndexPersistence — debounced 5s]
|
| 84 |
end
|
| 85 |
|
| 86 |
+
subgraph "Storage (src/db.py — SQLite KV)"
|
| 87 |
S1["mem:folders (index)"]
|
| 88 |
+
S2["mem:folder:{path}:{agent} (observations)"]
|
| 89 |
+
S3["mem:foldermeta:{path}:{agent} (metadata)"]
|
| 90 |
+
S4[mem:memories]
|
| 91 |
+
S5["mem:index:bm25:* (sharded BM25)"]
|
| 92 |
+
S6["mem:obs_lookup (O(1) hydration)"]
|
| 93 |
+
S7[audit_log]
|
| 94 |
end
|
| 95 |
|
| 96 |
+
subgraph "Search (src/search.py)"
|
| 97 |
+
SR1[SearchIndex BM25+stemmer+synonyms]
|
| 98 |
+
SR2[VectorIndex cosine sim]
|
| 99 |
+
SR3[HybridSearch RRF k=60]
|
| 100 |
+
EP1[GeminiEmbeddingProvider 768d]
|
| 101 |
+
EP2[OpenAIEmbeddingProvider 1536d]
|
| 102 |
+
EP3[SentenceTransformerProvider variable]
|
| 103 |
end
|
| 104 |
|
| 105 |
+
subgraph "Viewer (src/viewer/index.html)"
|
| 106 |
V1[Folders tab]
|
| 107 |
V2[Memories tab]
|
| 108 |
V3[Graph tab]
|
| 109 |
V4[Timeline tab]
|
| 110 |
end
|
| 111 |
|
| 112 |
+
A1 & A2 & A3 --> T1 & T2 & T3 & T4 & T5 & T6 & T7 & T8 & T9 & T10
|
| 113 |
T1 --> F1
|
| 114 |
T2 & T4 --> F2
|
| 115 |
T3 --> F3
|
| 116 |
T5 --> F6
|
| 117 |
T6 --> F3
|
| 118 |
T7 --> F6
|
| 119 |
+
T8 & T9 --> S1 & S2
|
| 120 |
+
T10 --> F4
|
| 121 |
|
| 122 |
F1 --> F7
|
| 123 |
+
F1 --> S2 & S3 & S1 & S6
|
| 124 |
F1 --> SR1 & SR2
|
| 125 |
+
F3 --> S4
|
|
|
|
| 126 |
F2 --> SR3
|
| 127 |
SR1 & SR2 --> SR3
|
| 128 |
+
EP1 & EP2 & EP3 --> SR2
|
| 129 |
+
F8 --> S5
|
| 130 |
SR1 & SR2 --> F8
|
| 131 |
|
| 132 |
+
F4 --> S1 & S2
|
| 133 |
+
F5 --> S1 & S2 & S4
|
|
|
|
|
|
|
| 134 |
|
| 135 |
Viewer --> T2 & T4
|
| 136 |
F1 -.->|WebSocket broadcast| Viewer
|
|
|
|
| 197 |
|
| 198 |
## Components and Interfaces
|
| 199 |
|
| 200 |
+
### Component 1: KV Scope Registry (src/functions.py + src/storage/scopes.py — `KV` class)
|
| 201 |
|
| 202 |
+
**Purpose**: Single source of truth for all KV scope names. Defined in `src/functions.py` (canonical) and mirrored to `src/storage/scopes.py` (A2.3).
|
| 203 |
|
| 204 |
+
**Active scopes:**
|
| 205 |
|
| 206 |
```python
|
| 207 |
class KV:
|
| 208 |
+
# Folder memory index — key = "{folder_path}:{agent_id}", value = FolderIndexEntry
|
| 209 |
folders = "mem:folders"
|
| 210 |
|
| 211 |
+
# O(1) observation hydration index — key = obs_id, value = {folderPath, agentId}
|
| 212 |
+
obs_lookup = "mem:obs_lookup"
|
| 213 |
+
|
| 214 |
+
# Per-(folder, agent) observations — key = obs_id, value = FolderObservation
|
| 215 |
@staticmethod
|
| 216 |
def folder_obs(folder_path: str, agent_id: str) -> str:
|
| 217 |
safe_path = folder_path.replace("\\", "/").strip("/")
|
| 218 |
safe_agent = agent_id.strip()
|
| 219 |
return f"mem:folder:{safe_path}:{safe_agent}"
|
| 220 |
|
| 221 |
+
# Per-(folder, agent) metadata — key = "meta", value = FolderMeta
|
| 222 |
@staticmethod
|
| 223 |
def folder_meta(folder_path: str, agent_id: str) -> str:
|
| 224 |
safe_path = folder_path.replace("\\", "/").strip("/")
|
|
|
|
| 228 |
# Global long-term memories (unchanged)
|
| 229 |
memories = "mem:memories"
|
| 230 |
|
| 231 |
+
# BM25 index shards (sharded, 2MB chunks)
|
| 232 |
bm25Index = "mem:index:bm25"
|
| 233 |
|
| 234 |
+
# Audit log
|
| 235 |
audit = "mem:audit"
|
| 236 |
+
|
| 237 |
+
# Graph edges (repurposed for folder graph)
|
| 238 |
+
relations = "mem:relations"
|
| 239 |
+
|
| 240 |
+
# Legacy scopes (read-only — for migration only)
|
| 241 |
+
sessions = "mem:sessions"
|
| 242 |
+
|
| 243 |
+
@staticmethod
|
| 244 |
+
def observations(session_id: str) -> str:
|
| 245 |
+
return f"mem:obs:{session_id}"
|
| 246 |
+
|
| 247 |
+
# Legacy (retained for backward compat code paths)
|
| 248 |
+
summaries = "mem:summaries"
|
| 249 |
+
profiles = "mem:profiles"
|
| 250 |
+
slots = "mem:slots"
|
| 251 |
+
imageRefs = "mem:image-refs"
|
| 252 |
+
globalSlots = "mem:global-slots"
|
| 253 |
```
|
| 254 |
|
| 255 |
+
**Notable addition**: `obs_lookup = "mem:obs_lookup"` — an O(1) reverse-lookup index that maps `obs_id → {folderPath, agentId}`, enabling fast hydration during search without scanning all folder scopes. Backfilled on startup via `backfill_obs_lookup_if_needed()`.
|
| 256 |
|
| 257 |
+
**Scopes fully removed from active use**: `lessons`, `actions`, `action-edges`, `crystals`, `sketches`, `facets`, `sentinels`, `signals`, `checkpoints`, `routines`, `routine-runs`, `semantic`, `procedural`, `commits`, `leases`, `mesh`, `recent-searches`, `claude-bridge`, `insights`, `retention`, `access`
|
| 258 |
|
| 259 |
---
|
| 260 |
|
|
|
|
| 863 |
|
| 864 |
---
|
| 865 |
|
| 866 |
+
## MCP Tools: Implemented
|
| 867 |
+
|
| 868 |
+
The server exposes **12 MCP tools** via `GET /agentmemory/mcp/tools` and `POST /agentmemory/mcp/tools`.
|
| 869 |
|
| 870 |
+
### Active Tools
|
| 871 |
|
| 872 |
+
| Tool | Description | Notes |
|
| 873 |
+
|------|-------------|-------|
|
| 874 |
+
| `agent_observe` | Log observation to a `(folderPath, agentId)` pair | Required: `folderPath`, `agentId`, `text`. Legacy `sessionId`/`cwd`/`project` accepted as fallback. |
|
| 875 |
+
| `agent_remember` | Save to global long-term memory | Unchanged |
|
| 876 |
+
| `memory_recall` | BM25+vector search across folder obs + global memories | Optional `folderPath`/`agentId` filters |
|
| 877 |
+
| `memory_smart_search` | Alias for `memory_recall` (hybrid search) | Same implementation |
|
| 878 |
+
| `memory_save` | Explicit save to global memory | Unchanged |
|
| 879 |
+
| `memory_export` | Export all data as JSON v2 format | `{folders: [...], memories: [...], version: "2.0"}` |
|
| 880 |
+
| `memory_forget` | Delete memory or folder pair | Accepts `memoryId` OR `folderPath+agentId` |
|
| 881 |
+
| `memory_diagnose` | Health check — folder/agent/obs/memory counts | Replaces session-based counts |
|
| 882 |
+
| `memory_folders` | List all `(folder, agent)` pairs | Returns sorted by `lastUpdated` |
|
| 883 |
+
| `memory_folder_observations` | Get observations for a specific pair | Required: `folderPath`, `agentId` |
|
| 884 |
+
| `memory_timeline` | Folder activity feed | Optional `folderPath`, `agentId`, `limit`, `before`, `after` |
|
| 885 |
|
| 886 |
+
### Removed Tools (from original session-based model)
|
| 887 |
|
| 888 |
+
`memory_sessions`, `memory_sessions_list`, `memory_observations` (session-based), `memory_profile`, `memory_lessons`, `memory_lesson_save`, `memory_lesson_recall`, `memory_lesson_search`, `memory_consolidate`, `memory_reflect`, `memory_crystallize`, `memory_slot_list`, `memory_slot_get`, `memory_slot_create`, `memory_slot_append`, `memory_slot_replace`, `memory_slot_delete`, `memory_action_create`, `memory_action_update`, `memory_frontier`, `memory_antigravity_sync`, `memory_antigravity_sync_all`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
|
| 890 |
+
---
|
| 891 |
|
| 892 |
+
## REST Endpoints: Implemented
|
| 893 |
+
|
| 894 |
+
### Active Endpoints
|
| 895 |
+
|
| 896 |
+
| Method | Path | Blueprint | Description |
|
| 897 |
+
|--------|------|-----------|-------------|
|
| 898 |
+
| POST | `/agentmemory/observe` | observations | Legacy compat shim — maps to folder_observe |
|
| 899 |
+
| POST | `/agentmemory/agent/observe` | observations | Folder observation ingest (strict) |
|
| 900 |
+
| GET | `/agentmemory/folders` | observations | List all (folder, agent) pairs |
|
| 901 |
+
| GET | `/agentmemory/folder/observations` | observations | Observations for a pair |
|
| 902 |
+
| POST | `/agentmemory/session/start` | observations | Legacy compat no-op → returns synthetic session ID |
|
| 903 |
+
| POST | `/agentmemory/session/end` | observations | Legacy compat no-op |
|
| 904 |
+
| GET | `/agentmemory/observations` | observations | Legacy compat reads from `mem:obs:{sessionId}` |
|
| 905 |
+
| POST | `/agentmemory/remember` | memories | Save global memory |
|
| 906 |
+
| POST | `/agentmemory/agent/remember` | memories | Save global memory (agent-scoped variant) |
|
| 907 |
+
| GET | `/agentmemory/memories` | memories | List global memories |
|
| 908 |
+
| POST | `/agentmemory/forget` | memories | Delete memory / folder pair |
|
| 909 |
+
| POST | `/agentmemory/search` | search | BM25+vector hybrid search |
|
| 910 |
+
| POST | `/agentmemory/timeline` | search | Folder activity feed |
|
| 911 |
+
| GET | `/agentmemory/graph` | graph | Graph nodes+edges |
|
| 912 |
+
| GET | `/agentmemory/graph/stats` | graph | Node/edge count |
|
| 913 |
+
| POST | `/agentmemory/graph/query` | graph | Reserved (returns empty) |
|
| 914 |
+
| POST | `/agentmemory/graph/build` | graph | Trigger consolidation if enabled |
|
| 915 |
+
| GET | `/agentmemory/livez` | health | Liveness probe (no auth) |
|
| 916 |
+
| GET | `/agentmemory/health` | health | Full health check |
|
| 917 |
+
| GET | `/agentmemory/audit` | health | Audit log |
|
| 918 |
+
| GET | `/agentmemory/config/flags` | health | Feature flags + provider info |
|
| 919 |
+
| GET | `/agentmemory/mcp/tools` | mcp | MCP tool schemas |
|
| 920 |
+
| POST | `/agentmemory/mcp/tools` | mcp | MCP tool dispatch |
|
| 921 |
+
| POST | `/agentmemory/migrate` | migration | Session → folder migration |
|
| 922 |
|
| 923 |
---
|
| 924 |
|
.kiro/specs/folder-based-memory/tasks.md
CHANGED
|
@@ -2,15 +2,14 @@
|
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
-
The folder-based memory migration is complete.
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
assigned to different engineers independently.
|
| 10 |
|
| 11 |
-
**Track A — Backend Structure**
|
| 12 |
**Track B — Viewer UX** (Frontend / Full-Stack)
|
| 13 |
-
**Track C — Reliability & Testing**
|
| 14 |
**Track D — Integrations & DX** (DevEx / Infra)
|
| 15 |
|
| 16 |
---
|
|
@@ -19,333 +18,158 @@ assigned to different engineers independently.
|
|
| 19 |
|
| 20 |
## Track A — Backend Structure Refactor
|
| 21 |
|
| 22 |
-
> Owner: Senior Backend Dev
|
| 23 |
-
> Goal: Break the god files apart so the codebase scales to parallel contributions.
|
| 24 |
-
> Priority: CRITICAL — blocks all other backend work from being clean.
|
| 25 |
-
|
| 26 |
- [x] A1. Split `src/app.py` into Flask blueprints
|
| 27 |
-
- [x] A1.1 Create `src/routes/` directory with
|
| 28 |
-
-
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
-
|
| 32 |
-
-
|
| 33 |
-
-
|
| 34 |
-
-
|
| 35 |
-
- _Each blueprint file registers with `Blueprint(__name__)` and imports only what it needs_
|
| 36 |
|
| 37 |
- [x] A1.2 Rebuild `src/app.py` as a thin factory
|
| 38 |
-
- `create_app()` factory
|
| 39 |
-
-
|
| 40 |
-
- `
|
| 41 |
-
- Target: `app.py` under 150 lines after refactor
|
| 42 |
|
| 43 |
- [x] A1.3 Validate no route regressions after blueprint split
|
| 44 |
-
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
- [
|
| 48 |
-
|
| 49 |
-
- `src/memory/
|
| 50 |
-
- `src/memory/
|
| 51 |
-
- `src/memory/
|
| 52 |
-
- `src/memory/
|
| 53 |
-
- `src/memory/
|
| 54 |
-
- `
|
| 55 |
|
| 56 |
- [~] A2.2 Keep `src/functions.py` as a compatibility shim
|
| 57 |
-
-
|
| 58 |
-
- This avoids breaking `app.py` imports during the transition period
|
| 59 |
-
- Remove the shim in a follow-up PR once blueprints are fully referencing the new modules
|
| 60 |
|
| 61 |
-
- [
|
| 62 |
-
- `src/storage/scopes.py` — `KV` class
|
| 63 |
- `src/storage/paths.py` — `normalize_folder_path()`, `validate_agent_id()`, `generate_id()`, `fingerprint_id()`
|
| 64 |
- `src/storage/images.py` — `save_image_to_disk()`, `delete_image()`, `touch_image()`, `is_managed_image_path()`
|
| 65 |
|
| 66 |
-
- [
|
| 67 |
-
- [
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
- Keep the write `_lock` for serializing INSERT/UPDATE/DELETE operations
|
| 71 |
-
|
| 72 |
-
- [~] A3.2 Add WAL checkpoint on graceful shutdown
|
| 73 |
-
- Register a `SIGTERM` / `atexit` handler in `src/workers.py` that calls `PRAGMA wal_checkpoint(FULL)` before exit
|
| 74 |
-
- This ensures the BM25/vector index shards are flushed to the DB before the process dies
|
| 75 |
-
|
| 76 |
-
- [~] A3.3 Add `db.stats()` method for the health endpoint
|
| 77 |
-
- Return `{"db_size_bytes": ..., "kv_row_count": ..., "audit_row_count": ..., "wal_size_bytes": ...}`
|
| 78 |
-
- Used by the upgraded `/health` endpoint
|
| 79 |
-
|
| 80 |
-
- [ ] A4. Debounce index persistence writes
|
| 81 |
-
- [~] A4.1 Replace `IndexPersistence.schedule_save()` with a real debounce queue
|
| 82 |
-
- Use a `threading.Timer` that resets on each call; fires the actual `save()` after 5 seconds of inactivity
|
| 83 |
-
- This prevents a save on every single observation write under high throughput
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
- [~] A4.3 Write unit test for debounce behavior
|
| 90 |
-
- Assert that 100 rapid `add()` calls result in exactly 1 persistence save, not 100
|
| 91 |
|
| 92 |
---
|
| 93 |
|
| 94 |
## Track B — Viewer UX Overhaul
|
| 95 |
|
| 96 |
> Owner: Frontend / Full-Stack Dev
|
| 97 |
-
> Goal: Elevate the viewer from a debug tool to a real developer dashboard.
|
| 98 |
-
> Priority: HIGH — highest user-visible impact.
|
| 99 |
|
| 100 |
- [ ] B1. Add Command Palette (`Ctrl+K` / `Cmd+K`)
|
| 101 |
-
- [~] B1.1
|
| 102 |
-
- Fuzzy-search across: folder names, memory content snippets, observation titles, tab names
|
| 103 |
-
- Data sources: in-memory cache of last-loaded folders + memories (no extra API calls on keypress)
|
| 104 |
-
- Keyboard: `↑`/`↓` to navigate results, `Enter` to navigate to item, `Escape` to close
|
| 105 |
-
- Show result type badge (Folder / Memory / Observation) and secondary detail (agentId / timestamp)
|
| 106 |
-
|
| 107 |
-
- [~] B1.2 Register global keydown listener
|
| 108 |
-
- `document.addEventListener("keydown", ...)` — detect `Ctrl+K` on Windows/Linux, `Cmd+K` on Mac
|
| 109 |
-
- Focus input field on open; re-focus last active element on close
|
| 110 |
-
|
| 111 |
-
- [~] B1.3 Style command palette
|
| 112 |
-
- Modal overlay with `backdrop-filter: blur(4px)` behind the panel
|
| 113 |
-
- Max-width 640px, centered; input at top, results list below with keyboard-highlight state
|
| 114 |
-
- Respect `data-theme="dark"` CSS vars; animate open/close with `transform: translateY(-8px) → 0` + `opacity`
|
| 115 |
|
| 116 |
- [ ] B2. Replace Canvas graph with D3 force-directed SVG
|
| 117 |
-
- [~] B2.1
|
| 118 |
-
- Load `d3.v7.min.js` via `<script nonce="__AGENTMEMORY_VIEWER_NONCE__">` inline or from self-hosted path
|
| 119 |
-
- Remove the existing Canvas `<canvas>` element and hand-rolled physics loop
|
| 120 |
-
|
| 121 |
-
- [~] B2.2 Implement D3 force simulation on SVG
|
| 122 |
-
- Nodes as `<circle>` + `<text>` SVG groups; size proportional to `obsCount`
|
| 123 |
-
- Three link types rendered as distinct path styles: `same-parent` (dashed `stroke-dasharray: 6,3`), `cross-ref` (solid), `agent-shared` (dotted `stroke-dasharray: 2,4`)
|
| 124 |
-
- `d3.forceManyBody().strength()` tuned per node count: `-80` for < 20 nodes, `-200` for 20–100, `-400` for 100+
|
| 125 |
-
|
| 126 |
-
- [~] B2.3 Add zoom/pan and click-to-expand
|
| 127 |
-
- `d3.zoom()` for pan + scroll-to-zoom; double-click node to highlight its edges and neighbors, dim all others
|
| 128 |
-
- Click on a node opens the Folders tab filtered to that `folderPath`
|
| 129 |
-
|
| 130 |
-
- [~] B2.4 Fix graph edge label overlap bug (ROADMAP item)
|
| 131 |
-
- Use `d3.forceCollide()` on label bounding boxes; set `textAnchor` dynamically based on node position relative to center
|
| 132 |
-
- Labels only show at zoom > 0.6; below that, show labels only for hovered node
|
| 133 |
|
| 134 |
- [ ] B3. Implement virtual scroll on Timeline and Folders tabs
|
| 135 |
-
- [~] B3.1
|
| 136 |
-
- Takes `container`, `items[]`, `rowHeight`, `renderItem(item) → HTMLElement`
|
| 137 |
-
- Uses `IntersectionObserver` on a sentinel element at the bottom to load next page
|
| 138 |
-
- Maintains a window of ~50 rendered DOM nodes, recycling off-screen rows
|
| 139 |
-
|
| 140 |
-
- [~] B3.2 Apply `VirtualList` to Timeline tab
|
| 141 |
-
- Replace current `observations.forEach(renderObsCard)` with `VirtualList`
|
| 142 |
-
- Fetch first 50 observations; load next 50 on scroll near bottom via `/timeline` with `before` cursor from last item's timestamp
|
| 143 |
-
|
| 144 |
-
- [~] B3.3 Apply `VirtualList` to Folders tab observation drill-down
|
| 145 |
-
- When a folder row is expanded, load observations with the same virtual scroll pattern
|
| 146 |
|
| 147 |
- [ ] B4. First-run onboarding experience
|
| 148 |
-
- [~] B4.1
|
| 149 |
-
- Check `health_check` response: if `observationCount === 0` and `memoryCount === 0`, show onboarding card
|
| 150 |
-
|
| 151 |
-
- [~] B4.2 Build onboarding card component
|
| 152 |
-
- Three-step guide: (1) Configure `AGENTMEMORY_SECRET` env var, (2) Set `GEMINI_API_KEY` for semantic search, (3) Point your agent hook at `POST /agentmemory/agent/observe`
|
| 153 |
-
- Each step shows a code snippet (pre-formatted, with a copy button) and a green checkmark if the feature is already active (read from `/config/flags` response)
|
| 154 |
-
- Dismissable with "Got it" button; stores dismissed state in `localStorage`
|
| 155 |
-
|
| 156 |
-
- [~] B4.3 Add "copy to clipboard" utility function
|
| 157 |
-
- `copyToClipboard(text)` uses `navigator.clipboard.writeText()` with fallback to `document.execCommand("copy")`
|
| 158 |
-
- Show a transient "Copied!" tooltip for 1.5 seconds after click
|
| 159 |
|
| 160 |
- [ ] B5. Add MCP Tool Tester tab
|
| 161 |
-
- [~] B5.1
|
| 162 |
-
- Fetch `GET /agentmemory/mcp/tools` on tab activate; render a list of all tools with name + description
|
| 163 |
-
|
| 164 |
-
- [~] B5.2 Build tool invocation panel
|
| 165 |
-
- Click a tool → show its `inputSchema` as an auto-generated JSON editor (textarea pre-filled with `{}` containing all required keys)
|
| 166 |
-
- "Run" button posts to `POST /agentmemory/mcp/tools` with `{name, arguments}`
|
| 167 |
-
- Response rendered as syntax-highlighted JSON below the editor; error state shown in red
|
| 168 |
-
|
| 169 |
-
- [~] B5.3 Add request/response history for the session
|
| 170 |
-
- Keep last 10 tool calls in memory; render as collapsible history entries above the editor
|
| 171 |
-
- Each history entry shows tool name, timestamp, and success/error badge
|
| 172 |
|
| 173 |
- [ ] B6. Viewer polish and accessibility
|
| 174 |
-
- [~] B6.1
|
| 175 |
-
- Audit all components against `data-theme="dark"` CSS vars; find and fix the graph tooltip and graph controls that don't fully respond to theme changes
|
| 176 |
-
- Test in both themes: all text must meet WCAG AA contrast ratio (4.5:1 for body, 3:1 for large text)
|
| 177 |
-
|
| 178 |
-
- [~] B6.2 Add keyboard navigation for tabs
|
| 179 |
-
- `Tab` key cycles through tab buttons; `Enter`/`Space` activates; `←`/`→` arrow keys move between tabs when a tab has focus
|
| 180 |
-
- Add `role="tablist"`, `role="tab"`, `aria-selected`, `aria-controls` ARIA attributes to the tab bar
|
| 181 |
-
|
| 182 |
-
- [~] B6.3 Add loading skeleton states
|
| 183 |
-
- Replace `<div class="loading">Loading...</div>` with animated skeleton placeholder cards that match the shape of the real content
|
| 184 |
-
- Use CSS `@keyframes shimmer` with a `background: linear-gradient(90deg, ...)` animation
|
| 185 |
|
| 186 |
---
|
| 187 |
|
| 188 |
## Track C — Reliability & Testing
|
| 189 |
|
| 190 |
-
>
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
- [
|
| 195 |
-
- [
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
- [
|
| 218 |
-
- [
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
- **Property 7: Memory Version Uniqueness** — only one memory per concept cluster has `isLatest=True`
|
| 226 |
-
- **Property 8: Path Normalization Idempotency** — `normalize(normalize(p)) == normalize(p)` for all valid inputs
|
| 227 |
-
- Use `tmp_path` pytest fixture for isolated SQLite DB per test; `@settings(max_examples=50)`
|
| 228 |
-
|
| 229 |
-
- [ ] C3. Add integration tests for REST endpoints
|
| 230 |
-
- [~] C3.1 Create `tests/test_api.py` using Flask test client
|
| 231 |
-
- Test `POST /agentmemory/agent/observe` → 201 with obs id
|
| 232 |
-
- Test `POST /agentmemory/agent/observe` missing `folderPath` → 400
|
| 233 |
-
- Test `POST /agentmemory/search` with query → 200 with results list
|
| 234 |
-
- Test `GET /agentmemory/folders` → 200 with folders list
|
| 235 |
-
- Test `GET /agentmemory/health` → 200 with `folderCount`, `observationCount` keys
|
| 236 |
-
- Test `GET /agentmemory/livez` → 200 without auth (open endpoint)
|
| 237 |
-
- Test authenticated endpoints return 401 when `AGENTMEMORY_SECRET` is set but token is wrong
|
| 238 |
-
|
| 239 |
-
- [ ] C4. Improve HuggingFace sync reliability
|
| 240 |
- [~] C4.1 Replace mtime-based sync trigger with audit log high-water mark
|
| 241 |
-
|
| 242 |
-
- Only upload if `current_max_ts > last_synced_ts`; update the watermark after successful upload
|
| 243 |
-
- This eliminates false-positive uploads caused by WAL mode mtime changes
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
- Read from `.sync_state` file; if file doesn't exist, report `"never"`
|
| 248 |
-
|
| 249 |
-
- [ ] C5. Add graceful shutdown handler
|
| 250 |
-
- [~] C5.1 Register `SIGTERM` and `SIGINT` handlers in `src/workers.py`
|
| 251 |
-
- On signal: set a global `_shutting_down` flag; wait for any in-flight `schedule_save()` debounce timer to fire; call `persistence.save()` synchronously; log shutdown message
|
| 252 |
-
- Exit cleanly with code 0
|
| 253 |
|
| 254 |
---
|
| 255 |
|
| 256 |
## Track D — Integrations & Developer Experience
|
| 257 |
|
| 258 |
-
> Owner: DevEx / Infra Dev
|
| 259 |
-
> Goal: Make agentmemory easier to install, configure, and connect to agents.
|
| 260 |
-
> Priority: MEDIUM — high value but not blocking.
|
| 261 |
-
|
| 262 |
- [ ] D1. Clean up dead code and scratch files
|
| 263 |
-
- [~] D1.1
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
- The Antigravity sync logic (both functions) is personal/business-specific; move to `examples/antigravity_sync.py` and remove from the generic MCP bridge
|
| 275 |
-
- Clean up the double-import of `os`, `json`, `requests` inside `perform_antigravity_sync_local()`
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
| 280 |
|
| 281 |
-
- [
|
| 282 |
-
- [
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
- Add `chrome-extension://` support as an explicit opt-in via env var, not hardcoded
|
| 286 |
-
|
| 287 |
-
- [~] D2.2 Stop injecting `AGENTMEMORY_SECRET` into HTML template
|
| 288 |
-
- The current `__AGENTMEMORY_AUTO_TOKEN__` replacement embeds the raw secret in the page source
|
| 289 |
-
- Replace with a session-scoped viewer token: on page load, the server issues a short-lived viewer token (signed HMAC of `secret + timestamp`, valid 1h) via a `Set-Cookie` header
|
| 290 |
-
- The viewer uses this cookie for API calls; the actual `AGENTMEMORY_SECRET` never touches the DOM
|
| 291 |
-
|
| 292 |
-
- [ ] D3. Build prebuilt agent hook scripts
|
| 293 |
-
- [~] D3.1 Create `hooks/claude-code-hook.sh` for Claude Code
|
| 294 |
-
- Bash script that posts to `POST /agentmemory/agent/observe` on each tool use
|
| 295 |
-
- Reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from environment
|
| 296 |
-
- Include `folderPath` from `$PWD`, `agentId` from `$CLAUDE_AGENT_ID` or `"claude-code"`
|
| 297 |
-
|
| 298 |
-
- [~] D3.2 Create `hooks/cursor-hook.js` for Cursor
|
| 299 |
-
- JavaScript snippet for Cursor's `.cursorrules` hook system
|
| 300 |
-
- Same endpoint and payload structure as the bash version
|
| 301 |
-
|
| 302 |
-
- [~] D3.3 Create `hooks/powershell-hook.ps1` for terminal sessions
|
| 303 |
-
- Formalize and generalize the hook currently documented in `agentmemory.md`
|
| 304 |
-
- Parameterize `$ServerUrl` and `$AgentId`; ship as a ready-to-source snippet
|
| 305 |
-
|
| 306 |
-
- [~] D3.4 Update `INSTALL_FOR_AGENTS.md` with hook setup instructions
|
| 307 |
-
- One section per agent type; link to the corresponding hook script in `hooks/`
|
| 308 |
-
- Include the `.env` file format and `AGENTMEMORY_SECRET` setup steps
|
| 309 |
-
|
| 310 |
-
- [ ] D4. Build `pip`-installable package
|
| 311 |
-
- [~] D4.1 Add `pyproject.toml`
|
| 312 |
-
- Package name: `agentmemory`; entry point: `agentmemory = src.app:main`
|
| 313 |
-
- Include `src/viewer/` as package data
|
| 314 |
-
- Python `>=3.10` requirement
|
| 315 |
-
|
| 316 |
-
- [~] D4.2 Add CLI entrypoint
|
| 317 |
-
- `agentmemory serve [--port PORT] [--host HOST]` — starts the Flask server
|
| 318 |
-
- `agentmemory migrate` — runs `migrate_sessions_to_folders()` and prints summary
|
| 319 |
-
- `agentmemory export [--output FILE]` — runs `export_data()` and writes JSON
|
| 320 |
-
|
| 321 |
-
- [~] D4.3 Update GitHub Actions CI workflow
|
| 322 |
-
- Add steps: `pip install -e .[dev]`, `pytest tests/ -v`, `pip install build; python -m build`
|
| 323 |
-
- Run on `push` to `main` and `pull_request`
|
| 324 |
-
|
| 325 |
-
- [ ] D5. Additional embedding provider support
|
| 326 |
-
- [~] D5.1 Add `OpenAIEmbeddingProvider` class to `src/search.py`
|
| 327 |
-
- Mirror the `GeminiEmbeddingProvider` interface: `embed(text)`, `embed_batch(texts)`, `dimensions` (1536 for `text-embedding-3-small`)
|
| 328 |
-
- Use `urllib.request` (no new dependencies); read API key from `OPENAI_API_KEY` env var
|
| 329 |
-
|
| 330 |
-
- [~] D5.2 Add local `sentence-transformers` provider (optional install)
|
| 331 |
-
- `SentenceTransformerProvider` class; lazy-import `sentence_transformers`; log a clear error if not installed
|
| 332 |
-
- Default model: `all-MiniLM-L6-v2` (384 dims); configurable via `AGENTMEMORY_LOCAL_EMBEDDING_MODEL` env var
|
| 333 |
-
|
| 334 |
-
- [~] D5.3 Auto-select embedding provider in `init_app()`
|
| 335 |
-
- Priority: `GEMINI_API_KEY` → `OPENAI_API_KEY` → `AGENTMEMORY_LOCAL_EMBEDDING_MODEL` → BM25-only
|
| 336 |
-
- Log which provider was selected on startup
|
| 337 |
|
| 338 |
---
|
| 339 |
|
| 340 |
-
##
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
- The `D2.2` security fix (token injection) should be shipped before any public/shared deployment.
|
| 349 |
|
| 350 |
---
|
| 351 |
|
|
@@ -356,33 +180,28 @@ assigned to different engineers independently.
|
|
| 356 |
"waves": [
|
| 357 |
{
|
| 358 |
"id": 0,
|
| 359 |
-
"label": "
|
| 360 |
-
"tasks": ["
|
| 361 |
},
|
| 362 |
{
|
| 363 |
"id": 1,
|
| 364 |
-
"label": "
|
| 365 |
-
"tasks": ["
|
| 366 |
},
|
| 367 |
{
|
| 368 |
"id": 2,
|
| 369 |
-
"label": "
|
| 370 |
-
"tasks": ["
|
| 371 |
},
|
| 372 |
{
|
| 373 |
"id": 3,
|
| 374 |
-
"label": "
|
| 375 |
-
"tasks": ["
|
| 376 |
},
|
| 377 |
{
|
| 378 |
"id": 4,
|
| 379 |
-
"label": "Advanced features",
|
| 380 |
-
"tasks": ["B2.3", "B2.4", "B4.3", "B5.1", "B5.2", "C4.1", "C4.2", "C5.1", "D3.1", "D3.2", "D3.3", "D5.1"]
|
| 381 |
-
},
|
| 382 |
-
{
|
| 383 |
-
"id": 5,
|
| 384 |
"label": "Polish + packaging",
|
| 385 |
-
"tasks": ["
|
| 386 |
}
|
| 387 |
]
|
| 388 |
}
|
|
|
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
| 5 |
+
The folder-based memory migration is complete. The backend structural refactor (Track A)
|
| 6 |
+
is largely done — blueprints are split, `src/memory/` and `src/storage/` packages exist,
|
| 7 |
+
`db.py` has per-thread connections + WAL checkpoint + stats, and `IndexPersistence` has
|
| 8 |
+
debounce + dirty-flag tracking. This plan tracks what remains.
|
|
|
|
| 9 |
|
| 10 |
+
**Track A — Backend Structure** ✅ mostly complete
|
| 11 |
**Track B — Viewer UX** (Frontend / Full-Stack)
|
| 12 |
+
**Track C — Reliability & Testing** ✅ largely complete
|
| 13 |
**Track D — Integrations & DX** (DevEx / Infra)
|
| 14 |
|
| 15 |
---
|
|
|
|
| 18 |
|
| 19 |
## Track A — Backend Structure Refactor
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
- [x] A1. Split `src/app.py` into Flask blueprints
|
| 22 |
+
- [x] A1.1 Create `src/routes/` directory with blueprints
|
| 23 |
+
- `src/routes/observations.py` — `/observe`, `/agent/observe`, `/folder/observations`, `/folders`
|
| 24 |
+
- `src/routes/memories.py` — `/remember`, `/agent/remember`, `/memories`, `/forget`
|
| 25 |
+
- `src/routes/search.py` — `/search`, `/timeline`
|
| 26 |
+
- `src/routes/graph.py` — `/graph`, `/graph/stats`, `/graph/query`, `/graph/build`
|
| 27 |
+
- `src/routes/health.py` — `/livez`, `/health`, `/audit`, `/config/flags`
|
| 28 |
+
- `src/routes/mcp.py` — `/mcp/tools` GET + POST
|
| 29 |
+
- `src/routes/migration.py` — `/migrate`
|
|
|
|
| 30 |
|
| 31 |
- [x] A1.2 Rebuild `src/app.py` as a thin factory
|
| 32 |
+
- `create_app()` factory: init DB, init embeddings (D5.3 priority: Gemini → OpenAI → SentenceTransformer → BM25-only), register all blueprints, set up WebSocket, register CORS hook
|
| 33 |
+
- Background worker setup moved to `src/workers.py`
|
| 34 |
+
- `app.py` is now ~170 lines
|
|
|
|
| 35 |
|
| 36 |
- [x] A1.3 Validate no route regressions after blueprint split
|
| 37 |
+
- `tests/test_route_regressions.py` covers all main endpoints
|
| 38 |
+
|
| 39 |
+
- [x] A2. Split `src/functions.py` into focused modules
|
| 40 |
+
- [x] A2.1 Create `src/memory/` package
|
| 41 |
+
- `src/memory/observe.py` — thin re-export shim for `folder_observe`, `observe`, `build_synthetic_compression`, `strip_private_data`
|
| 42 |
+
- `src/memory/remember.py` — thin re-export shim for `remember`, `forget`, `jaccard_similarity`
|
| 43 |
+
- `src/memory/context.py` — thin re-export shim for `context`, `export_data`, `rebuild_index`
|
| 44 |
+
- `src/memory/graph.py` — thin re-export shim for `folder_graph_build`
|
| 45 |
+
- `src/memory/timeline.py` — thin re-export shim for `folder_timeline`, `folder_search`
|
| 46 |
+
- `src/memory/health.py` — thin re-export shim for `health_check`, `auto_forget`
|
| 47 |
+
- _Note: the shims currently delegate back to `functions.py`. The canonical implementation still lives in `functions.py`._
|
| 48 |
|
| 49 |
- [~] A2.2 Keep `src/functions.py` as a compatibility shim
|
| 50 |
+
- All routes import from `functions` directly; `src/memory/*` shims exist for future decoupling
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
- [x] A2.3 Move `KV` class and path utilities to `src/storage/`
|
| 53 |
+
- `src/storage/scopes.py` — `KV` class (copy; canonical version still in `functions.py`)
|
| 54 |
- `src/storage/paths.py` — `normalize_folder_path()`, `validate_agent_id()`, `generate_id()`, `fingerprint_id()`
|
| 55 |
- `src/storage/images.py` — `save_image_to_disk()`, `delete_image()`, `touch_image()`, `is_managed_image_path()`
|
| 56 |
|
| 57 |
+
- [x] A3. Fix `src/db.py` connection management
|
| 58 |
+
- [x] A3.1 Per-thread persistent connections via `threading.local()`
|
| 59 |
+
- [x] A3.2 WAL checkpoint on graceful shutdown (`atexit` + `SIGTERM`/`SIGINT` handlers in `workers.py`)
|
| 60 |
+
- [x] A3.3 `db.stats()` method returning `db_size_bytes`, `kv_row_count`, `audit_row_count`, `wal_size_bytes`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
- [x] A4. Debounce index persistence writes
|
| 63 |
+
- [x] A4.1 `IndexPersistence.schedule_save()` uses `threading.Timer` (5s debounce)
|
| 64 |
+
- [x] A4.2 `_dirty` flag on `SearchIndex` and `VectorIndex` — skip save if not dirty
|
| 65 |
+
- [~] A4.3 Unit test for debounce behavior → `tests/test_debounce.py` exists
|
|
|
|
|
|
|
| 66 |
|
| 67 |
---
|
| 68 |
|
| 69 |
## Track B — Viewer UX Overhaul
|
| 70 |
|
| 71 |
> Owner: Frontend / Full-Stack Dev
|
|
|
|
|
|
|
| 72 |
|
| 73 |
- [ ] B1. Add Command Palette (`Ctrl+K` / `Cmd+K`)
|
| 74 |
+
- [~] B1.1–B1.3 Not yet implemented in `src/viewer/index.html`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
- [ ] B2. Replace Canvas graph with D3 force-directed SVG
|
| 77 |
+
- [~] B2.1–B2.4 Not yet implemented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
- [ ] B3. Implement virtual scroll on Timeline and Folders tabs
|
| 80 |
+
- [~] B3.1–B3.3 Not yet implemented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
- [ ] B4. First-run onboarding experience
|
| 83 |
+
- [~] B4.1–B4.3 Not yet implemented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
- [ ] B5. Add MCP Tool Tester tab
|
| 86 |
+
- [~] B5.1–B5.3 Not yet implemented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
- [ ] B6. Viewer polish and accessibility
|
| 89 |
+
- [~] B6.1–B6.3 Not yet implemented
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
---
|
| 92 |
|
| 93 |
## Track C — Reliability & Testing
|
| 94 |
|
| 95 |
+
> All test files exist and are passing.
|
| 96 |
+
|
| 97 |
+
- [x] C1. Core pipeline tests
|
| 98 |
+
- [x] `tests/test_observe_core.py` — folder observe validation, privacy stripping, cap enforcement
|
| 99 |
+
- [x] `tests/test_remember.py` — memory creation, Jaccard dedup, independent memories
|
| 100 |
+
- [x] `tests/test_search.py` — BM25 add/search, prefix matching, synonym expansion, hybrid search
|
| 101 |
+
- [x] `tests/test_context.py` — TOKEN_BUDGET, context compilation
|
| 102 |
+
|
| 103 |
+
- [x] C2. Hypothesis property tests
|
| 104 |
+
- [x] `tests/test_properties.py` — 8 properties:
|
| 105 |
+
1. Pair Isolation
|
| 106 |
+
2. Observation Count Consistency
|
| 107 |
+
3. Index Coverage
|
| 108 |
+
4. Privacy Invariant
|
| 109 |
+
5. Timeline Ordering
|
| 110 |
+
6. Forget Completeness
|
| 111 |
+
7. Memory Version Uniqueness
|
| 112 |
+
8. Path Normalization Idempotency
|
| 113 |
+
|
| 114 |
+
- [x] C3. Integration tests
|
| 115 |
+
- [x] `tests/test_api.py` — Flask test client hitting all main endpoints
|
| 116 |
+
- [x] `tests/test_route_regressions.py` — regression suite after blueprint split
|
| 117 |
+
|
| 118 |
+
- [x] Additional test files
|
| 119 |
+
- [x] `tests/test_folder_observe.py` — folder_observe unit tests
|
| 120 |
+
- [x] `tests/test_folder_graph_build.py` — graph builder tests
|
| 121 |
+
- [x] `tests/test_forget.py` — forget function tests
|
| 122 |
+
- [x] `tests/test_normalize.py` — path normalization tests
|
| 123 |
+
- [x] `tests/test_obs_lookup.py` — obs_lookup index tests
|
| 124 |
+
- [x] `tests/test_migration.py` — session → folder migration tests
|
| 125 |
+
- [x] `tests/test_timeline.py` — folder_timeline tests
|
| 126 |
+
- [x] `tests/test_graph.py` — graph edge tests
|
| 127 |
+
- [x] `tests/test_debounce.py` — IndexPersistence debounce test
|
| 128 |
+
|
| 129 |
+
- [ ] C4. HuggingFace sync reliability
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
- [~] C4.1 Replace mtime-based sync trigger with audit log high-water mark
|
| 131 |
+
- [~] C4.2 Add sync status to `/health` endpoint
|
|
|
|
|
|
|
| 132 |
|
| 133 |
+
- [x] C5. Graceful shutdown handler
|
| 134 |
+
- [x] `src/workers.py` registers SIGTERM/SIGINT handlers, flushes persistence, runs WAL checkpoint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
---
|
| 137 |
|
| 138 |
## Track D — Integrations & Developer Experience
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
- [ ] D1. Clean up dead code and scratch files
|
| 141 |
+
- [~] D1.1 Remove scratch files (scratch_diff.txt, push_local_data.py, push_second_brain.py, migrate_dolt_to_sqlite.py)
|
| 142 |
+
- [~] D1.2 Remove 410-stub route handlers from app.py
|
| 143 |
+
- [~] D1.3 Fix dead references in `mcp_stdio.py`
|
| 144 |
+
- [~] D1.4 Move/rename `DESIGN.md` → `docs/viewer-design-system.md`; ensure `ARCHITECTURE.md` is accurate
|
| 145 |
|
| 146 |
+
- [ ] D2. Fix CORS and security hardening
|
| 147 |
+
- [x] D2.1 CORS configurable via `AGENTMEMORY_CORS_ORIGINS` env var (implemented in `app.py`)
|
| 148 |
+
- [~] D2.2 Stop injecting `AGENTMEMORY_SECRET` into HTML template (viewer token via cookie)
|
| 149 |
|
| 150 |
+
- [ ] D3. Prebuilt agent hook scripts
|
| 151 |
+
- [~] D3.1–D3.4 `hooks/` directory may contain scripts; need verification and docs update
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
- [x] D4. `pip`-installable package
|
| 154 |
+
- [x] `pyproject.toml` with `agentmemory` entry point → `src.cli:main`
|
| 155 |
+
- [x] `src/cli.py` CLI entrypoint exists
|
| 156 |
+
- [~] D4.3 GitHub Actions CI workflow updates
|
| 157 |
|
| 158 |
+
- [x] D5. Additional embedding providers
|
| 159 |
+
- [x] `OpenAIEmbeddingProvider` in `src/search.py` (D5.1)
|
| 160 |
+
- [x] `SentenceTransformerProvider` in `src/search.py` (D5.2)
|
| 161 |
+
- [x] Auto-select provider in `create_app()` — Gemini → OpenAI → SentenceTransformer → BM25-only (D5.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
---
|
| 164 |
|
| 165 |
+
## Remaining High-Priority Work
|
| 166 |
|
| 167 |
+
The following items are not yet done and represent the bulk of remaining effort:
|
| 168 |
+
|
| 169 |
+
1. **Track B (Viewer UX)** — Command palette, D3 graph, virtual scroll, onboarding, MCP tool tester, accessibility polish
|
| 170 |
+
2. **D1.2** — Remove 410 stub routes from `app.py` (they live in `observations.py` as compat shims — keep those, but the dead `app.py` stubs should be gone; already done since the monolith was split)
|
| 171 |
+
3. **D2.2** — Viewer token security (secret not in DOM)
|
| 172 |
+
4. **C4** — HF sync reliability with audit watermark
|
|
|
|
| 173 |
|
| 174 |
---
|
| 175 |
|
|
|
|
| 180 |
"waves": [
|
| 181 |
{
|
| 182 |
"id": 0,
|
| 183 |
+
"label": "Completed — structural foundation",
|
| 184 |
+
"tasks": ["A1.1", "A1.2", "A1.3", "A2.1", "A2.3", "A3.1", "A3.2", "A3.3", "A4.1", "A4.2", "C1.1", "C1.2", "C1.3", "C1.4", "C2.1", "C3.1", "C5.1", "D4.1", "D4.2", "D5.1", "D5.2", "D5.3", "D2.1"]
|
| 185 |
},
|
| 186 |
{
|
| 187 |
"id": 1,
|
| 188 |
+
"label": "Next — viewer UX foundation",
|
| 189 |
+
"tasks": ["B4.1", "B4.2", "B4.3", "B1.1", "B2.1", "B3.1"]
|
| 190 |
},
|
| 191 |
{
|
| 192 |
"id": 2,
|
| 193 |
+
"label": "Viewer components",
|
| 194 |
+
"tasks": ["B1.2", "B1.3", "B2.2", "B3.2", "B3.3", "B5.1", "B5.2", "D2.2"]
|
| 195 |
},
|
| 196 |
{
|
| 197 |
"id": 3,
|
| 198 |
+
"label": "Advanced viewer + sync",
|
| 199 |
+
"tasks": ["B2.3", "B2.4", "B5.3", "B6.1", "B6.2", "B6.3", "C4.1", "C4.2"]
|
| 200 |
},
|
| 201 |
{
|
| 202 |
"id": 4,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
"label": "Polish + packaging",
|
| 204 |
+
"tasks": ["D1.1", "D1.2", "D1.3", "D1.4", "D3.1", "D3.2", "D3.3", "D3.4", "D4.3"]
|
| 205 |
}
|
| 206 |
]
|
| 207 |
}
|
.windsurfrules
CHANGED
|
@@ -1,20 +1,23 @@
|
|
| 1 |
-
# Agent
|
| 2 |
|
| 3 |
-
This workspace is integrated with long-term semantic memory via `
|
| 4 |
-
You must act as your own memory manager by calling the
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
-
At the start of every session or new task, immediately call `
|
| 10 |
-
- Example: `
|
| 11 |
|
| 12 |
-
2. **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 14 |
-
- Call `
|
| 15 |
-
- Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
| 16 |
|
| 17 |
-
|
| 18 |
Before stating a task is complete:
|
| 19 |
-
- Reflect on whether any
|
| 20 |
-
-
|
|
|
|
| 1 |
+
# Agent Cache Rules
|
| 2 |
|
| 3 |
+
This workspace is integrated with long-term semantic memory via `agentcache-python`.
|
| 4 |
+
You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
|
| 5 |
|
| 6 |
## Rules & Workflow
|
| 7 |
|
| 8 |
1. **Initial Search (Prefetch Context)**:
|
| 9 |
+
At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, and past work.
|
| 10 |
+
- Example: `cache_smart_search(query="jwt token rotation logic")`
|
| 11 |
|
| 12 |
+
2. **Log Observations**:
|
| 13 |
+
As you work, call `agent_observe` to record what you are doing. Include `folderPath` (the working directory), `agentId` (your agent name), and `text` (what happened).
|
| 14 |
+
- Example: `agent_observe(folderPath="/your/project/path", agentId="windsurf", text="Refactored auth middleware to use JWT rotation")`
|
| 15 |
+
|
| 16 |
+
3. **Save Long-Term Memories**:
|
| 17 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 18 |
+
- Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
|
|
|
| 19 |
|
| 20 |
+
4. **Checklist Before Ending**:
|
| 21 |
Before stating a task is complete:
|
| 22 |
+
- Reflect on whether any insights should be saved via `cache_save`.
|
| 23 |
+
- Review the folder activity feed with `cache_timeline` to confirm all significant work was captured.
|
AGENTS.md
CHANGED
|
@@ -1,23 +1,53 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
## What this project is
|
| 4 |
|
| 5 |
-
A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no iii-engine, no Dolt. Agents use it to store observations, memories
|
| 6 |
|
| 7 |
## Project layout
|
| 8 |
|
| 9 |
```
|
| 10 |
src/
|
| 11 |
-
├── app.py
|
| 12 |
-
├──
|
| 13 |
-
├──
|
| 14 |
-
├──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
└── viewer/
|
| 16 |
-
└── index.html
|
|
|
|
| 17 |
sync.py HuggingFace dataset backup/restore
|
| 18 |
Dockerfile HF Space container definition
|
| 19 |
start.sh Boot: restore DB → start server → start sync loop
|
| 20 |
requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
|
|
|
|
|
|
|
| 21 |
```
|
| 22 |
|
| 23 |
## Running
|
|
@@ -29,130 +59,130 @@ python src/app.py
|
|
| 29 |
# Viewer at http://localhost:3111/viewer
|
| 30 |
```
|
| 31 |
|
| 32 |
-
No build step. No external database. SQLite file lives at `~/.
|
| 33 |
|
| 34 |
## Architecture
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
### Storage — `src/db.py`
|
| 37 |
|
| 38 |
-
`StateKV` wraps a single SQLite file with
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
-
|
| 41 |
-
- `audit_log(id, ts, agent_id, message)` — write audit trail (replaces Dolt versioning)
|
| 42 |
|
| 43 |
-
Key scopes (defined in `functions.py` `KV` class):
|
| 44 |
|
| 45 |
| Scope | Content |
|
| 46 |
|-------|---------|
|
| 47 |
-
| `mem:
|
| 48 |
-
| `mem:
|
| 49 |
-
| `mem:
|
| 50 |
-
| `mem:
|
| 51 |
-
| `mem:
|
|
|
|
|
|
|
| 52 |
| `mem:relations` | Knowledge graph edges |
|
| 53 |
-
| `mem:
|
|
|
|
| 54 |
|
| 55 |
### Business logic — `src/functions.py`
|
| 56 |
|
| 57 |
-
|
| 58 |
-
- `_bm25_index` / `_vector_index` — in-memory search indexes, rebuilt from DB on startup if empty
|
| 59 |
-
- `_hybrid_search` — combines BM25 + vector; only initialized when Gemini key is set
|
| 60 |
-
- `_stream_broadcaster` — WebSocket broadcast callback injected by `app.py`
|
| 61 |
|
| 62 |
**Observation pipeline:**
|
| 63 |
```
|
| 64 |
-
raw payload →
|
| 65 |
-
→
|
| 66 |
-
→
|
|
|
|
| 67 |
```
|
| 68 |
|
| 69 |
**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).
|
| 70 |
|
| 71 |
-
**
|
| 72 |
|
| 73 |
-
**
|
| 74 |
|
| 75 |
### Search — `src/search.py`
|
| 76 |
|
| 77 |
-
- `SearchIndex`: BM25 with
|
| 78 |
-
- `VectorIndex`: cosine similarity over
|
| 79 |
-
- `HybridSearch`: fuses BM25 + vector scores with RRF (k=60
|
| 80 |
|
| 81 |
-
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
## MCP Tools
|
| 95 |
|
| 96 |
-
The server exposes
|
| 97 |
|
| 98 |
| Tool | Description | Status |
|
| 99 |
|------|-------------|--------|
|
| 100 |
-
| `
|
| 101 |
-
| `
|
| 102 |
-
| `
|
| 103 |
-
| `
|
| 104 |
-
| `
|
| 105 |
-
| `
|
| 106 |
-
| `
|
| 107 |
-
| `
|
| 108 |
-
| `
|
| 109 |
-
| `
|
| 110 |
-
| `
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
| `memory_reflect` | Reflect on session, update context | Working |
|
| 114 |
-
| `memory_diagnose` | Health check subsystems | Working |
|
| 115 |
-
| `memory_forget` | Delete memory/session/observations | Working |
|
| 116 |
-
| `memory_export` | Export all data as JSON | Working |
|
| 117 |
-
| `agent_observe` | Log agent execution observation | Working |
|
| 118 |
-
| `agent_remember` | Save agent memory to long-term | Working |
|
| 119 |
-
| `memory_antigravity_sync` | Sync Antigravity transcripts | Working |
|
| 120 |
-
| `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect | Working |
|
| 121 |
-
| `memory_slot_list` | List all pinned memory slots | Working |
|
| 122 |
-
| `memory_slot_get` | Retrieve a specific pinned slot | Working |
|
| 123 |
-
| `memory_slot_create` | Create/overwrite pinned slot | Working |
|
| 124 |
-
| `memory_slot_append` | Append text content to slot | Working |
|
| 125 |
-
| `memory_slot_replace` | Replace slot content | Working |
|
| 126 |
-
| `memory_slot_delete` | Delete pinned memory slot | Working |
|
| 127 |
-
| `memory_action_create` | Create a new work item / action | Working |
|
| 128 |
-
| `memory_action_update` | Update fields of existing action | Working |
|
| 129 |
-
| `memory_frontier` | Get active/pending actions | Working |
|
| 130 |
-
| `memory_crystallize` | Summarize session observations | Working |
|
| 131 |
-
|
| 132 |
-
**MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from environment variables dynamically.
|
| 133 |
|
| 134 |
## Consistency rules
|
| 135 |
|
| 136 |
**When adding a REST endpoint:**
|
| 137 |
-
1. Add the route in `src/
|
| 138 |
-
2. Update `API Reference` section in `README.md`
|
| 139 |
-
3. Add the MCP tool in `src/
|
| 140 |
|
| 141 |
**When adding an MCP tool:**
|
| 142 |
-
1. Add the schema to the `GET /mcp/tools` response in `src/
|
| 143 |
-
2. Add the handler case to the `POST /mcp/tools` dispatch in `src/
|
| 144 |
3. Update the tool table in `README.md`
|
| 145 |
4. Update `AGENTS.md` tool list
|
| 146 |
|
| 147 |
**When changing data scopes:**
|
| 148 |
-
1. Update the `KV` class in `src/functions.py`
|
| 149 |
-
2.
|
|
|
|
| 150 |
|
| 151 |
## Code patterns
|
| 152 |
|
| 153 |
### Adding a new KV scope
|
| 154 |
|
| 155 |
```python
|
|
|
|
| 156 |
class KV:
|
| 157 |
your_scope = "mem:your-scope"
|
| 158 |
|
|
@@ -164,21 +194,21 @@ class KV:
|
|
| 164 |
### Adding a REST endpoint
|
| 165 |
|
| 166 |
```python
|
| 167 |
-
|
|
|
|
| 168 |
def your_endpoint():
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
body = request.get_json(silent=True) or {}
|
| 174 |
# validate fields explicitly — never pass raw body to functions
|
| 175 |
-
result = your_function(
|
| 176 |
return jsonify(result), 200
|
| 177 |
```
|
| 178 |
|
| 179 |
### Adding an MCP tool schema
|
| 180 |
|
| 181 |
-
In
|
| 182 |
```python
|
| 183 |
{
|
| 184 |
"name": "memory_your_tool",
|
|
@@ -193,11 +223,11 @@ In the `GET /mcp/tools` handler, add to the tools list:
|
|
| 193 |
}
|
| 194 |
```
|
| 195 |
|
| 196 |
-
In
|
| 197 |
```python
|
| 198 |
elif tool_name == 'memory_your_tool':
|
| 199 |
query = args.get('query', '')
|
| 200 |
-
result = your_function(kv, query)
|
| 201 |
return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
|
| 202 |
```
|
| 203 |
|
|
@@ -206,26 +236,44 @@ elif tool_name == 'memory_your_tool':
|
|
| 206 |
| Variable | Default | Purpose |
|
| 207 |
|----------|---------|---------|
|
| 208 |
| `III_REST_PORT` / `PORT` | `3111` | Server port |
|
| 209 |
-
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables vector search
|
| 210 |
-
| `
|
|
|
|
|
|
|
| 211 |
| `AGENT_ID` | — | Default agent ID |
|
| 212 |
-
| `
|
| 213 |
-
| `
|
|
|
|
| 214 |
| `TOKEN_BUDGET` | `2000` | Context compilation cap |
|
| 215 |
| `GRAPH_EXTRACTION_ENABLED` | `false` | Graph extraction (needs LLM) |
|
| 216 |
| `CONSOLIDATION_ENABLED` | `false` | Consolidation (needs LLM) |
|
| 217 |
-
| `
|
|
|
|
|
|
|
|
|
|
| 218 |
| `HF_TOKEN` | — | HuggingFace sync |
|
| 219 |
-
| `
|
| 220 |
|
| 221 |
-
##
|
| 222 |
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
|
| 227 |
`start.sh` sequence:
|
| 228 |
-
1. Restore `
|
| 229 |
2. Start `python src/app.py` in background
|
| 230 |
3. Run `sync.py` in a loop (backup every ~60s if changed)
|
| 231 |
|
|
@@ -233,10 +281,9 @@ Data flow: `agentmemory.db` (SQLite) ↔ `sync.py` ↔ HF dataset repo.
|
|
| 233 |
|
| 234 |
Single-file HTML dashboard, served directly by Flask at `/viewer`. No build step, no bundler.
|
| 235 |
|
| 236 |
-
Tabs:
|
| 237 |
-
|
| 238 |
-
**Graph tab** (`loadGraph()`): fetches sessions + memories, groups by `project` path into folder nodes. Edges connect folders that share concepts or parent path segments. Each folder node gets a unique color via `folderColor(id)` — a hash-to-hex function that converts the folder path string into a distinct HSL color. The simulation uses force-directed physics with per-node-count repulsion tuning.
|
| 239 |
-
|
| 240 |
-
## No tests yet
|
| 241 |
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agentcache-python — Agent Instructions
|
| 2 |
|
| 3 |
## What this project is
|
| 4 |
|
| 5 |
+
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.
|
| 6 |
|
| 7 |
## Project layout
|
| 8 |
|
| 9 |
```
|
| 10 |
src/
|
| 11 |
+
├── app.py Thin Flask factory — create_app(), WebSocket, CORS hook
|
| 12 |
+
├── cli.py CLI entrypoint (agentcache serve/migrate/export)
|
| 13 |
+
├── connect.py Client connection helper
|
| 14 |
+
├── db.py StateKV — SQLite WAL, per-thread connections, stats()
|
| 15 |
+
├── functions.py All canonical business logic (large; memory/ shims delegate here)
|
| 16 |
+
├── search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
|
| 17 |
+
├── viewer_helpers.py Viewer HTML injection helper
|
| 18 |
+
├── workers.py Background threads — index rebuild, auto-forget, SIGTERM handler
|
| 19 |
+
│
|
| 20 |
+
├── routes/ Flask blueprints
|
| 21 |
+
│ ├── observations.py /observe, /agent/observe, /folders, /folder/observations
|
| 22 |
+
│ ├── memories.py /remember, /agent/remember, /memories, /forget
|
| 23 |
+
│ ├── search.py /search, /timeline
|
| 24 |
+
│ ├── graph.py /graph, /graph/stats, /graph/query, /graph/build
|
| 25 |
+
│ ├── health.py /livez, /health, /audit, /config/flags
|
| 26 |
+
│ ├── mcp.py /mcp/tools GET+POST (12 active tools)
|
| 27 |
+
│ └── migration.py /migrate
|
| 28 |
+
│
|
| 29 |
+
├── memory/ Thin shim package — delegates to functions.py
|
| 30 |
+
│ ├── observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
|
| 31 |
+
│ ├── remember.py remember, forget, jaccard_similarity
|
| 32 |
+
│ ├── context.py context, export_data, rebuild_index
|
| 33 |
+
│ ├── graph.py folder_graph_build
|
| 34 |
+
│ ├── timeline.py folder_timeline, folder_search
|
| 35 |
+
│ └── health.py health_check, auto_forget
|
| 36 |
+
│
|
| 37 |
+
├── storage/ KV scope registry + path/ID utilities
|
| 38 |
+
│ ├── scopes.py KV class (mirrored from functions.py)
|
| 39 |
+
│ ├── paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
|
| 40 |
+
│ └── images.py save_image_to_disk, delete_image, touch_image
|
| 41 |
+
│
|
| 42 |
└── viewer/
|
| 43 |
+
└── index.html Single-file HTML dashboard (4 tabs: Folders / Memories / Graph / Timeline)
|
| 44 |
+
|
| 45 |
sync.py HuggingFace dataset backup/restore
|
| 46 |
Dockerfile HF Space container definition
|
| 47 |
start.sh Boot: restore DB → start server → start sync loop
|
| 48 |
requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
|
| 49 |
+
pyproject.toml pip-installable package (agentcache==0.9.8, Python ≥3.10)
|
| 50 |
+
tests/ pytest suite — unit, integration, and Hypothesis property tests
|
| 51 |
```
|
| 52 |
|
| 53 |
## Running
|
|
|
|
| 59 |
# Viewer at http://localhost:3111/viewer
|
| 60 |
```
|
| 61 |
|
| 62 |
+
No build step. No external database. SQLite file lives at `~/.agentcache/agentcache.db`.
|
| 63 |
|
| 64 |
## Architecture
|
| 65 |
|
| 66 |
+
### Data Model — Folder-Based Memory
|
| 67 |
+
|
| 68 |
+
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.
|
| 69 |
+
|
| 70 |
### Storage — `src/db.py`
|
| 71 |
|
| 72 |
+
`StateKV` wraps a single SQLite file with:
|
| 73 |
+
- `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — all data as JSON
|
| 74 |
+
- `audit_log(id, ts, agent_id, message)` — write audit trail
|
| 75 |
+
- `sync_state_metadata(key, value)` — HuggingFace sync watermark
|
| 76 |
|
| 77 |
+
Per-thread persistent connections via `threading.local()`. WAL checkpoint registered via `atexit` and on `SIGTERM`/`SIGINT`.
|
|
|
|
| 78 |
|
| 79 |
+
Key scopes (defined in `functions.py` `KV` class and mirrored in `src/storage/scopes.py`):
|
| 80 |
|
| 81 |
| Scope | Content |
|
| 82 |
|-------|---------|
|
| 83 |
+
| `mem:folders` | Index of all `(folder_path, agent_id)` pairs — key = `"{path}:{agent}"` |
|
| 84 |
+
| `mem:folder:{path}:{agent}` | Observations for a pair — key = obs_id |
|
| 85 |
+
| `mem:foldermeta:{path}:{agent}` | Metadata for a pair (obsCount, lastUpdated, summary) |
|
| 86 |
+
| `mem:obs_lookup` | O(1) reverse-lookup: obs_id → `{folderPath, agentId}` |
|
| 87 |
+
| `mem:memories` | Global long-term memories |
|
| 88 |
+
| `mem:index:bm25:*` | Sharded BM25 index (2MB chunks) |
|
| 89 |
+
| `mem:audit` | Audit log entries |
|
| 90 |
| `mem:relations` | Knowledge graph edges |
|
| 91 |
+
| `mem:sessions` | Legacy session store (read-only, migration only) |
|
| 92 |
+
| `mem:obs:{session_id}` | Legacy per-session observations (read-only, migration only) |
|
| 93 |
|
| 94 |
### Business logic — `src/functions.py`
|
| 95 |
|
| 96 |
+
All canonical implementations live here. `src/memory/*` are thin shims that re-export from this module (for future decoupling).
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
**Observation pipeline:**
|
| 99 |
```
|
| 100 |
+
raw payload → normalize_folder_path() → validate_agent_id() → strip_private_data()
|
| 101 |
+
→ build obs dict → kv.set(folder_obs scope) → upsert folder_meta + folders index
|
| 102 |
+
→ kv.set(obs_lookup) → BM25-indexed → vector-indexed (if key set)
|
| 103 |
+
→ schedule_save() (debounced 5s) → audit log → WebSocket broadcast
|
| 104 |
```
|
| 105 |
|
| 106 |
**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).
|
| 107 |
|
| 108 |
+
**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.
|
| 109 |
|
| 110 |
+
**`health_check()`** returns: `folderCount`, `agentCount`, `pairCount`, `observationCount`, `memoryCount`, `bm25IndexSize`, `vectorIndexSize`, `dbPath`, plus `db.stats()` fields.
|
| 111 |
|
| 112 |
### Search — `src/search.py`
|
| 113 |
|
| 114 |
+
- `SearchIndex`: BM25 with Porter stemmer and synonym expansion. Dirty-flag (`_dirty`) prevents unnecessary saves. Persisted in sharded 2MB KV chunks.
|
| 115 |
+
- `VectorIndex`: cosine similarity over embeddings stored as base64-encoded float32 arrays. Also has `_dirty` flag.
|
| 116 |
+
- `HybridSearch`: fuses BM25 + vector scores with RRF (k=60).
|
| 117 |
|
| 118 |
+
**Embedding providers** (auto-selected by priority in `create_app()`):
|
| 119 |
|
| 120 |
+
| Priority | Provider | Env var | Dimensions |
|
| 121 |
+
|----------|----------|---------|------------|
|
| 122 |
+
| 1 | `GeminiEmbeddingProvider` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | 768 |
|
| 123 |
+
| 2 | `OpenAIEmbeddingProvider` | `OPENAI_API_KEY` | 1536 |
|
| 124 |
+
| 3 | `SentenceTransformerProvider` | `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | variable |
|
| 125 |
+
| 4 | BM25-only | — | — |
|
| 126 |
|
| 127 |
+
### Server — `src/app.py`
|
| 128 |
|
| 129 |
+
Boot order:
|
| 130 |
+
1. Load `~/.agentcache/.env` if present
|
| 131 |
+
2. Initialize `StateKV` (SQLite)
|
| 132 |
+
3. Auto-select embedding provider (Gemini → OpenAI → SentenceTransformer → BM25-only)
|
| 133 |
+
4. Initialize `IndexPersistence` (load or rebuild)
|
| 134 |
+
5. Backfill `obs_lookup` index if missing
|
| 135 |
+
6. Create Flask app, register blueprints
|
| 136 |
+
7. Set up WebSocket `/stream/mem-live/viewer`
|
| 137 |
+
8. Register CORS `after_request` hook
|
| 138 |
+
9. Start background workers (index rebuild if empty/stale, auto-forget loop)
|
| 139 |
+
|
| 140 |
+
Auth: all endpoints check `AGENTCACHE_SECRET` via `hmac.compare_digest`. `/livez` is always open.
|
| 141 |
|
| 142 |
## MCP Tools
|
| 143 |
|
| 144 |
+
The server exposes **12 MCP tools** via `GET /agentcache/mcp/tools` and `POST /agentcache/mcp/tools`.
|
| 145 |
|
| 146 |
| Tool | Description | Status |
|
| 147 |
|------|-------------|--------|
|
| 148 |
+
| `agent_observe` | Log observation to a `(folderPath, agentId)` pair | Working |
|
| 149 |
+
| `agent_remember` | Save to global long-term memory | Working |
|
| 150 |
+
| `memory_recall` | Search folder obs + global memories (BM25+vector) | Working |
|
| 151 |
+
| `memory_smart_search` | Hybrid semantic+keyword search (alias for recall) | Working |
|
| 152 |
+
| `memory_save` | Explicitly save insight to long-term memory | Working |
|
| 153 |
+
| `memory_export` | Export all data as JSON (v2 format) | Working |
|
| 154 |
+
| `memory_forget` | Delete memory or folder pair observations | Working |
|
| 155 |
+
| `memory_diagnose` | Health check (folder/agent/obs/memory counts) | Working |
|
| 156 |
+
| `memory_folders` | List all `(folder, agent)` pairs | Working |
|
| 157 |
+
| `memory_folder_observations` | Get observations for a specific pair | Working |
|
| 158 |
+
| `memory_timeline` | Folder activity feed (sorted by time, filterable) | Working |
|
| 159 |
+
|
| 160 |
+
**MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTCACHE_URL` and `AGENTCACHE_SECRET` from environment variables dynamically.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
## Consistency rules
|
| 163 |
|
| 164 |
**When adding a REST endpoint:**
|
| 165 |
+
1. Add the route in the appropriate `src/routes/*.py` blueprint
|
| 166 |
+
2. Update the `API Reference` section in `README.md`
|
| 167 |
+
3. Add the MCP tool in `src/routes/mcp.py` if it should be agent-callable
|
| 168 |
|
| 169 |
**When adding an MCP tool:**
|
| 170 |
+
1. Add the schema to the `GET /mcp/tools` response in `src/routes/mcp.py`
|
| 171 |
+
2. Add the handler case to the `POST /mcp/tools` dispatch in `src/routes/mcp.py`
|
| 172 |
3. Update the tool table in `README.md`
|
| 173 |
4. Update `AGENTS.md` tool list
|
| 174 |
|
| 175 |
**When changing data scopes:**
|
| 176 |
+
1. Update the `KV` class in `src/functions.py` (canonical)
|
| 177 |
+
2. Mirror the change in `src/storage/scopes.py`
|
| 178 |
+
3. Update the scope table in this file
|
| 179 |
|
| 180 |
## Code patterns
|
| 181 |
|
| 182 |
### Adding a new KV scope
|
| 183 |
|
| 184 |
```python
|
| 185 |
+
# In src/functions.py KV class (canonical):
|
| 186 |
class KV:
|
| 187 |
your_scope = "mem:your-scope"
|
| 188 |
|
|
|
|
| 194 |
### Adding a REST endpoint
|
| 195 |
|
| 196 |
```python
|
| 197 |
+
# In the appropriate src/routes/*.py blueprint:
|
| 198 |
+
@your_bp.route('/agentcache/your-path', methods=['POST'])
|
| 199 |
def your_endpoint():
|
| 200 |
+
auth_err = _check_auth()
|
| 201 |
+
if auth_err:
|
| 202 |
+
return auth_err
|
| 203 |
+
body = request.get_json(force=True) or {}
|
|
|
|
| 204 |
# validate fields explicitly — never pass raw body to functions
|
| 205 |
+
result = functions.your_function(_get_kv(), body.get('field'))
|
| 206 |
return jsonify(result), 200
|
| 207 |
```
|
| 208 |
|
| 209 |
### Adding an MCP tool schema
|
| 210 |
|
| 211 |
+
In `src/routes/mcp.py`, `GET /agentcache/mcp/tools` handler:
|
| 212 |
```python
|
| 213 |
{
|
| 214 |
"name": "memory_your_tool",
|
|
|
|
| 223 |
}
|
| 224 |
```
|
| 225 |
|
| 226 |
+
In `src/routes/mcp.py`, `POST /agentcache/mcp/tools` handler:
|
| 227 |
```python
|
| 228 |
elif tool_name == 'memory_your_tool':
|
| 229 |
query = args.get('query', '')
|
| 230 |
+
result = functions.your_function(kv, query)
|
| 231 |
return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
|
| 232 |
```
|
| 233 |
|
|
|
|
| 236 |
| Variable | Default | Purpose |
|
| 237 |
|----------|---------|---------|
|
| 238 |
| `III_REST_PORT` / `PORT` | `3111` | Server port |
|
| 239 |
+
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables Gemini vector search (priority 1) |
|
| 240 |
+
| `OPENAI_API_KEY` | — | Enables OpenAI vector search (priority 2) |
|
| 241 |
+
| `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | — | SentenceTransformer model name (priority 3) |
|
| 242 |
+
| `AGENTCACHE_SECRET` | — | Bearer token auth |
|
| 243 |
| `AGENT_ID` | — | Default agent ID |
|
| 244 |
+
| `AGENTCACHE_AGENT_SCOPE=isolated` | — | Filter data to current agent |
|
| 245 |
+
| `AGENTCACHE_CWD` | — | Fallback folder path for legacy clients |
|
| 246 |
+
| `MAX_OBS_PER_FOLDER` | `2000` | Observations hard cap per (folder, agent) pair |
|
| 247 |
| `TOKEN_BUDGET` | `2000` | Context compilation cap |
|
| 248 |
| `GRAPH_EXTRACTION_ENABLED` | `false` | Graph extraction (needs LLM) |
|
| 249 |
| `CONSOLIDATION_ENABLED` | `false` | Consolidation (needs LLM) |
|
| 250 |
+
| `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM compression |
|
| 251 |
+
| `AUTO_FORGET_ENABLED` | — | Auto-forget sweep (set to "false" to disable) |
|
| 252 |
+
| `AGENTCACHE_CORS_ORIGINS` | see app.py | Comma-separated allowed origins |
|
| 253 |
+
| `AGENTCACHE_IMAGE_STORE_MAX_BYTES` | 500MB | Image store byte limit |
|
| 254 |
| `HF_TOKEN` | — | HuggingFace sync |
|
| 255 |
+
| `AGENTCACHE_DATASET_REPO` | — | HF dataset repo for backup |
|
| 256 |
|
| 257 |
+
## Testing
|
| 258 |
|
| 259 |
+
```bash
|
| 260 |
+
pip install -e ".[dev]"
|
| 261 |
+
pytest tests/ -v
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
Tests live in `tests/` — 17 test files covering unit tests, integration tests (Flask test client), and Hypothesis property tests.
|
| 265 |
+
|
| 266 |
+
Key test files:
|
| 267 |
+
- `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)
|
| 268 |
+
- `tests/test_api.py` — Flask test client integration tests
|
| 269 |
+
- `tests/test_route_regressions.py` — regression suite after blueprint split
|
| 270 |
|
| 271 |
+
## HuggingFace Space deployment
|
| 272 |
+
|
| 273 |
+
Data flow: `agentcache.db` (SQLite) ↔ `sync.py` ↔ HF dataset repo.
|
| 274 |
|
| 275 |
`start.sh` sequence:
|
| 276 |
+
1. Restore `agentcache.db` from dataset repo
|
| 277 |
2. Start `python src/app.py` in background
|
| 278 |
3. Run `sync.py` in a loop (backup every ~60s if changed)
|
| 279 |
|
|
|
|
| 281 |
|
| 282 |
Single-file HTML dashboard, served directly by Flask at `/viewer`. No build step, no bundler.
|
| 283 |
|
| 284 |
+
Tabs: **Folders**, **Memories**, **Graph**, **Timeline**.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
+
- **Folders tab**: lists all `(folder, agent)` pairs; click a row to drill into observations
|
| 287 |
+
- **Memories tab**: global long-term memories with search
|
| 288 |
+
- **Graph tab**: force-directed graph — nodes = folder paths, edges = same-parent / cross-ref / agent-shared
|
| 289 |
+
- **Timeline tab**: all observations sorted by timestamp desc, filterable by folder path and agent ID
|
ARCHITECTURE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
|
@@ -44,8 +44,8 @@ src/
|
|
| 44 |
├── viewer_helpers.py make_viewer_response() — reads viewer/index.html,
|
| 45 |
│ injects nonce + version, sets CSP headers.
|
| 46 |
│
|
| 47 |
-
├── mcp_stdio.py stdio MCP bridge: reads
|
| 48 |
-
│
|
| 49 |
│
|
| 50 |
└── viewer/
|
| 51 |
└── index.html Single-file HTML dashboard (no bundler).
|
|
@@ -55,7 +55,7 @@ src/
|
|
| 55 |
|
| 56 |
## KV Scope Layout
|
| 57 |
|
| 58 |
-
All data lives in a single SQLite file (`~/.
|
| 59 |
|
| 60 |
- `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — JSON values
|
| 61 |
- `audit_log(id, ts, agent_id, message)` — write audit trail
|
|
@@ -120,7 +120,7 @@ If found, old memory is marked `isLatest=False` and new memory sets `parentId`.
|
|
| 120 |
|
| 121 |
## Authentication
|
| 122 |
|
| 123 |
-
All endpoints except `/livez` check `
|
| 124 |
`hmac.compare_digest` Bearer token comparison. No secret → no auth check.
|
| 125 |
|
| 126 |
---
|
|
@@ -138,7 +138,7 @@ Priority order (auto-selected at startup):
|
|
| 138 |
|
| 139 |
1. `GeminiEmbeddingProvider` — if `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set (768 dims)
|
| 140 |
2. `OpenAIEmbeddingProvider` — if `OPENAI_API_KEY` is set (1536 dims)
|
| 141 |
-
3. `SentenceTransformerProvider` — if `
|
| 142 |
4. BM25-only fallback
|
| 143 |
|
| 144 |
Without an embedding provider, `HybridSearch` falls back to pure BM25.
|
|
|
|
| 1 |
+
# agentcache-python — Architecture
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
|
|
|
|
| 44 |
├── viewer_helpers.py make_viewer_response() — reads viewer/index.html,
|
| 45 |
│ injects nonce + version, sets CSP headers.
|
| 46 |
│
|
| 47 |
+
├── mcp_stdio.py stdio MCP bridge: reads AGENTCACHE_URL and
|
| 48 |
+
│ AGENTCACHE_SECRET, proxies tool calls to the HTTP API.
|
| 49 |
│
|
| 50 |
└── viewer/
|
| 51 |
└── index.html Single-file HTML dashboard (no bundler).
|
|
|
|
| 55 |
|
| 56 |
## KV Scope Layout
|
| 57 |
|
| 58 |
+
All data lives in a single SQLite file (`~/.agentcache/agentcache.db`) in two tables:
|
| 59 |
|
| 60 |
- `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — JSON values
|
| 61 |
- `audit_log(id, ts, agent_id, message)` — write audit trail
|
|
|
|
| 120 |
|
| 121 |
## Authentication
|
| 122 |
|
| 123 |
+
All endpoints except `/livez` check `AGENTCACHE_SECRET` via timing-safe
|
| 124 |
`hmac.compare_digest` Bearer token comparison. No secret → no auth check.
|
| 125 |
|
| 126 |
---
|
|
|
|
| 138 |
|
| 139 |
1. `GeminiEmbeddingProvider` — if `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set (768 dims)
|
| 140 |
2. `OpenAIEmbeddingProvider` — if `OPENAI_API_KEY` is set (1536 dims)
|
| 141 |
+
3. `SentenceTransformerProvider` — if `AGENTCACHE_LOCAL_EMBEDDING_MODEL` is set
|
| 142 |
4. BM25-only fallback
|
| 143 |
|
| 144 |
Without an embedding provider, `HybridSearch` falls back to pure BM25.
|
CLAUDE.md
CHANGED
|
@@ -4,100 +4,106 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
| 4 |
|
| 5 |
## What This Is
|
| 6 |
|
| 7 |
-
Python
|
| 8 |
|
| 9 |
## Running
|
| 10 |
|
| 11 |
-
**Prerequisite**: Dolt SQL Server must be running on `127.0.0.1:3306` with a database named `agentmemory`. Config is read from `~/.agentmemory/.env` at startup.
|
| 12 |
-
|
| 13 |
```bash
|
| 14 |
-
|
| 15 |
python src/app.py
|
|
|
|
|
|
|
| 16 |
```
|
| 17 |
|
| 18 |
-
|
| 19 |
-
- `http://localhost:3111/viewer` or `http://localhost:3111/`
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
|
| 27 |
| Variable | Default | Purpose |
|
| 28 |
|---|---|---|
|
| 29 |
| `III_REST_PORT` / `PORT` | `3111` | API server port |
|
| 30 |
-
| `
|
| 31 |
-
| `
|
| 32 |
-
| `
|
| 33 |
-
| `
|
| 34 |
-
| `
|
| 35 |
-
| `
|
| 36 |
-
| `
|
| 37 |
-
| `
|
| 38 |
-
| `
|
| 39 |
-
| `
|
| 40 |
-
| `
|
|
|
|
|
|
|
| 41 |
|
| 42 |
## Architecture
|
| 43 |
|
| 44 |
### `src/db.py` — Storage Layer
|
| 45 |
-
`StateKV` wraps a single Dolt table `kv_store(scope VARCHAR, key VARCHAR, value LONGTEXT)`. All data is JSON-serialized. Scopes are namespaced strings (e.g. `mem:sessions`, `mem:obs:{session_id}`). Dolt versioning is triggered via `CALL dolt_add('-A')` + `CALL dolt_commit(...)` stored procedures — this is what makes the store git-versioned.
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
- `
|
| 50 |
-
- `_hybrid_search` — combines BM25 + vector search; only initialized when embedding provider is set
|
| 51 |
-
- `_stream_broadcaster` — WebSocket broadcast callback injected by `app.py`
|
| 52 |
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
*
|
| 62 |
|
| 63 |
### `src/search.py` — Search Indexes
|
| 64 |
-
|
| 65 |
-
- `
|
| 66 |
-
- `
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
- `GET /replay/sessions` — sessions list for replay tab
|
| 102 |
-
- `GET /replay/load?sessionId=<id>` — full session + observations for replay
|
| 103 |
-
- `GET/POST /mcp/tools` — MCP protocol adapter
|
|
|
|
| 4 |
|
| 5 |
## What This Is
|
| 6 |
|
| 7 |
+
A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no Dolt. Agents use it to store observations scoped to `(folder_path, agent_id)` pairs and global long-term memories. The architecture is **folder-based** — sessions, lessons, slots, and actions are removed.
|
| 8 |
|
| 9 |
## Running
|
| 10 |
|
|
|
|
|
|
|
| 11 |
```bash
|
| 12 |
+
pip install -r requirements.txt
|
| 13 |
python src/app.py
|
| 14 |
+
# Server on http://localhost:3111
|
| 15 |
+
# Viewer at http://localhost:3111/viewer
|
| 16 |
```
|
| 17 |
|
| 18 |
+
No build step. SQLite file lives at `~/.agentcache/agentcache.db`. Config optionally loaded from `~/.agentcache/.env`.
|
|
|
|
| 19 |
|
| 20 |
+
## Running Tests
|
| 21 |
|
| 22 |
+
```bash
|
| 23 |
+
pip install -e ".[dev]"
|
| 24 |
+
pytest tests/ -v
|
| 25 |
+
```
|
| 26 |
|
| 27 |
+
## Key Environment Variables
|
| 28 |
|
| 29 |
| Variable | Default | Purpose |
|
| 30 |
|---|---|---|
|
| 31 |
| `III_REST_PORT` / `PORT` | `3111` | API server port |
|
| 32 |
+
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Gemini vector search (priority 1) |
|
| 33 |
+
| `OPENAI_API_KEY` | — | OpenAI vector search (priority 2) |
|
| 34 |
+
| `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | — | SentenceTransformer model (priority 3) |
|
| 35 |
+
| `AGENTCACHE_SECRET` | — | Bearer token auth on all endpoints |
|
| 36 |
+
| `AGENT_ID` | — | Default agent ID |
|
| 37 |
+
| `AGENTCACHE_AGENT_SCOPE=isolated` | — | Filter data to current `AGENT_ID` |
|
| 38 |
+
| `AGENTCACHE_CWD` | — | Fallback folder path for legacy clients |
|
| 39 |
+
| `MAX_OBS_PER_FOLDER` | `2000` | Hard cap on observations per (folder, agent) pair |
|
| 40 |
+
| `TOKEN_BUDGET` | `2000` | Context compilation cap |
|
| 41 |
+
| `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph extraction (needs LLM) |
|
| 42 |
+
| `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
|
| 43 |
+
| `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM-powered observation compression |
|
| 44 |
+
| `AGENTCACHE_CORS_ORIGINS` | see app.py | Comma-separated allowed CORS origins |
|
| 45 |
|
| 46 |
## Architecture
|
| 47 |
|
| 48 |
### `src/db.py` — Storage Layer
|
|
|
|
| 49 |
|
| 50 |
+
`StateKV` wraps SQLite with two tables:
|
| 51 |
+
- `kv_store(scope, key, value)` — all data as JSON, namespaced by scope
|
| 52 |
+
- `audit_log(id, ts, agent_id, message)` — write audit trail
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
Per-thread persistent connections via `threading.local()`. WAL checkpoint on shutdown.
|
| 55 |
|
| 56 |
+
Key scopes: `mem:folders` (index), `mem:folder:{path}:{agent}` (observations), `mem:foldermeta:{path}:{agent}` (metadata), `mem:obs_lookup` (O(1) reverse lookup), `mem:memories` (global), `mem:index:bm25:*` (search).
|
| 57 |
|
| 58 |
+
### `src/functions.py` — Business Logic
|
| 59 |
|
| 60 |
+
All core implementations. Key functions:
|
| 61 |
+
- `folder_observe(kv, payload)` — ingest a folder-scoped observation
|
| 62 |
+
- `folder_search(kv, query, limit, folder_path, agent_id)` — BM25+vector hybrid search
|
| 63 |
+
- `folder_timeline(kv, limit, folder_path, agent_id, before, after)` — activity feed
|
| 64 |
+
- `folder_graph_build(kv)` — build graph nodes + edges
|
| 65 |
+
- `remember(kv, data)` / `forget(kv, data)` — global memory management
|
| 66 |
+
- `health_check(kv)` — system stats
|
| 67 |
+
- `export_data(kv, data)` — v2 JSON export
|
| 68 |
+
- `migrate_sessions_to_folders(kv, dry_run)` — legacy migration
|
| 69 |
|
| 70 |
+
`src/memory/*` contains thin shim modules that re-export from here.
|
| 71 |
|
| 72 |
### `src/search.py` — Search Indexes
|
| 73 |
+
|
| 74 |
+
- `SearchIndex`: BM25 with Porter stemmer + synonym expansion. `_dirty` flag prevents unnecessary saves.
|
| 75 |
+
- `VectorIndex`: cosine similarity over embeddings as base64 float32. `_dirty` flag.
|
| 76 |
+
- `HybridSearch`: RRF fusion (k=60) of BM25 + vector.
|
| 77 |
+
- `GeminiEmbeddingProvider`, `OpenAIEmbeddingProvider`, `SentenceTransformerProvider`.
|
| 78 |
+
|
| 79 |
+
### `src/app.py` — Flask Factory
|
| 80 |
+
|
| 81 |
+
`create_app()`: init DB → auto-select embedding provider → init IndexPersistence → backfill obs_lookup → register blueprints → setup WebSocket → register CORS → start background workers.
|
| 82 |
+
|
| 83 |
+
### `src/routes/` — Flask Blueprints
|
| 84 |
+
|
| 85 |
+
| Blueprint | Endpoints |
|
| 86 |
+
|-----------|-----------|
|
| 87 |
+
| `observations.py` | `/observe`, `/agent/observe`, `/folders`, `/folder/observations`, legacy session shims |
|
| 88 |
+
| `memories.py` | `/remember`, `/agent/remember`, `/memories`, `/forget` |
|
| 89 |
+
| `search.py` | `/search`, `/timeline` |
|
| 90 |
+
| `graph.py` | `/graph`, `/graph/stats`, `/graph/query`, `/graph/build` |
|
| 91 |
+
| `health.py` | `/livez`, `/health`, `/audit`, `/config/flags` |
|
| 92 |
+
| `mcp.py` | `/mcp/tools` GET+POST (12 tools) |
|
| 93 |
+
| `migration.py` | `/migrate` |
|
| 94 |
+
|
| 95 |
+
### `src/workers.py` — Background Threads
|
| 96 |
+
|
| 97 |
+
- Index rebuild thread (if BM25 index is empty or out of sync on boot)
|
| 98 |
+
- Auto-forget sweep loop (hourly)
|
| 99 |
+
- SIGTERM/SIGINT handlers: flush `IndexPersistence`, WAL checkpoint, exit 0
|
| 100 |
+
|
| 101 |
+
### `src/viewer/index.html` — Dashboard
|
| 102 |
+
|
| 103 |
+
Single-file HTML, served at `/viewer`. Tabs: **Folders**, **Memories**, **Graph**, **Timeline**. No build step.
|
| 104 |
+
|
| 105 |
+
## MCP Tools (12 active)
|
| 106 |
+
|
| 107 |
+
`agent_observe`, `agent_remember`, `memory_recall`, `memory_smart_search`, `memory_save`, `memory_export`, `memory_forget`, `memory_diagnose`, `memory_folders`, `memory_folder_observations`, `memory_timeline`
|
| 108 |
+
|
| 109 |
+
Full schema at `GET /agentcache/mcp/tools`.
|
|
|
|
|
|
|
|
|
Caddyfile
CHANGED
|
@@ -4,16 +4,16 @@
|
|
| 4 |
handle /stream/* {
|
| 5 |
reverse_proxy localhost:3112 {
|
| 6 |
header_up Host localhost:3112
|
| 7 |
-
header_up Authorization "Bearer {env.
|
| 8 |
}
|
| 9 |
}
|
| 10 |
|
| 11 |
# Route REST API endpoints (iii-http on 3111)
|
| 12 |
# Caddy injects the secret so the dashboard viewer works without exposing it to the browser
|
| 13 |
-
handle /agentmemory/* {
|
| 14 |
reverse_proxy localhost:3111 {
|
| 15 |
header_up Host localhost:3111
|
| 16 |
-
header_up Authorization "Bearer {env.
|
| 17 |
}
|
| 18 |
}
|
| 19 |
|
|
|
|
| 4 |
handle /stream/* {
|
| 5 |
reverse_proxy localhost:3112 {
|
| 6 |
header_up Host localhost:3112
|
| 7 |
+
header_up Authorization "Bearer {env.AGENTCACHE_SECRET}"
|
| 8 |
}
|
| 9 |
}
|
| 10 |
|
| 11 |
# Route REST API endpoints (iii-http on 3111)
|
| 12 |
# Caddy injects the secret so the dashboard viewer works without exposing it to the browser
|
| 13 |
+
handle /agentcache/* /agentmemory/* {
|
| 14 |
reverse_proxy localhost:3111 {
|
| 15 |
header_up Host localhost:3111
|
| 16 |
+
header_up Authorization "Bearer {env.AGENTCACHE_SECRET}"
|
| 17 |
}
|
| 18 |
}
|
| 19 |
|
INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
# Install
|
| 2 |
|
| 3 |
-
You are a coding agent. Follow this runbook top to bottom to install
|
| 4 |
|
| 5 |
## What you are installing
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
Default mode works with no API key. It runs BM25 keyword search out of the box. Set `GEMINI_API_KEY` to also enable vector semantic search.
|
| 10 |
|
|
@@ -12,13 +12,13 @@ Default mode works with no API key. It runs BM25 keyword search out of the box.
|
|
| 12 |
|
| 13 |
- Python 3.10 or newer. Check with `python --version` or `python3 --version`.
|
| 14 |
- pip. Check with `pip --version`.
|
| 15 |
-
- Port 3111 free. If taken, set `III_REST_PORT=<other>` in `~/.
|
| 16 |
|
| 17 |
## 1. Clone the repo
|
| 18 |
|
| 19 |
```bash
|
| 20 |
-
git clone https://github.com/Yash030/
|
| 21 |
-
cd
|
| 22 |
```
|
| 23 |
|
| 24 |
Expect: the directory exists with `src/app.py` inside.
|
|
@@ -44,7 +44,7 @@ Or run in a separate terminal: `python src/app.py`
|
|
| 44 |
Wait until it is reachable:
|
| 45 |
|
| 46 |
```bash
|
| 47 |
-
curl -fsS http://localhost:3111/
|
| 48 |
```
|
| 49 |
|
| 50 |
Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the background; this takes under a second.
|
|
@@ -53,12 +53,12 @@ Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the backg
|
|
| 53 |
|
| 54 |
```bash
|
| 55 |
# Save a memory
|
| 56 |
-
curl -X POST http://localhost:3111/
|
| 57 |
-H "Content-Type: application/json" \
|
| 58 |
-
-d '{"content":"
|
| 59 |
|
| 60 |
# Recall it
|
| 61 |
-
curl -X POST http://localhost:3111/
|
| 62 |
-H "Content-Type: application/json" \
|
| 63 |
-d '{"query":"install verification probe","limit":5}'
|
| 64 |
```
|
|
@@ -67,18 +67,18 @@ Expect: first call returns `{"id": "...", "success": true}`, second returns resu
|
|
| 67 |
|
| 68 |
## 5. Wire MCP into the calling agent
|
| 69 |
|
| 70 |
-
Detect which agent is running this runbook, then add
|
| 71 |
|
| 72 |
### Standard MCP block (Cursor, Claude Desktop, Cline, Windsurf, Gemini CLI)
|
| 73 |
|
| 74 |
Merge this into the agent's `mcpServers` object:
|
| 75 |
|
| 76 |
```json
|
| 77 |
-
"
|
| 78 |
"command": "npx",
|
| 79 |
-
"args": ["-y", "@
|
| 80 |
"env": {
|
| 81 |
-
"
|
| 82 |
}
|
| 83 |
}
|
| 84 |
```
|
|
@@ -88,11 +88,11 @@ Merge this into the agent's `mcpServers` object:
|
|
| 88 |
Add to `~/.claude.json` under `mcpServers`:
|
| 89 |
|
| 90 |
```json
|
| 91 |
-
"
|
| 92 |
"command": "npx",
|
| 93 |
-
"args": ["-y", "@
|
| 94 |
"env": {
|
| 95 |
-
"
|
| 96 |
}
|
| 97 |
}
|
| 98 |
```
|
|
@@ -101,17 +101,17 @@ Then reload MCP: run `/mcp` in Claude Code.
|
|
| 101 |
|
| 102 |
### Any agent — verify tool count
|
| 103 |
|
| 104 |
-
After wiring, the agent should list
|
| 105 |
|
| 106 |
-
If you see 0 tools or an error, check that `python src/app.py` is running and `
|
| 107 |
|
| 108 |
## 6. Setting up agent hooks
|
| 109 |
|
| 110 |
-
Agent hooks post observations to
|
| 111 |
|
| 112 |
### Claude Code (`hooks/claude-code-hook.sh`)
|
| 113 |
|
| 114 |
-
Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `
|
| 115 |
|
| 116 |
```json
|
| 117 |
{
|
|
@@ -134,8 +134,8 @@ Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `AGEN
|
|
| 134 |
Set the required environment variables before starting Claude Code:
|
| 135 |
|
| 136 |
```bash
|
| 137 |
-
export
|
| 138 |
-
export
|
| 139 |
```
|
| 140 |
|
| 141 |
The script picks up `$PWD` as `folderPath` and `$CLAUDE_AGENT_ID` (falling back to `"claude-code"`) as `agentId`.
|
|
@@ -151,7 +151,7 @@ const { logObservation } = require('/path/to/hooks/cursor-hook.js');
|
|
| 151 |
logObservation(`Tool: ${toolName}\nInput: ${JSON.stringify(toolInput)}`);
|
| 152 |
```
|
| 153 |
|
| 154 |
-
The module reads `
|
| 155 |
|
| 156 |
### PowerShell terminal (`hooks/powershell-hook.ps1`)
|
| 157 |
|
|
@@ -164,24 +164,24 @@ Add a single dot-source line to your PowerShell `$PROFILE` to activate automatic
|
|
| 164 |
Set the required variables in `$PROFILE` before the dot-source line:
|
| 165 |
|
| 166 |
```powershell
|
| 167 |
-
$env:
|
| 168 |
-
$env:
|
| 169 |
-
$env:
|
| 170 |
```
|
| 171 |
|
| 172 |
-
The hook installs a PSReadLine `CommandValidationHandler` that fires a background job on every command you run. If PSReadLine is not available, call `Send-
|
| 173 |
|
| 174 |
### `.env` file format
|
| 175 |
|
| 176 |
-
All hooks and the server itself read credentials from `~/.
|
| 177 |
|
| 178 |
```
|
| 179 |
III_REST_PORT=3111
|
| 180 |
-
|
| 181 |
GEMINI_API_KEY=your-gemini-key-here
|
| 182 |
```
|
| 183 |
|
| 184 |
-
The server loads this file on startup. Hook scripts read the same variables from your shell environment (export them from your profile after sourcing `~/.
|
| 185 |
|
| 186 |
## 7. Open the viewer (optional)
|
| 187 |
|
|
@@ -200,8 +200,8 @@ The viewer shows live sessions, memories, and the knowledge graph.
|
|
| 200 |
Vector search finds memories semantically, not just by keyword. Enable it with a free Gemini API key:
|
| 201 |
|
| 202 |
```bash
|
| 203 |
-
mkdir -p ~/.
|
| 204 |
-
echo "GEMINI_API_KEY=your-key-here" >> ~/.
|
| 205 |
```
|
| 206 |
|
| 207 |
Restart the server after adding the key. The viewer's search bar will now use hybrid BM25 + vector retrieval.
|
|
@@ -211,9 +211,9 @@ Restart the server after adding the key. The viewer's search bar will now use hy
|
|
| 211 |
LLM compression makes each observation richer and more searchable. Requires an LLM API key:
|
| 212 |
|
| 213 |
```bash
|
| 214 |
-
# Add to ~/.
|
| 215 |
ANTHROPIC_API_KEY=your-key # or OPENAI_API_KEY or GEMINI_API_KEY
|
| 216 |
-
|
| 217 |
```
|
| 218 |
|
| 219 |
## Lifecycle
|
|
@@ -223,11 +223,11 @@ AGENTMEMORY_AUTO_COMPRESS=true
|
|
| 223 |
pkill -f "python src/app.py"
|
| 224 |
|
| 225 |
# Check what's stored
|
| 226 |
-
curl http://localhost:3111/
|
| 227 |
-
curl http://localhost:3111/
|
| 228 |
|
| 229 |
# Health check
|
| 230 |
-
curl http://localhost:3111/
|
| 231 |
```
|
| 232 |
|
| 233 |
## Troubleshooting
|
|
@@ -235,18 +235,18 @@ curl http://localhost:3111/agentmemory/livez
|
|
| 235 |
| Symptom | Fix |
|
| 236 |
|---------|-----|
|
| 237 |
| `ModuleNotFoundError: No module named 'flask'` | Run `pip install -r requirements.txt` |
|
| 238 |
-
| `Address already in use` on port 3111 | Set `III_REST_PORT=3112` in `~/.
|
| 239 |
| `/livez` never returns | Check `python src/app.py` stderr for import errors |
|
| 240 |
-
| 0 tools in agent | Confirm `
|
| 241 |
-
| `401 Unauthorized` | Set `
|
| 242 |
-
| Vector search not working | Add `GEMINI_API_KEY` to `~/.
|
| 243 |
| Windows Python path issue | Use `python3` instead of `python`, or use WSL2 |
|
| 244 |
|
| 245 |
## Report success
|
| 246 |
|
| 247 |
Report back:
|
| 248 |
|
| 249 |
-
-
|
| 250 |
- MCP wired into the agent, tool count visible
|
| 251 |
- Save and recall round-trip returned the probe memory
|
| 252 |
- Viewer available at http://localhost:3111/viewer
|
|
|
|
| 1 |
+
# Install agentcache-python (agent runbook)
|
| 2 |
|
| 3 |
+
You are a coding agent. Follow this runbook top to bottom to install agentcache-python, wire it into yourself, and verify that persistent memory works. Run each command, check the result against the "Expect" note, then move on.
|
| 4 |
|
| 5 |
## What you are installing
|
| 6 |
|
| 7 |
+
agentcache-python is a local memory server for coding agents. It runs on your machine, exposes a REST API on port 3111 and an MCP endpoint, and stores all memories in a single SQLite file at `~/.agentcache/agentcache.db`. It is built with Python and Flask — no Node.js, no external database, no daemon to manage separately.
|
| 8 |
|
| 9 |
Default mode works with no API key. It runs BM25 keyword search out of the box. Set `GEMINI_API_KEY` to also enable vector semantic search.
|
| 10 |
|
|
|
|
| 12 |
|
| 13 |
- Python 3.10 or newer. Check with `python --version` or `python3 --version`.
|
| 14 |
- pip. Check with `pip --version`.
|
| 15 |
+
- Port 3111 free. If taken, set `III_REST_PORT=<other>` in `~/.agentcache/.env`.
|
| 16 |
|
| 17 |
## 1. Clone the repo
|
| 18 |
|
| 19 |
```bash
|
| 20 |
+
git clone https://github.com/Yash030/agentcache-python.git
|
| 21 |
+
cd agentcache-python
|
| 22 |
```
|
| 23 |
|
| 24 |
Expect: the directory exists with `src/app.py` inside.
|
|
|
|
| 44 |
Wait until it is reachable:
|
| 45 |
|
| 46 |
```bash
|
| 47 |
+
curl -fsS http://localhost:3111/agentcache/livez
|
| 48 |
```
|
| 49 |
|
| 50 |
Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the background; this takes under a second.
|
|
|
|
| 53 |
|
| 54 |
```bash
|
| 55 |
# Save a memory
|
| 56 |
+
curl -X POST http://localhost:3111/agentcache/remember \
|
| 57 |
-H "Content-Type: application/json" \
|
| 58 |
+
-d '{"content":"agentcache-python install verification probe","concepts":["install-check"]}'
|
| 59 |
|
| 60 |
# Recall it
|
| 61 |
+
curl -X POST http://localhost:3111/agentcache/search \
|
| 62 |
-H "Content-Type: application/json" \
|
| 63 |
-d '{"query":"install verification probe","limit":5}'
|
| 64 |
```
|
|
|
|
| 67 |
|
| 68 |
## 5. Wire MCP into the calling agent
|
| 69 |
|
| 70 |
+
Detect which agent is running this runbook, then add agentcache-python to its MCP config.
|
| 71 |
|
| 72 |
### Standard MCP block (Cursor, Claude Desktop, Cline, Windsurf, Gemini CLI)
|
| 73 |
|
| 74 |
Merge this into the agent's `mcpServers` object:
|
| 75 |
|
| 76 |
```json
|
| 77 |
+
"agentcache": {
|
| 78 |
"command": "npx",
|
| 79 |
+
"args": ["-y", "@agentcache/mcp"],
|
| 80 |
"env": {
|
| 81 |
+
"AGENTCACHE_URL": "http://localhost:3111"
|
| 82 |
}
|
| 83 |
}
|
| 84 |
```
|
|
|
|
| 88 |
Add to `~/.claude.json` under `mcpServers`:
|
| 89 |
|
| 90 |
```json
|
| 91 |
+
"agentcache": {
|
| 92 |
"command": "npx",
|
| 93 |
+
"args": ["-y", "@agentcache/mcp"],
|
| 94 |
"env": {
|
| 95 |
+
"AGENTCACHE_URL": "http://localhost:3111"
|
| 96 |
}
|
| 97 |
}
|
| 98 |
```
|
|
|
|
| 101 |
|
| 102 |
### Any agent — verify tool count
|
| 103 |
|
| 104 |
+
After wiring, the agent should list agentcache's tools. With the server running you should see 16 tools (e.g. `memory_save`, `memory_smart_search`, `memory_sessions`).
|
| 105 |
|
| 106 |
+
If you see 0 tools or an error, check that `python src/app.py` is running and `AGENTCACHE_URL` points at it.
|
| 107 |
|
| 108 |
## 6. Setting up agent hooks
|
| 109 |
|
| 110 |
+
Agent hooks post observations to agentcache automatically on every tool use, command, or edit — no manual calls required. Hook scripts live in the [`hooks/`](hooks/) directory.
|
| 111 |
|
| 112 |
### Claude Code (`hooks/claude-code-hook.sh`)
|
| 113 |
|
| 114 |
+
Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `AGENTCACHE_URL` and `AGENTCACHE_SECRET` from your shell environment, so no secrets are embedded in the config file.
|
| 115 |
|
| 116 |
```json
|
| 117 |
{
|
|
|
|
| 134 |
Set the required environment variables before starting Claude Code:
|
| 135 |
|
| 136 |
```bash
|
| 137 |
+
export AGENTCACHE_URL=http://127.0.0.1:3111
|
| 138 |
+
export AGENTCACHE_SECRET=your-secret-here # omit if no auth set
|
| 139 |
```
|
| 140 |
|
| 141 |
The script picks up `$PWD` as `folderPath` and `$CLAUDE_AGENT_ID` (falling back to `"claude-code"`) as `agentId`.
|
|
|
|
| 151 |
logObservation(`Tool: ${toolName}\nInput: ${JSON.stringify(toolInput)}`);
|
| 152 |
```
|
| 153 |
|
| 154 |
+
The module reads `AGENTCACHE_URL`, `AGENTCACHE_SECRET`, and `AGENTCACHE_AGENT_ID` from `process.env`. Set them in your shell profile or in Cursor's environment settings.
|
| 155 |
|
| 156 |
### PowerShell terminal (`hooks/powershell-hook.ps1`)
|
| 157 |
|
|
|
|
| 164 |
Set the required variables in `$PROFILE` before the dot-source line:
|
| 165 |
|
| 166 |
```powershell
|
| 167 |
+
$env:AGENTCACHE_URL = "http://127.0.0.1:3111"
|
| 168 |
+
$env:AGENTCACHE_SECRET = "your-secret-here" # omit if no auth set
|
| 169 |
+
$env:AGENTCACHE_AGENT_ID = "powershell"
|
| 170 |
```
|
| 171 |
|
| 172 |
+
The hook installs a PSReadLine `CommandValidationHandler` that fires a background job on every command you run. If PSReadLine is not available, call `Send-AgentCacheObservation -Text "..."` manually.
|
| 173 |
|
| 174 |
### `.env` file format
|
| 175 |
|
| 176 |
+
All hooks and the server itself read credentials from `~/.agentcache/.env`. Create this file if it doesn't exist:
|
| 177 |
|
| 178 |
```
|
| 179 |
III_REST_PORT=3111
|
| 180 |
+
AGENTCACHE_SECRET=your-secret-here
|
| 181 |
GEMINI_API_KEY=your-gemini-key-here
|
| 182 |
```
|
| 183 |
|
| 184 |
+
The server loads this file on startup. Hook scripts read the same variables from your shell environment (export them from your profile after sourcing `~/.agentcache/.env`, or use `direnv` / `dotenv` tooling).
|
| 185 |
|
| 186 |
## 7. Open the viewer (optional)
|
| 187 |
|
|
|
|
| 200 |
Vector search finds memories semantically, not just by keyword. Enable it with a free Gemini API key:
|
| 201 |
|
| 202 |
```bash
|
| 203 |
+
mkdir -p ~/.agentcache
|
| 204 |
+
echo "GEMINI_API_KEY=your-key-here" >> ~/.agentcache/.env
|
| 205 |
```
|
| 206 |
|
| 207 |
Restart the server after adding the key. The viewer's search bar will now use hybrid BM25 + vector retrieval.
|
|
|
|
| 211 |
LLM compression makes each observation richer and more searchable. Requires an LLM API key:
|
| 212 |
|
| 213 |
```bash
|
| 214 |
+
# Add to ~/.agentcache/.env
|
| 215 |
ANTHROPIC_API_KEY=your-key # or OPENAI_API_KEY or GEMINI_API_KEY
|
| 216 |
+
AGENTCACHE_AUTO_COMPRESS=true
|
| 217 |
```
|
| 218 |
|
| 219 |
## Lifecycle
|
|
|
|
| 223 |
pkill -f "python src/app.py"
|
| 224 |
|
| 225 |
# Check what's stored
|
| 226 |
+
curl http://localhost:3111/agentcache/sessions
|
| 227 |
+
curl http://localhost:3111/agentcache/memories
|
| 228 |
|
| 229 |
# Health check
|
| 230 |
+
curl http://localhost:3111/agentcache/livez
|
| 231 |
```
|
| 232 |
|
| 233 |
## Troubleshooting
|
|
|
|
| 235 |
| Symptom | Fix |
|
| 236 |
|---------|-----|
|
| 237 |
| `ModuleNotFoundError: No module named 'flask'` | Run `pip install -r requirements.txt` |
|
| 238 |
+
| `Address already in use` on port 3111 | Set `III_REST_PORT=3112` in `~/.agentcache/.env` and restart |
|
| 239 |
| `/livez` never returns | Check `python src/app.py` stderr for import errors |
|
| 240 |
+
| 0 tools in agent | Confirm `AGENTCACHE_URL=http://localhost:3111` and the server is running |
|
| 241 |
+
| `401 Unauthorized` | Set `AGENTCACHE_SECRET` env var in the MCP config to match `~/.agentcache/.env` |
|
| 242 |
+
| Vector search not working | Add `GEMINI_API_KEY` to `~/.agentcache/.env` and restart |
|
| 243 |
| Windows Python path issue | Use `python3` instead of `python`, or use WSL2 |
|
| 244 |
|
| 245 |
## Report success
|
| 246 |
|
| 247 |
Report back:
|
| 248 |
|
| 249 |
+
- agentcache-python server running on port 3111
|
| 250 |
- MCP wired into the agent, tool count visible
|
| 251 |
- Save and recall round-trip returned the probe memory
|
| 252 |
- Viewer available at http://localhost:3111/viewer
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
@@ -7,7 +7,7 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
<h1 align="center">
|
| 11 |
|
| 12 |
<p align="center">
|
| 13 |
<strong>Persistent memory for AI coding agents — pure Python, zero external databases.</strong><br/>
|
|
@@ -49,14 +49,14 @@ pinned: false
|
|
| 49 |
|
| 50 |
## What Is This?
|
| 51 |
|
| 52 |
-
**
|
| 53 |
|
| 54 |
Key differences from the Node.js original:
|
| 55 |
|
| 56 |
- **No Node.js or iii-engine** — runs with plain `python src/app.py`
|
| 57 |
- **SQLite instead of Dolt** — single file, WAL mode, instant startup
|
| 58 |
- **HuggingFace Space ready** — deploys in one click, data synced to an HF dataset repo
|
| 59 |
-
- **Same REST + MCP wire format** — drop-in for any agent already wired to
|
| 60 |
|
| 61 |
Your agent captures every tool call, stores them as observations, compresses them into searchable memory, and injects the right context at the start of every new session — automatically.
|
| 62 |
|
|
@@ -68,8 +68,8 @@ Your agent captures every tool call, stores them as observations, compresses the
|
|
| 68 |
|
| 69 |
```bash
|
| 70 |
# Clone
|
| 71 |
-
git clone https://github.com/Yash030/
|
| 72 |
-
cd
|
| 73 |
|
| 74 |
# Install dependencies (no build step)
|
| 75 |
pip install -r requirements.txt
|
|
@@ -84,16 +84,16 @@ Server starts on **http://localhost:3111**. Open the viewer at http://localhost:
|
|
| 84 |
|
| 85 |
```bash
|
| 86 |
# Health check
|
| 87 |
-
curl http://localhost:3111/
|
| 88 |
# {"status": "ok"}
|
| 89 |
|
| 90 |
# Save a memory
|
| 91 |
-
curl -X POST http://localhost:3111/
|
| 92 |
-H "Content-Type: application/json" \
|
| 93 |
-d '{"content": "JWT auth uses jose middleware in src/middleware/auth.ts", "concepts": ["auth", "jwt"]}'
|
| 94 |
|
| 95 |
# Recall it
|
| 96 |
-
curl -X POST http://localhost:3111/
|
| 97 |
-H "Content-Type: application/json" \
|
| 98 |
-d '{"query": "authentication middleware", "limit": 5}'
|
| 99 |
```
|
|
@@ -112,7 +112,7 @@ curl -X POST http://localhost:3111/agentmemory/search \
|
|
| 112 |
| Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
|
| 113 |
| 4-tier memory consolidation | ⚙️ | `CONSOLIDATION_ENABLED=true` + LLM key |
|
| 114 |
| Knowledge graph extraction | ⚙️ | `GRAPH_EXTRACTION_ENABLED=true` + LLM key |
|
| 115 |
-
| LLM observation compression | ⚙️ | `
|
| 116 |
| Lessons with confidence decay | ✅ | Fingerprinted, auto-strengthen on repeat |
|
| 117 |
| Memory slots (pinned context) | ✅ | CRUD + auto-reflect |
|
| 118 |
| Session replay | ✅ | Full timeline in viewer |
|
|
@@ -135,18 +135,18 @@ Inspired by how human memory works — raw experience → compressed episodes
|
|
| 135 |
|
| 136 |
## MCP Integration
|
| 137 |
|
| 138 |
-
Wire
|
| 139 |
|
| 140 |
### Most agents (Cursor, Claude Desktop, Cline, Windsurf)
|
| 141 |
|
| 142 |
```json
|
| 143 |
{
|
| 144 |
"mcpServers": {
|
| 145 |
-
"
|
| 146 |
"command": "npx",
|
| 147 |
-
"args": ["-y", "@
|
| 148 |
"env": {
|
| 149 |
-
"
|
| 150 |
}
|
| 151 |
}
|
| 152 |
}
|
|
@@ -158,16 +158,16 @@ Wire agentmemory-python into your agent's MCP config. It speaks the same MCP pro
|
|
| 158 |
Paste this prompt and your agent will wire everything:
|
| 159 |
|
| 160 |
```
|
| 161 |
-
Start
|
| 162 |
Then add this MCP server to ~/.claude.json under mcpServers:
|
| 163 |
{
|
| 164 |
-
"
|
| 165 |
"command": "npx",
|
| 166 |
-
"args": ["-y", "@
|
| 167 |
-
"env": { "
|
| 168 |
}
|
| 169 |
}
|
| 170 |
-
Verify with: curl http://localhost:3111/
|
| 171 |
Open the viewer at: http://localhost:3111/viewer
|
| 172 |
```
|
| 173 |
|
|
@@ -193,7 +193,7 @@ Open the viewer at: http://localhost:3111/viewer
|
|
| 193 |
| `memory_forget` | Delete memory, session, or observations |
|
| 194 |
| `memory_export` | Export all memory data as JSON |
|
| 195 |
| `agent_observe` | Log agent execution observation |
|
| 196 |
-
| `agent_remember` | Save agent
|
| 197 |
| `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
|
| 198 |
| `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect |
|
| 199 |
| `memory_slot_list` | List all pinned memory slots |
|
|
@@ -211,7 +211,7 @@ Open the viewer at: http://localhost:3111/viewer
|
|
| 211 |
|
| 212 |
## API Reference
|
| 213 |
|
| 214 |
-
Base URL: `http://localhost:3111/
|
| 215 |
|
| 216 |
### Health
|
| 217 |
|
|
@@ -304,7 +304,7 @@ Base URL: `http://localhost:3111/agentmemory`
|
|
| 304 |
|
| 305 |
## Configuration
|
| 306 |
|
| 307 |
-
Create `~/.
|
| 308 |
|
| 309 |
```env
|
| 310 |
# Server port
|
|
@@ -322,22 +322,22 @@ ANTHROPIC_API_KEY=your-anthropic-key
|
|
| 322 |
# LLM-powered features (disabled by default — spend tokens)
|
| 323 |
CONSOLIDATION_ENABLED=true
|
| 324 |
GRAPH_EXTRACTION_ENABLED=true
|
| 325 |
-
|
| 326 |
|
| 327 |
# Context injection limits
|
| 328 |
TOKEN_BUDGET=2000
|
| 329 |
MAX_OBS_PER_SESSION=500
|
| 330 |
|
| 331 |
# Auth — set to require Bearer token on all endpoints
|
| 332 |
-
|
| 333 |
|
| 334 |
# Agent scope isolation
|
| 335 |
AGENT_ID=my-agent
|
| 336 |
-
|
| 337 |
|
| 338 |
# HuggingFace sync
|
| 339 |
HF_TOKEN=your-hf-token
|
| 340 |
-
|
| 341 |
```
|
| 342 |
|
| 343 |
### Full Variable Reference
|
|
@@ -346,14 +346,14 @@ AGENTMEMORY_DATASET_REPO=username/agentmemory-data
|
|
| 346 |
|----------|---------|---------|
|
| 347 |
| `III_REST_PORT` / `PORT` | `3111` | API server port |
|
| 348 |
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables 768-dim vector search |
|
| 349 |
-
| `
|
| 350 |
| `AGENT_ID` | — | Default agent ID for scope isolation |
|
| 351 |
-
| `
|
| 352 |
| `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
|
| 353 |
| `TOKEN_BUDGET` | `2000` | Max tokens in compiled context |
|
| 354 |
| `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph (needs LLM) |
|
| 355 |
| `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
|
| 356 |
-
| `
|
| 357 |
|
| 358 |
---
|
| 359 |
|
|
@@ -381,17 +381,17 @@ This project is designed to run as a HuggingFace Space. Data is stored in an HF
|
|
| 381 |
### Setup
|
| 382 |
|
| 383 |
1. Fork this repo as a HuggingFace Space (SDK: Docker)
|
| 384 |
-
2. Create a dataset repo (e.g. `your-username/
|
| 385 |
3. Add Space secrets in the HF dashboard:
|
| 386 |
|
| 387 |
| Secret | Value |
|
| 388 |
|--------|-------|
|
| 389 |
| `HF_TOKEN` | Your HF write token |
|
| 390 |
-
| `
|
| 391 |
-
| `
|
| 392 |
| `GEMINI_API_KEY` | Gemini key (optional, enables vector search) |
|
| 393 |
|
| 394 |
-
4. The Space boots, restores `
|
| 395 |
|
| 396 |
### How sync works
|
| 397 |
|
|
@@ -403,7 +403,7 @@ python sync.py
|
|
| 403 |
|
| 404 |
# Environment for sync
|
| 405 |
HF_TOKEN=...
|
| 406 |
-
|
| 407 |
```
|
| 408 |
|
| 409 |
---
|
|
@@ -411,7 +411,7 @@ AGENTMEMORY_DATASET_REPO=username/agentmemory-data
|
|
| 411 |
## Architecture
|
| 412 |
|
| 413 |
```
|
| 414 |
-
|
| 415 |
├── src/
|
| 416 |
│ ├── app.py Flask server — all endpoints, WebSocket broadcaster
|
| 417 |
│ ├── db.py SQLite StateKV — WAL mode, audit_log table
|
|
@@ -427,7 +427,7 @@ agentmemory-python/
|
|
| 427 |
|
| 428 |
### Database layout
|
| 429 |
|
| 430 |
-
Two SQLite tables in `~/.
|
| 431 |
|
| 432 |
```sql
|
| 433 |
-- All data lives here, namespaced by scope
|
|
@@ -460,9 +460,9 @@ Query
|
|
| 460 |
|
| 461 |
---
|
| 462 |
|
| 463 |
-
## vs Original
|
| 464 |
|
| 465 |
-
| |
|
| 466 |
|---|---|---|
|
| 467 |
| Runtime | Node.js 20+ | Python 3.10+ |
|
| 468 |
| Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
|
|
@@ -473,7 +473,7 @@ Query
|
|
| 473 |
| Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
|
| 474 |
| Cold boot | ~7s (iii engine warm-up) | <2s |
|
| 475 |
| Database size | ~232MB (417 Dolt chunk files) | ~20MB (single `.db` file) |
|
| 476 |
-
| Setup | `npm install -g @
|
| 477 |
|
| 478 |
Choose the Python version for: simpler setup, HF Space deployment, single-file database, no Node.js, or Python ecosystem integration.
|
| 479 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AgentCache Python
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
<h1 align="center">agentcache-python</h1>
|
| 11 |
|
| 12 |
<p align="center">
|
| 13 |
<strong>Persistent memory for AI coding agents — pure Python, zero external databases.</strong><br/>
|
|
|
|
| 49 |
|
| 50 |
## What Is This?
|
| 51 |
|
| 52 |
+
**agentcache-python** is a Python reimplementation of the [agentcache](https://github.com/rohitg00/agentcache) persistent memory server. It exposes a REST API, WebSocket stream, and MCP tools endpoint that AI coding agents use to store and retrieve session observations, long-term memories, lessons, and pinned memory slots.
|
| 53 |
|
| 54 |
Key differences from the Node.js original:
|
| 55 |
|
| 56 |
- **No Node.js or iii-engine** — runs with plain `python src/app.py`
|
| 57 |
- **SQLite instead of Dolt** — single file, WAL mode, instant startup
|
| 58 |
- **HuggingFace Space ready** — deploys in one click, data synced to an HF dataset repo
|
| 59 |
+
- **Same REST + MCP wire format** — drop-in for any agent already wired to agentcache
|
| 60 |
|
| 61 |
Your agent captures every tool call, stores them as observations, compresses them into searchable memory, and injects the right context at the start of every new session — automatically.
|
| 62 |
|
|
|
|
| 68 |
|
| 69 |
```bash
|
| 70 |
# Clone
|
| 71 |
+
git clone https://github.com/Yash030/agentcache-python.git
|
| 72 |
+
cd agentcache-python
|
| 73 |
|
| 74 |
# Install dependencies (no build step)
|
| 75 |
pip install -r requirements.txt
|
|
|
|
| 84 |
|
| 85 |
```bash
|
| 86 |
# Health check
|
| 87 |
+
curl http://localhost:3111/agentcache/livez
|
| 88 |
# {"status": "ok"}
|
| 89 |
|
| 90 |
# Save a memory
|
| 91 |
+
curl -X POST http://localhost:3111/agentcache/remember \
|
| 92 |
-H "Content-Type: application/json" \
|
| 93 |
-d '{"content": "JWT auth uses jose middleware in src/middleware/auth.ts", "concepts": ["auth", "jwt"]}'
|
| 94 |
|
| 95 |
# Recall it
|
| 96 |
+
curl -X POST http://localhost:3111/agentcache/search \
|
| 97 |
-H "Content-Type: application/json" \
|
| 98 |
-d '{"query": "authentication middleware", "limit": 5}'
|
| 99 |
```
|
|
|
|
| 112 |
| Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
|
| 113 |
| 4-tier memory consolidation | ⚙️ | `CONSOLIDATION_ENABLED=true` + LLM key |
|
| 114 |
| Knowledge graph extraction | ⚙️ | `GRAPH_EXTRACTION_ENABLED=true` + LLM key |
|
| 115 |
+
| LLM observation compression | ⚙️ | `AGENTCACHE_AUTO_COMPRESS=true` + LLM key |
|
| 116 |
| Lessons with confidence decay | ✅ | Fingerprinted, auto-strengthen on repeat |
|
| 117 |
| Memory slots (pinned context) | ✅ | CRUD + auto-reflect |
|
| 118 |
| Session replay | ✅ | Full timeline in viewer |
|
|
|
|
| 135 |
|
| 136 |
## MCP Integration
|
| 137 |
|
| 138 |
+
Wire agentcache-python into your agent's MCP config. It speaks the same MCP protocol as the Node.js original.
|
| 139 |
|
| 140 |
### Most agents (Cursor, Claude Desktop, Cline, Windsurf)
|
| 141 |
|
| 142 |
```json
|
| 143 |
{
|
| 144 |
"mcpServers": {
|
| 145 |
+
"agentcache": {
|
| 146 |
"command": "npx",
|
| 147 |
+
"args": ["-y", "@agentcache/mcp"],
|
| 148 |
"env": {
|
| 149 |
+
"AGENTCACHE_URL": "http://localhost:3111"
|
| 150 |
}
|
| 151 |
}
|
| 152 |
}
|
|
|
|
| 158 |
Paste this prompt and your agent will wire everything:
|
| 159 |
|
| 160 |
```
|
| 161 |
+
Start agentcache-python: run `python src/app.py` from the agentcache-python directory.
|
| 162 |
Then add this MCP server to ~/.claude.json under mcpServers:
|
| 163 |
{
|
| 164 |
+
"agentcache": {
|
| 165 |
"command": "npx",
|
| 166 |
+
"args": ["-y", "@agentcache/mcp"],
|
| 167 |
+
"env": { "AGENTCACHE_URL": "http://localhost:3111" }
|
| 168 |
}
|
| 169 |
}
|
| 170 |
+
Verify with: curl http://localhost:3111/agentcache/livez
|
| 171 |
Open the viewer at: http://localhost:3111/viewer
|
| 172 |
```
|
| 173 |
|
|
|
|
| 193 |
| `memory_forget` | Delete memory, session, or observations |
|
| 194 |
| `memory_export` | Export all memory data as JSON |
|
| 195 |
| `agent_observe` | Log agent execution observation |
|
| 196 |
+
| `agent_remember` | Save agent cache to long-term storage |
|
| 197 |
| `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
|
| 198 |
| `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect |
|
| 199 |
| `memory_slot_list` | List all pinned memory slots |
|
|
|
|
| 211 |
|
| 212 |
## API Reference
|
| 213 |
|
| 214 |
+
Base URL: `http://localhost:3111/agentcache`
|
| 215 |
|
| 216 |
### Health
|
| 217 |
|
|
|
|
| 304 |
|
| 305 |
## Configuration
|
| 306 |
|
| 307 |
+
Create `~/.agentcache/.env` (no `export` prefix needed):
|
| 308 |
|
| 309 |
```env
|
| 310 |
# Server port
|
|
|
|
| 322 |
# LLM-powered features (disabled by default — spend tokens)
|
| 323 |
CONSOLIDATION_ENABLED=true
|
| 324 |
GRAPH_EXTRACTION_ENABLED=true
|
| 325 |
+
AGENTCACHE_AUTO_COMPRESS=true
|
| 326 |
|
| 327 |
# Context injection limits
|
| 328 |
TOKEN_BUDGET=2000
|
| 329 |
MAX_OBS_PER_SESSION=500
|
| 330 |
|
| 331 |
# Auth — set to require Bearer token on all endpoints
|
| 332 |
+
AGENTCACHE_SECRET=your-secret
|
| 333 |
|
| 334 |
# Agent scope isolation
|
| 335 |
AGENT_ID=my-agent
|
| 336 |
+
AGENTCACHE_AGENT_SCOPE=isolated # only see this agent's data
|
| 337 |
|
| 338 |
# HuggingFace sync
|
| 339 |
HF_TOKEN=your-hf-token
|
| 340 |
+
AGENTCACHE_DATASET_REPO=username/agentcache-data
|
| 341 |
```
|
| 342 |
|
| 343 |
### Full Variable Reference
|
|
|
|
| 346 |
|----------|---------|---------|
|
| 347 |
| `III_REST_PORT` / `PORT` | `3111` | API server port |
|
| 348 |
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables 768-dim vector search |
|
| 349 |
+
| `AGENTCACHE_SECRET` | — | Bearer token auth on all endpoints |
|
| 350 |
| `AGENT_ID` | — | Default agent ID for scope isolation |
|
| 351 |
+
| `AGENTCACHE_AGENT_SCOPE=isolated` | — | Filters data to current `AGENT_ID` |
|
| 352 |
| `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
|
| 353 |
| `TOKEN_BUDGET` | `2000` | Max tokens in compiled context |
|
| 354 |
| `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph (needs LLM) |
|
| 355 |
| `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
|
| 356 |
+
| `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM observation compression |
|
| 357 |
|
| 358 |
---
|
| 359 |
|
|
|
|
| 381 |
### Setup
|
| 382 |
|
| 383 |
1. Fork this repo as a HuggingFace Space (SDK: Docker)
|
| 384 |
+
2. Create a dataset repo (e.g. `your-username/agentcache-data`)
|
| 385 |
3. Add Space secrets in the HF dashboard:
|
| 386 |
|
| 387 |
| Secret | Value |
|
| 388 |
|--------|-------|
|
| 389 |
| `HF_TOKEN` | Your HF write token |
|
| 390 |
+
| `AGENTCACHE_DATASET_REPO` | `your-username/agentcache-data` |
|
| 391 |
+
| `AGENTCACHE_SECRET` | A random secret (optional but recommended) |
|
| 392 |
| `GEMINI_API_KEY` | Gemini key (optional, enables vector search) |
|
| 393 |
|
| 394 |
+
4. The Space boots, restores `agentcache.db` from the dataset repo, and starts the server
|
| 395 |
|
| 396 |
### How sync works
|
| 397 |
|
|
|
|
| 403 |
|
| 404 |
# Environment for sync
|
| 405 |
HF_TOKEN=...
|
| 406 |
+
AGENTCACHE_DATASET_REPO=username/agentcache-data
|
| 407 |
```
|
| 408 |
|
| 409 |
---
|
|
|
|
| 411 |
## Architecture
|
| 412 |
|
| 413 |
```
|
| 414 |
+
agentcache-python/
|
| 415 |
├── src/
|
| 416 |
│ ├── app.py Flask server — all endpoints, WebSocket broadcaster
|
| 417 |
│ ├── db.py SQLite StateKV — WAL mode, audit_log table
|
|
|
|
| 427 |
|
| 428 |
### Database layout
|
| 429 |
|
| 430 |
+
Two SQLite tables in `~/.agentcache/agentcache.db`:
|
| 431 |
|
| 432 |
```sql
|
| 433 |
-- All data lives here, namespaced by scope
|
|
|
|
| 460 |
|
| 461 |
---
|
| 462 |
|
| 463 |
+
## vs Original agentcache
|
| 464 |
|
| 465 |
+
| | agentcache (Node.js) | agentcache-python |
|
| 466 |
|---|---|---|
|
| 467 |
| Runtime | Node.js 20+ | Python 3.10+ |
|
| 468 |
| Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
|
|
|
|
| 473 |
| Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
|
| 474 |
| Cold boot | ~7s (iii engine warm-up) | <2s |
|
| 475 |
| Database size | ~232MB (417 Dolt chunk files) | ~20MB (single `.db` file) |
|
| 476 |
+
| Setup | `npm install -g @agentcache/agentcache` | `pip install -r requirements.txt` |
|
| 477 |
|
| 478 |
Choose the Python version for: simpler setup, HF Space deployment, single-file database, no Node.js, or Python ecosystem integration.
|
| 479 |
|
agentmemory.md → agentcache.md
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
This file documents the technical details and configurations for automating memory sessions, observations, and prompt-context syncing in environments that lack native client-side hook runners.
|
| 4 |
|
|
@@ -9,7 +9,7 @@ For standard command shells that do not have custom plugins (such as PowerShell)
|
|
| 9 |
* **Profile File**: `D:\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
|
| 10 |
* **Automation Mechanism**:
|
| 11 |
1. **Prompt Redefinition**: Overrides the default PowerShell `prompt` function to retrieve the last executed command from the session history (`Get-History`).
|
| 12 |
-
2. **Asynchronous Dispatch**: Uses PowerShell's `Start-Job` to send the command, execution status, and duration to the Hugging Face Space database (`https://yash030-
|
| 13 |
3. **Interactive Shield**: Wraps the prompts and startup session creation in `if (-not [Console]::IsInputRedirected)` so background script executions or tool runners do not pollute the database.
|
| 14 |
4. **Active Session Reuse**: On console startup, checks for a cached session ID in `current_session.txt` and reuses it to prevent session clutter.
|
| 15 |
|
|
|
|
| 1 |
+
# AgentCache Automation & Client Hook Roundabout
|
| 2 |
|
| 3 |
This file documents the technical details and configurations for automating memory sessions, observations, and prompt-context syncing in environments that lack native client-side hook runners.
|
| 4 |
|
|
|
|
| 9 |
* **Profile File**: `D:\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
|
| 10 |
* **Automation Mechanism**:
|
| 11 |
1. **Prompt Redefinition**: Overrides the default PowerShell `prompt` function to retrieve the last executed command from the session history (`Get-History`).
|
| 12 |
+
2. **Asynchronous Dispatch**: Uses PowerShell's `Start-Job` to send the command, execution status, and duration to the Hugging Face Space database (`https://yash030-agentcache-python.hf.space/agentcache/observe`) in the background, keeping terminal prompts lag-free.
|
| 13 |
3. **Interactive Shield**: Wraps the prompts and startup session creation in `if (-not [Console]::IsInputRedirected)` so background script executions or tool runners do not pollute the database.
|
| 14 |
4. **Active Session Reuse**: On console startup, checks for a cached session ID in `current_session.txt` and reuses it to prevent session clutter.
|
| 15 |
|
docker-compose.yml
CHANGED
|
@@ -22,8 +22,8 @@ services:
|
|
| 22 |
# refactored to register as a sandboxed worker.
|
| 23 |
#
|
| 24 |
# Override per-shell or via .env file:
|
| 25 |
-
#
|
| 26 |
-
image: iiidev/iii:${AGENTMEMORY_III_VERSION:-0.11.2}
|
| 27 |
user: "65532:65532"
|
| 28 |
depends_on:
|
| 29 |
iii-init:
|
|
|
|
| 22 |
# refactored to register as a sandboxed worker.
|
| 23 |
#
|
| 24 |
# Override per-shell or via .env file:
|
| 25 |
+
# AGENTCACHE_III_VERSION=0.11.7 docker compose up
|
| 26 |
+
image: iiidev/iii:${AGENTCACHE_III_VERSION:-${AGENTMEMORY_III_VERSION:-0.11.2}}
|
| 27 |
user: "65532:65532"
|
| 28 |
depends_on:
|
| 29 |
iii-init:
|
pyproject.toml
CHANGED
|
@@ -3,9 +3,9 @@ requires = ["setuptools>=68", "wheel"]
|
|
| 3 |
build-backend = "setuptools.backends.legacy:build"
|
| 4 |
|
| 5 |
[project]
|
| 6 |
-
name = "
|
| 7 |
version = "0.9.8"
|
| 8 |
-
description = "A Python REST + WebSocket + MCP
|
| 9 |
readme = "README.md"
|
| 10 |
license = { text = "MIT" }
|
| 11 |
requires-python = ">=3.10"
|
|
@@ -28,7 +28,7 @@ local-embeddings = [
|
|
| 28 |
]
|
| 29 |
|
| 30 |
[project.scripts]
|
| 31 |
-
|
| 32 |
|
| 33 |
[tool.setuptools.packages.find]
|
| 34 |
where = ["."]
|
|
|
|
| 3 |
build-backend = "setuptools.backends.legacy:build"
|
| 4 |
|
| 5 |
[project]
|
| 6 |
+
name = "agentcache"
|
| 7 |
version = "0.9.8"
|
| 8 |
+
description = "A Python REST + WebSocket + MCP cache server for AI agents, backed by SQLite"
|
| 9 |
readme = "README.md"
|
| 10 |
license = { text = "MIT" }
|
| 11 |
requires-python = ">=3.10"
|
|
|
|
| 28 |
]
|
| 29 |
|
| 30 |
[project.scripts]
|
| 31 |
+
agentcache = "src.cli:main"
|
| 32 |
|
| 33 |
[tool.setuptools.packages.find]
|
| 34 |
where = ["."]
|
src/app.py
CHANGED
|
@@ -18,7 +18,9 @@ if __name__ == "__main__":
|
|
| 18 |
|
| 19 |
|
| 20 |
def _load_env() -> None:
|
| 21 |
-
env_path = os.path.join(os.path.expanduser("~"), ".
|
|
|
|
|
|
|
| 22 |
if not os.path.exists(env_path):
|
| 23 |
return
|
| 24 |
try:
|
|
@@ -47,8 +49,8 @@ def create_app() -> Flask:
|
|
| 47 |
global kv, embedding_provider, persistence
|
| 48 |
|
| 49 |
# Check security credentials
|
| 50 |
-
if not os.getenv("AGENTMEMORY_SECRET"):
|
| 51 |
-
print("[security] WARNING: AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication.")
|
| 52 |
|
| 53 |
import search as search_mod
|
| 54 |
import functions
|
|
@@ -59,10 +61,10 @@ def create_app() -> Flask:
|
|
| 59 |
kv = StateKV()
|
| 60 |
|
| 61 |
# 2. Embedding provider — auto-select by priority (D5.3):
|
| 62 |
-
# GEMINI_API_KEY → OPENAI_API_KEY →
|
| 63 |
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 64 |
openai_key = os.getenv("OPENAI_API_KEY")
|
| 65 |
-
local_model = os.getenv("AGENTMEMORY_LOCAL_EMBEDDING_MODEL")
|
| 66 |
|
| 67 |
if api_key:
|
| 68 |
try:
|
|
@@ -118,7 +120,7 @@ def create_app() -> Flask:
|
|
| 118 |
|
| 119 |
@sock.route("/stream/mem-live/viewer")
|
| 120 |
def stream_viewer(ws):
|
| 121 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 122 |
if secret:
|
| 123 |
token = request.args.get("token") or request.args.get("secret")
|
| 124 |
if not token or not hmac.compare_digest(
|
|
@@ -150,6 +152,7 @@ def create_app() -> Flask:
|
|
| 150 |
|
| 151 |
@flask_app.route("/")
|
| 152 |
@flask_app.route("/viewer")
|
|
|
|
| 153 |
@flask_app.route("/agentmemory/viewer")
|
| 154 |
def serve_viewer():
|
| 155 |
try:
|
|
@@ -161,7 +164,7 @@ def create_app() -> Flask:
|
|
| 161 |
def serve_favicon():
|
| 162 |
return send_from_directory(os.path.join(_base_dir, "viewer"), "favicon.svg")
|
| 163 |
|
| 164 |
-
# 7. CORS after_request — D2.1: configurable via
|
| 165 |
# Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
|
| 166 |
# Wildcard entries like "*.hf.space" match any subdomain via suffix check.
|
| 167 |
_default_cors = (
|
|
@@ -169,7 +172,7 @@ def create_app() -> Flask:
|
|
| 169 |
"https://huggingface.co,https://*.hf.space,"
|
| 170 |
"vscode-webview://*,chrome-extension://*"
|
| 171 |
)
|
| 172 |
-
_cors_origins_raw = os.getenv("AGENTMEMORY_CORS_ORIGINS"
|
| 173 |
|
| 174 |
def _parse_cors_origins(raw: str):
|
| 175 |
"""Return (exact_set, suffix_list) for efficient origin matching."""
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
def _load_env() -> None:
|
| 21 |
+
env_path = os.path.join(os.path.expanduser("~"), ".agentcache", ".env")
|
| 22 |
+
if not os.path.exists(env_path):
|
| 23 |
+
env_path = os.path.join(os.path.expanduser("~"), ".agentmemory", ".env")
|
| 24 |
if not os.path.exists(env_path):
|
| 25 |
return
|
| 26 |
try:
|
|
|
|
| 49 |
global kv, embedding_provider, persistence
|
| 50 |
|
| 51 |
# Check security credentials
|
| 52 |
+
if not os.getenv("AGENTCACHE_SECRET") and not os.getenv("AGENTMEMORY_SECRET"):
|
| 53 |
+
print("[security] WARNING: AGENTCACHE_SECRET/AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication.")
|
| 54 |
|
| 55 |
import search as search_mod
|
| 56 |
import functions
|
|
|
|
| 61 |
kv = StateKV()
|
| 62 |
|
| 63 |
# 2. Embedding provider — auto-select by priority (D5.3):
|
| 64 |
+
# GEMINI_API_KEY → OPENAI_API_KEY → AGENTCACHE_LOCAL_EMBEDDING_MODEL → BM25-only
|
| 65 |
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
| 66 |
openai_key = os.getenv("OPENAI_API_KEY")
|
| 67 |
+
local_model = os.getenv("AGENTCACHE_LOCAL_EMBEDDING_MODEL") or os.getenv("AGENTMEMORY_LOCAL_EMBEDDING_MODEL")
|
| 68 |
|
| 69 |
if api_key:
|
| 70 |
try:
|
|
|
|
| 120 |
|
| 121 |
@sock.route("/stream/mem-live/viewer")
|
| 122 |
def stream_viewer(ws):
|
| 123 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 124 |
if secret:
|
| 125 |
token = request.args.get("token") or request.args.get("secret")
|
| 126 |
if not token or not hmac.compare_digest(
|
|
|
|
| 152 |
|
| 153 |
@flask_app.route("/")
|
| 154 |
@flask_app.route("/viewer")
|
| 155 |
+
@flask_app.route("/agentcache/viewer")
|
| 156 |
@flask_app.route("/agentmemory/viewer")
|
| 157 |
def serve_viewer():
|
| 158 |
try:
|
|
|
|
| 164 |
def serve_favicon():
|
| 165 |
return send_from_directory(os.path.join(_base_dir, "viewer"), "favicon.svg")
|
| 166 |
|
| 167 |
+
# 7. CORS after_request — D2.1: configurable via AGENTCACHE_CORS_ORIGINS env var
|
| 168 |
# Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
|
| 169 |
# Wildcard entries like "*.hf.space" match any subdomain via suffix check.
|
| 170 |
_default_cors = (
|
|
|
|
| 172 |
"https://huggingface.co,https://*.hf.space,"
|
| 173 |
"vscode-webview://*,chrome-extension://*"
|
| 174 |
)
|
| 175 |
+
_cors_origins_raw = os.getenv("AGENTCACHE_CORS_ORIGINS") or os.getenv("AGENTMEMORY_CORS_ORIGINS") or _default_cors
|
| 176 |
|
| 177 |
def _parse_cors_origins(raw: str):
|
| 178 |
"""Return (exact_set, suffix_list) for efficient origin matching."""
|
src/{memory → cache}/__init__.py
RENAMED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
src/
|
| 3 |
|
| 4 |
Sub-modules:
|
| 5 |
observe — folder_observe(), observe(), build_synthetic_compression(), strip_private_data()
|
|
|
|
| 1 |
"""
|
| 2 |
+
src/cache/ — Core business logic package for agentcache-python.
|
| 3 |
|
| 4 |
Sub-modules:
|
| 5 |
observe — folder_observe(), observe(), build_synthetic_compression(), strip_private_data()
|
src/{memory → cache}/context.py
RENAMED
|
File without changes
|
src/{memory → cache}/graph.py
RENAMED
|
File without changes
|
src/{memory → cache}/health.py
RENAMED
|
File without changes
|
src/{memory → cache}/observe.py
RENAMED
|
File without changes
|
src/{memory → cache}/remember.py
RENAMED
|
File without changes
|
src/{memory → cache}/timeline.py
RENAMED
|
File without changes
|
src/cli.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
|
| 4 |
|
| 5 |
Commands:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
"""
|
| 10 |
|
| 11 |
import argparse
|
|
@@ -20,7 +20,7 @@ def cmd_serve(args) -> None:
|
|
| 20 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
from app import create_app
|
| 22 |
flask_app = create_app()
|
| 23 |
-
print(f"[cli] Starting
|
| 24 |
flask_app.run(host=args.host, port=args.port, debug=False)
|
| 25 |
|
| 26 |
|
|
@@ -67,13 +67,13 @@ def cmd_export(args) -> None:
|
|
| 67 |
|
| 68 |
def main() -> None:
|
| 69 |
parser = argparse.ArgumentParser(
|
| 70 |
-
prog="
|
| 71 |
-
description="
|
| 72 |
)
|
| 73 |
subparsers = parser.add_subparsers(dest="command", required=True)
|
| 74 |
|
| 75 |
# serve
|
| 76 |
-
serve_parser = subparsers.add_parser("serve", help="Start the
|
| 77 |
serve_parser.add_argument("--port", type=int, default=int(os.getenv("III_REST_PORT", "3111")))
|
| 78 |
serve_parser.add_argument("--host", default="0.0.0.0")
|
| 79 |
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
agentcache CLI entrypoint.
|
| 4 |
|
| 5 |
Commands:
|
| 6 |
+
agentcache serve [--port PORT] [--host HOST]
|
| 7 |
+
agentcache migrate [--dry-run]
|
| 8 |
+
agentcache export [--output FILE]
|
| 9 |
"""
|
| 10 |
|
| 11 |
import argparse
|
|
|
|
| 20 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
from app import create_app
|
| 22 |
flask_app = create_app()
|
| 23 |
+
print(f"[cli] Starting agentcache on {args.host}:{args.port}")
|
| 24 |
flask_app.run(host=args.host, port=args.port, debug=False)
|
| 25 |
|
| 26 |
|
|
|
|
| 67 |
|
| 68 |
def main() -> None:
|
| 69 |
parser = argparse.ArgumentParser(
|
| 70 |
+
prog="agentcache",
|
| 71 |
+
description="agentcache — AI agent cache server",
|
| 72 |
)
|
| 73 |
subparsers = parser.add_subparsers(dest="command", required=True)
|
| 74 |
|
| 75 |
# serve
|
| 76 |
+
serve_parser = subparsers.add_parser("serve", help="Start the cache server")
|
| 77 |
serve_parser.add_argument("--port", type=int, default=int(os.getenv("III_REST_PORT", "3111")))
|
| 78 |
serve_parser.add_argument("--host", default="0.0.0.0")
|
| 79 |
|
src/connect.py
CHANGED
|
@@ -124,22 +124,22 @@ class ClaudeCodeAdapter:
|
|
| 124 |
next_cfg = existing.copy()
|
| 125 |
servers = next_cfg.get("mcpServers", {})
|
| 126 |
|
| 127 |
-
already_has = "
|
| 128 |
if already_has and not args.force:
|
| 129 |
print(f"[OK] Claude Code already wired in {claude_json}")
|
| 130 |
else:
|
| 131 |
if args.dry_run:
|
| 132 |
-
print(f"[dry-run] Would write mcpServers.
|
| 133 |
else:
|
| 134 |
backup = backup_file(claude_json, "claude-code")
|
| 135 |
if backup:
|
| 136 |
print(f"Backed up configuration to {backup}")
|
| 137 |
|
| 138 |
-
env = {"
|
| 139 |
-
secret = os.environ.get("AGENTMEMORY_SECRET")
|
| 140 |
if secret:
|
| 141 |
-
env["
|
| 142 |
-
servers["
|
| 143 |
"command": sys.executable,
|
| 144 |
"args": [mcp_stdio_path],
|
| 145 |
"env": env
|
|
@@ -179,17 +179,17 @@ class CodexAdapter:
|
|
| 179 |
codex_toml = os.path.join(get_home_dir(), ".codex", "config.toml")
|
| 180 |
mcp_stdio_path = get_mcp_stdio_path()
|
| 181 |
|
| 182 |
-
url = os.environ.get("AGENTMEMORY_URL"
|
| 183 |
-
secret = os.environ.get("AGENTMEMORY_SECRET")
|
| 184 |
toml_block = f"""
|
| 185 |
-
[mcp_servers.
|
| 186 |
command = "{sys.executable.replace('\\', '/')}"
|
| 187 |
args = ["{mcp_stdio_path.replace('\\', '/')}"]
|
| 188 |
-
[mcp_servers.
|
| 189 |
-
|
| 190 |
"""
|
| 191 |
if secret:
|
| 192 |
-
toml_block += f'
|
| 193 |
|
| 194 |
exists = os.path.exists(codex_toml)
|
| 195 |
current = ""
|
|
@@ -197,12 +197,12 @@ AGENTMEMORY_URL = "{url}"
|
|
| 197 |
with open(codex_toml, "r", encoding="utf-8") as f:
|
| 198 |
current = f.read()
|
| 199 |
|
| 200 |
-
wired = "[mcp_servers.
|
| 201 |
if wired and not args.force:
|
| 202 |
print(f"[OK] Codex CLI already wired in {codex_toml}")
|
| 203 |
else:
|
| 204 |
if args.dry_run:
|
| 205 |
-
print(f"[dry-run] Would write [mcp_servers.
|
| 206 |
else:
|
| 207 |
backup = backup_file(codex_toml, "codex", "toml")
|
| 208 |
if backup:
|
|
@@ -216,10 +216,10 @@ AGENTMEMORY_URL = "{url}"
|
|
| 216 |
skipping = False
|
| 217 |
for line in lines:
|
| 218 |
trimmed = line.strip()
|
| 219 |
-
if trimmed == "[mcp_servers.
|
| 220 |
skipping = True
|
| 221 |
continue
|
| 222 |
-
if skipping and trimmed.startswith("[") and trimmed != "[mcp_servers.
|
| 223 |
skipping = False
|
| 224 |
if not skipping:
|
| 225 |
out.append(line)
|
|
@@ -258,7 +258,7 @@ class HermesAdapter:
|
|
| 258 |
return os.path.exists(hermes_dir)
|
| 259 |
|
| 260 |
def install(self, args):
|
| 261 |
-
dest_dir = os.path.join(get_home_dir(), ".hermes", "plugins", "
|
| 262 |
src_dir = os.path.dirname(os.path.abspath(__file__))
|
| 263 |
project_root = os.path.dirname(src_dir)
|
| 264 |
hermes_src = os.path.join(project_root, "integrations", "hermes")
|
|
@@ -277,14 +277,14 @@ class HermesAdapter:
|
|
| 277 |
shutil.rmtree(dest_dir)
|
| 278 |
|
| 279 |
shutil.copytree(hermes_src, dest_dir)
|
| 280 |
-
print(f"[OK] Copied Hermes
|
| 281 |
print("To finish configuration, add to ~/.hermes/config.yaml:")
|
| 282 |
print(" mcp_servers:")
|
| 283 |
-
print("
|
| 284 |
print(" command: python")
|
| 285 |
print(f' args: ["{get_mcp_stdio_path()}"]')
|
| 286 |
-
print("
|
| 287 |
-
print(" provider:
|
| 288 |
|
| 289 |
class AntigravityAdapter:
|
| 290 |
name = "antigravity"
|
|
@@ -310,22 +310,22 @@ class AntigravityAdapter:
|
|
| 310 |
next_cfg = existing.copy()
|
| 311 |
servers = next_cfg.get("mcpServers", {})
|
| 312 |
|
| 313 |
-
already_has = "
|
| 314 |
if already_has and not args.force:
|
| 315 |
print(f"[OK] Antigravity already wired in {mcp_config_path}")
|
| 316 |
else:
|
| 317 |
if args.dry_run:
|
| 318 |
-
print(f"[dry-run] Would write mcpServers.
|
| 319 |
else:
|
| 320 |
backup = backup_file(mcp_config_path, "antigravity")
|
| 321 |
if backup:
|
| 322 |
print(f"Backed up config to {backup}")
|
| 323 |
|
| 324 |
-
env = {"
|
| 325 |
-
secret = os.environ.get("AGENTMEMORY_SECRET")
|
| 326 |
if secret:
|
| 327 |
-
env["
|
| 328 |
-
servers["
|
| 329 |
"command": sys.executable,
|
| 330 |
"args": [mcp_stdio_path],
|
| 331 |
"env": env
|
|
@@ -350,22 +350,22 @@ class KiroAdapter:
|
|
| 350 |
next_cfg = existing.copy()
|
| 351 |
servers = next_cfg.get("mcpServers", {})
|
| 352 |
|
| 353 |
-
already_has = "
|
| 354 |
if already_has and not args.force:
|
| 355 |
print(f"[OK] Kiro already wired in {mcp_config_path}")
|
| 356 |
else:
|
| 357 |
if args.dry_run:
|
| 358 |
-
print(f"[dry-run] Would write mcpServers.
|
| 359 |
else:
|
| 360 |
backup = backup_file(mcp_config_path, "kiro")
|
| 361 |
if backup:
|
| 362 |
print(f"Backed up config to {backup}")
|
| 363 |
|
| 364 |
-
env = {"
|
| 365 |
-
secret = os.environ.get("AGENTMEMORY_SECRET")
|
| 366 |
if secret:
|
| 367 |
-
env["
|
| 368 |
-
servers["
|
| 369 |
"command": sys.executable,
|
| 370 |
"args": [mcp_stdio_path],
|
| 371 |
"env": env
|
|
@@ -383,26 +383,26 @@ class RulesGeneratorAdapter:
|
|
| 383 |
return True
|
| 384 |
|
| 385 |
def install(self, args):
|
| 386 |
-
rule_content = """# Agent
|
| 387 |
|
| 388 |
-
This workspace is integrated with long-term semantic memory via `
|
| 389 |
-
You must act as your own
|
| 390 |
|
| 391 |
## Rules & Workflow
|
| 392 |
|
| 393 |
1. **Initial Search (Prefetch Context)**:
|
| 394 |
-
At the start of every session or new task, immediately call `
|
| 395 |
-
- Example: `
|
| 396 |
|
| 397 |
2. **Lessons & Insights Capture**:
|
| 398 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 399 |
-
- Call `
|
| 400 |
-
- Call `
|
| 401 |
|
| 402 |
3. **Checklist Before Ending**:
|
| 403 |
Before stating a task is complete:
|
| 404 |
- Reflect on whether any lessons learned should be saved.
|
| 405 |
-
- Call `
|
| 406 |
"""
|
| 407 |
cwd = os.getcwd()
|
| 408 |
|
|
@@ -435,7 +435,7 @@ ADAPTERS = [
|
|
| 435 |
]
|
| 436 |
|
| 437 |
def main():
|
| 438 |
-
parser = argparse.ArgumentParser(description="Wired
|
| 439 |
parser.add_argument("agent", nargs="?", choices=[a.name for a in ADAPTERS], help="Specify target agent.")
|
| 440 |
parser.add_argument("--with-hooks", action="store_true", help="Install global workspace hook execution blocks (Claude/Codex).")
|
| 441 |
parser.add_argument("--dry-run", action="store_true", help="Log proposed configuration modifications without writing.")
|
|
|
|
| 124 |
next_cfg = existing.copy()
|
| 125 |
servers = next_cfg.get("mcpServers", {})
|
| 126 |
|
| 127 |
+
already_has = "agentcache" in servers
|
| 128 |
if already_has and not args.force:
|
| 129 |
print(f"[OK] Claude Code already wired in {claude_json}")
|
| 130 |
else:
|
| 131 |
if args.dry_run:
|
| 132 |
+
print(f"[dry-run] Would write mcpServers.agentcache in {claude_json}")
|
| 133 |
else:
|
| 134 |
backup = backup_file(claude_json, "claude-code")
|
| 135 |
if backup:
|
| 136 |
print(f"Backed up configuration to {backup}")
|
| 137 |
|
| 138 |
+
env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
|
| 139 |
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
|
| 140 |
if secret:
|
| 141 |
+
env["AGENTCACHE_SECRET"] = secret
|
| 142 |
+
servers["agentcache"] = {
|
| 143 |
"command": sys.executable,
|
| 144 |
"args": [mcp_stdio_path],
|
| 145 |
"env": env
|
|
|
|
| 179 |
codex_toml = os.path.join(get_home_dir(), ".codex", "config.toml")
|
| 180 |
mcp_stdio_path = get_mcp_stdio_path()
|
| 181 |
|
| 182 |
+
url = os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"
|
| 183 |
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
|
| 184 |
toml_block = f"""
|
| 185 |
+
[mcp_servers.agentcache]
|
| 186 |
command = "{sys.executable.replace('\\', '/')}"
|
| 187 |
args = ["{mcp_stdio_path.replace('\\', '/')}"]
|
| 188 |
+
[mcp_servers.agentcache.env]
|
| 189 |
+
AGENTCACHE_URL = "{url}"
|
| 190 |
"""
|
| 191 |
if secret:
|
| 192 |
+
toml_block += f'AGENTCACHE_SECRET = "{secret}"\n'
|
| 193 |
|
| 194 |
exists = os.path.exists(codex_toml)
|
| 195 |
current = ""
|
|
|
|
| 197 |
with open(codex_toml, "r", encoding="utf-8") as f:
|
| 198 |
current = f.read()
|
| 199 |
|
| 200 |
+
wired = "[mcp_servers.agentcache]" in current
|
| 201 |
if wired and not args.force:
|
| 202 |
print(f"[OK] Codex CLI already wired in {codex_toml}")
|
| 203 |
else:
|
| 204 |
if args.dry_run:
|
| 205 |
+
print(f"[dry-run] Would write [mcp_servers.agentcache] block to {codex_toml}")
|
| 206 |
else:
|
| 207 |
backup = backup_file(codex_toml, "codex", "toml")
|
| 208 |
if backup:
|
|
|
|
| 216 |
skipping = False
|
| 217 |
for line in lines:
|
| 218 |
trimmed = line.strip()
|
| 219 |
+
if trimmed == "[mcp_servers.agentcache]" or trimmed == "[mcp_servers.agentcache.env]":
|
| 220 |
skipping = True
|
| 221 |
continue
|
| 222 |
+
if skipping and trimmed.startswith("[") and trimmed != "[mcp_servers.agentcache.env]":
|
| 223 |
skipping = False
|
| 224 |
if not skipping:
|
| 225 |
out.append(line)
|
|
|
|
| 258 |
return os.path.exists(hermes_dir)
|
| 259 |
|
| 260 |
def install(self, args):
|
| 261 |
+
dest_dir = os.path.join(get_home_dir(), ".hermes", "plugins", "agentcache")
|
| 262 |
src_dir = os.path.dirname(os.path.abspath(__file__))
|
| 263 |
project_root = os.path.dirname(src_dir)
|
| 264 |
hermes_src = os.path.join(project_root, "integrations", "hermes")
|
|
|
|
| 277 |
shutil.rmtree(dest_dir)
|
| 278 |
|
| 279 |
shutil.copytree(hermes_src, dest_dir)
|
| 280 |
+
print(f"[OK] Copied Hermes cache provider plugin to {dest_dir}")
|
| 281 |
print("To finish configuration, add to ~/.hermes/config.yaml:")
|
| 282 |
print(" mcp_servers:")
|
| 283 |
+
print(" agentcache:")
|
| 284 |
print(" command: python")
|
| 285 |
print(f' args: ["{get_mcp_stdio_path()}"]')
|
| 286 |
+
print(" cache:")
|
| 287 |
+
print(" provider: agentcache")
|
| 288 |
|
| 289 |
class AntigravityAdapter:
|
| 290 |
name = "antigravity"
|
|
|
|
| 310 |
next_cfg = existing.copy()
|
| 311 |
servers = next_cfg.get("mcpServers", {})
|
| 312 |
|
| 313 |
+
already_has = "agentcache" in servers
|
| 314 |
if already_has and not args.force:
|
| 315 |
print(f"[OK] Antigravity already wired in {mcp_config_path}")
|
| 316 |
else:
|
| 317 |
if args.dry_run:
|
| 318 |
+
print(f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}")
|
| 319 |
else:
|
| 320 |
backup = backup_file(mcp_config_path, "antigravity")
|
| 321 |
if backup:
|
| 322 |
print(f"Backed up config to {backup}")
|
| 323 |
|
| 324 |
+
env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
|
| 325 |
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
|
| 326 |
if secret:
|
| 327 |
+
env["AGENTCACHE_SECRET"] = secret
|
| 328 |
+
servers["agentcache"] = {
|
| 329 |
"command": sys.executable,
|
| 330 |
"args": [mcp_stdio_path],
|
| 331 |
"env": env
|
|
|
|
| 350 |
next_cfg = existing.copy()
|
| 351 |
servers = next_cfg.get("mcpServers", {})
|
| 352 |
|
| 353 |
+
already_has = "agentcache" in servers
|
| 354 |
if already_has and not args.force:
|
| 355 |
print(f"[OK] Kiro already wired in {mcp_config_path}")
|
| 356 |
else:
|
| 357 |
if args.dry_run:
|
| 358 |
+
print(f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}")
|
| 359 |
else:
|
| 360 |
backup = backup_file(mcp_config_path, "kiro")
|
| 361 |
if backup:
|
| 362 |
print(f"Backed up config to {backup}")
|
| 363 |
|
| 364 |
+
env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
|
| 365 |
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
|
| 366 |
if secret:
|
| 367 |
+
env["AGENTCACHE_SECRET"] = secret
|
| 368 |
+
servers["agentcache"] = {
|
| 369 |
"command": sys.executable,
|
| 370 |
"args": [mcp_stdio_path],
|
| 371 |
"env": env
|
|
|
|
| 383 |
return True
|
| 384 |
|
| 385 |
def install(self, args):
|
| 386 |
+
rule_content = """# Agent Cache Rules
|
| 387 |
|
| 388 |
+
This workspace is integrated with long-term semantic memory via `agentcache-python`.
|
| 389 |
+
You must act as your own cache manager by calling the cache MCP tools at critical boundaries.
|
| 390 |
|
| 391 |
## Rules & Workflow
|
| 392 |
|
| 393 |
1. **Initial Search (Prefetch Context)**:
|
| 394 |
+
At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
|
| 395 |
+
- Example: `cache_smart_search(query="jwt token rotation logic")`
|
| 396 |
|
| 397 |
2. **Lessons & Insights Capture**:
|
| 398 |
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
| 399 |
+
- Call `cache_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
|
| 400 |
+
- Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
| 401 |
|
| 402 |
3. **Checklist Before Ending**:
|
| 403 |
Before stating a task is complete:
|
| 404 |
- Reflect on whether any lessons learned should be saved.
|
| 405 |
+
- Call `cache_reflect` to automatically distribute observations into slots if needed.
|
| 406 |
"""
|
| 407 |
cwd = os.getcwd()
|
| 408 |
|
|
|
|
| 435 |
]
|
| 436 |
|
| 437 |
def main():
|
| 438 |
+
parser = argparse.ArgumentParser(description="Wired agentcache MCP and Hooks into client agents.")
|
| 439 |
parser.add_argument("agent", nargs="?", choices=[a.name for a in ADAPTERS], help="Specify target agent.")
|
| 440 |
parser.add_argument("--with-hooks", action="store_true", help="Install global workspace hook execution blocks (Claude/Codex).")
|
| 441 |
parser.add_argument("--dry-run", action="store_true", help="Log proposed configuration modifications without writing.")
|
src/db.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Dict, Any, List, Optional, TypeVar, Union
|
|
| 8 |
|
| 9 |
T = TypeVar("T")
|
| 10 |
|
| 11 |
-
DB_PATH = os.path.join(os.path.expanduser("~"), ".
|
| 12 |
|
| 13 |
|
| 14 |
class StateKV:
|
|
|
|
| 8 |
|
| 9 |
T = TypeVar("T")
|
| 10 |
|
| 11 |
+
DB_PATH = os.path.join(os.path.expanduser("~"), ".agentcache", "agentcache.db")
|
| 12 |
|
| 13 |
|
| 14 |
class StateKV:
|
src/functions.py
CHANGED
|
@@ -409,10 +409,10 @@ def query_audit(
|
|
| 409 |
# Image Store System
|
| 410 |
# =====================================================================
|
| 411 |
|
| 412 |
-
IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".
|
| 413 |
|
| 414 |
def get_max_bytes() -> int:
|
| 415 |
-
return int(os.getenv("AGENTMEMORY_IMAGE_STORE_MAX_BYTES"
|
| 416 |
|
| 417 |
def is_managed_image_path(file_path: str) -> bool:
|
| 418 |
if not file_path:
|
|
@@ -468,7 +468,7 @@ def delete_image(file_path: Optional[str]) -> int:
|
|
| 468 |
os.remove(file_path)
|
| 469 |
return size
|
| 470 |
except Exception as e:
|
| 471 |
-
print(f"[
|
| 472 |
return 0
|
| 473 |
|
| 474 |
def touch_image(file_path: str) -> None:
|
|
@@ -680,16 +680,16 @@ def commit_if_enabled(kv: StateKV, message: str, agent_id: Optional[str]) -> Opt
|
|
| 680 |
|
| 681 |
|
| 682 |
def is_agent_scope_isolated() -> bool:
|
| 683 |
-
return os.getenv("AGENTMEMORY_AGENT_SCOPE") == "isolated"
|
| 684 |
|
| 685 |
def is_auto_compress_enabled() -> bool:
|
| 686 |
-
return os.getenv("AGENTMEMORY_AUTO_COMPRESS") == "true"
|
| 687 |
|
| 688 |
def is_slots_enabled() -> bool:
|
| 689 |
-
return os.getenv("AGENTMEMORY_SLOTS") == "true"
|
| 690 |
|
| 691 |
def is_reflect_enabled() -> bool:
|
| 692 |
-
return os.getenv("AGENTMEMORY_REFLECT") == "true"
|
| 693 |
|
| 694 |
def is_graph_extraction_enabled() -> bool:
|
| 695 |
return os.getenv("GRAPH_EXTRACTION_ENABLED") == "true"
|
|
@@ -1825,8 +1825,8 @@ def context(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1825 |
|
| 1826 |
blocks.sort(key=lambda b: b.get("recency", 0), reverse=True)
|
| 1827 |
|
| 1828 |
-
header = f'<
|
| 1829 |
-
footer = "</
|
| 1830 |
used_tokens = estimate_tokens(header) + estimate_tokens(footer)
|
| 1831 |
|
| 1832 |
selected = []
|
|
@@ -1949,7 +1949,7 @@ def list_pinned_slots(kv: StateKV, project: Optional[str] = None) -> List[Dict[s
|
|
| 1949 |
def render_pinned_context(slots: List[Dict[str, Any]]) -> str:
|
| 1950 |
if not slots:
|
| 1951 |
return ""
|
| 1952 |
-
lines = ["#
|
| 1953 |
for s in slots:
|
| 1954 |
lines.append(f"## {s['label']}")
|
| 1955 |
lines.append(s["content"].strip())
|
|
@@ -3006,7 +3006,7 @@ def health_check(kv: StateKV) -> Dict[str, Any]:
|
|
| 3006 |
db_size_bytes = 0
|
| 3007 |
wal_size_bytes = 0
|
| 3008 |
try:
|
| 3009 |
-
sync_state_path = os.path.join(os.path.expanduser("~"), ".
|
| 3010 |
if os.path.exists(sync_state_path):
|
| 3011 |
with open(sync_state_path, "r", encoding="utf-8") as _sf:
|
| 3012 |
_sync = json.loads(_sf.read())
|
|
|
|
| 409 |
# Image Store System
|
| 410 |
# =====================================================================
|
| 411 |
|
| 412 |
+
IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentcache", "images")
|
| 413 |
|
| 414 |
def get_max_bytes() -> int:
|
| 415 |
+
return int(os.getenv("AGENTCACHE_IMAGE_STORE_MAX_BYTES") or os.getenv("AGENTMEMORY_IMAGE_STORE_MAX_BYTES") or 500 * 1024 * 1024)
|
| 416 |
|
| 417 |
def is_managed_image_path(file_path: str) -> bool:
|
| 418 |
if not file_path:
|
|
|
|
| 468 |
os.remove(file_path)
|
| 469 |
return size
|
| 470 |
except Exception as e:
|
| 471 |
+
print(f"[agentcache] Failed to delete image context: {e}")
|
| 472 |
return 0
|
| 473 |
|
| 474 |
def touch_image(file_path: str) -> None:
|
|
|
|
| 680 |
|
| 681 |
|
| 682 |
def is_agent_scope_isolated() -> bool:
|
| 683 |
+
return (os.getenv("AGENTCACHE_AGENT_SCOPE") or os.getenv("AGENTMEMORY_AGENT_SCOPE")) == "isolated"
|
| 684 |
|
| 685 |
def is_auto_compress_enabled() -> bool:
|
| 686 |
+
return (os.getenv("AGENTCACHE_AUTO_COMPRESS") or os.getenv("AGENTMEMORY_AUTO_COMPRESS")) == "true"
|
| 687 |
|
| 688 |
def is_slots_enabled() -> bool:
|
| 689 |
+
return (os.getenv("AGENTCACHE_SLOTS") or os.getenv("AGENTMEMORY_SLOTS")) == "true"
|
| 690 |
|
| 691 |
def is_reflect_enabled() -> bool:
|
| 692 |
+
return (os.getenv("AGENTCACHE_REFLECT") or os.getenv("AGENTMEMORY_REFLECT")) == "true"
|
| 693 |
|
| 694 |
def is_graph_extraction_enabled() -> bool:
|
| 695 |
return os.getenv("GRAPH_EXTRACTION_ENABLED") == "true"
|
|
|
|
| 1825 |
|
| 1826 |
blocks.sort(key=lambda b: b.get("recency", 0), reverse=True)
|
| 1827 |
|
| 1828 |
+
header = f'<agentcache-context project="{escape_xml_attr(project)}">'
|
| 1829 |
+
footer = "</agentcache-context>"
|
| 1830 |
used_tokens = estimate_tokens(header) + estimate_tokens(footer)
|
| 1831 |
|
| 1832 |
selected = []
|
|
|
|
| 1949 |
def render_pinned_context(slots: List[Dict[str, Any]]) -> str:
|
| 1950 |
if not slots:
|
| 1951 |
return ""
|
| 1952 |
+
lines = ["# agentcache pinned slots", ""]
|
| 1953 |
for s in slots:
|
| 1954 |
lines.append(f"## {s['label']}")
|
| 1955 |
lines.append(s["content"].strip())
|
|
|
|
| 3006 |
db_size_bytes = 0
|
| 3007 |
wal_size_bytes = 0
|
| 3008 |
try:
|
| 3009 |
+
sync_state_path = os.path.join(os.path.expanduser("~"), ".agentcache", ".sync_state")
|
| 3010 |
if os.path.exists(sync_state_path):
|
| 3011 |
with open(sync_state_path, "r", encoding="utf-8") as _sf:
|
| 3012 |
_sync = json.loads(_sf.read())
|
src/import_data.py
CHANGED
|
@@ -53,7 +53,7 @@ def import_old_data(old_db_path: str, kv: StateKV) -> bool:
|
|
| 53 |
|
| 54 |
if count > 0:
|
| 55 |
print("[import] Creating Dolt version commit...")
|
| 56 |
-
kv.commit_version("Import legacy
|
| 57 |
|
| 58 |
print(f"[import] Finished! Migrated {count} total records into Dolt SQL.")
|
| 59 |
return True
|
|
@@ -61,8 +61,8 @@ def import_old_data(old_db_path: str, kv: StateKV) -> bool:
|
|
| 61 |
|
| 62 |
if __name__ == "__main__":
|
| 63 |
# Prioritize the restored HF data in the user's home folder, falling back to local workspace data
|
| 64 |
-
home_path = os.path.expanduser(os.path.join("~", ".
|
| 65 |
-
workspace_path = r"d:\Downloads\Projects\Other Projects\
|
| 66 |
|
| 67 |
if os.path.exists(home_path):
|
| 68 |
default_path = home_path
|
|
|
|
| 53 |
|
| 54 |
if count > 0:
|
| 55 |
print("[import] Creating Dolt version commit...")
|
| 56 |
+
kv.commit_version("Import legacy AgentCache database from Hugging Face", "system")
|
| 57 |
|
| 58 |
print(f"[import] Finished! Migrated {count} total records into Dolt SQL.")
|
| 59 |
return True
|
|
|
|
| 61 |
|
| 62 |
if __name__ == "__main__":
|
| 63 |
# Prioritize the restored HF data in the user's home folder, falling back to local workspace data
|
| 64 |
+
home_path = os.path.expanduser(os.path.join("~", ".agentcache", "state_store.db"))
|
| 65 |
+
workspace_path = r"d:\Downloads\Projects\Other Projects\agentcache\agentcache\data\state_store.db"
|
| 66 |
|
| 67 |
if os.path.exists(home_path):
|
| 68 |
default_path = home_path
|
src/mcp_stdio.py
CHANGED
|
@@ -13,11 +13,11 @@ import json
|
|
| 13 |
import os
|
| 14 |
import requests
|
| 15 |
|
| 16 |
-
BASE = os.getenv("AGENTMEMORY_URL"
|
| 17 |
-
if not BASE.endswith("/agentmemory"):
|
| 18 |
-
BASE = f"{BASE}/
|
| 19 |
|
| 20 |
-
_secret = os.getenv("AGENTMEMORY_SECRET")
|
| 21 |
|
| 22 |
|
| 23 |
def headers():
|
|
@@ -44,7 +44,7 @@ def handle(req):
|
|
| 44 |
"result": {
|
| 45 |
"protocolVersion": "2024-11-05",
|
| 46 |
"capabilities": {"tools": {}},
|
| 47 |
-
"serverInfo": {"name": "
|
| 48 |
}
|
| 49 |
})
|
| 50 |
|
|
|
|
| 13 |
import os
|
| 14 |
import requests
|
| 15 |
|
| 16 |
+
BASE = (os.getenv("AGENTCACHE_URL") or os.getenv("AGENTMEMORY_URL") or "http://127.0.0.1:3111").rstrip("/")
|
| 17 |
+
if not BASE.endswith("/agentcache") and not BASE.endswith("/agentmemory"):
|
| 18 |
+
BASE = f"{BASE}/agentcache"
|
| 19 |
|
| 20 |
+
_secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 21 |
|
| 22 |
|
| 23 |
def headers():
|
|
|
|
| 44 |
"result": {
|
| 45 |
"protocolVersion": "2024-11-05",
|
| 46 |
"capabilities": {"tools": {}},
|
| 47 |
+
"serverInfo": {"name": "agentcache-local", "version": "0.9.8"}
|
| 48 |
}
|
| 49 |
})
|
| 50 |
|
src/routes/graph.py
CHANGED
|
@@ -18,7 +18,7 @@ graph_bp = Blueprint("graph", __name__)
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -36,9 +36,10 @@ def _get_kv():
|
|
| 36 |
|
| 37 |
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
-
# GET /
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
|
|
|
| 42 |
@graph_bp.route("/agentmemory/graph", methods=["GET"])
|
| 43 |
def api_graph():
|
| 44 |
auth_err = _check_auth()
|
|
@@ -49,9 +50,10 @@ def api_graph():
|
|
| 49 |
|
| 50 |
|
| 51 |
# ---------------------------------------------------------------------------
|
| 52 |
-
# GET /
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
|
|
|
|
| 55 |
@graph_bp.route("/agentmemory/graph/stats", methods=["GET"])
|
| 56 |
def api_graph_stats():
|
| 57 |
auth_err = _check_auth()
|
|
@@ -67,9 +69,10 @@ def api_graph_stats():
|
|
| 67 |
|
| 68 |
|
| 69 |
# ---------------------------------------------------------------------------
|
| 70 |
-
# POST /
|
| 71 |
# ---------------------------------------------------------------------------
|
| 72 |
|
|
|
|
| 73 |
@graph_bp.route("/agentmemory/graph/query", methods=["POST"])
|
| 74 |
def api_graph_query():
|
| 75 |
auth_err = _check_auth()
|
|
@@ -84,9 +87,10 @@ def api_graph_query():
|
|
| 84 |
|
| 85 |
|
| 86 |
# ---------------------------------------------------------------------------
|
| 87 |
-
# POST /
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
|
|
|
|
| 90 |
@graph_bp.route("/agentmemory/graph/build", methods=["POST"])
|
| 91 |
def api_graph_build():
|
| 92 |
auth_err = _check_auth()
|
|
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
+
# GET /agentcache/graph
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
+
@graph_bp.route("/agentcache/graph", methods=["GET"])
|
| 43 |
@graph_bp.route("/agentmemory/graph", methods=["GET"])
|
| 44 |
def api_graph():
|
| 45 |
auth_err = _check_auth()
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
# ---------------------------------------------------------------------------
|
| 53 |
+
# GET /agentcache/graph/stats
|
| 54 |
# ---------------------------------------------------------------------------
|
| 55 |
|
| 56 |
+
@graph_bp.route("/agentcache/graph/stats", methods=["GET"])
|
| 57 |
@graph_bp.route("/agentmemory/graph/stats", methods=["GET"])
|
| 58 |
def api_graph_stats():
|
| 59 |
auth_err = _check_auth()
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
# ---------------------------------------------------------------------------
|
| 72 |
+
# POST /agentcache/graph/query
|
| 73 |
# ---------------------------------------------------------------------------
|
| 74 |
|
| 75 |
+
@graph_bp.route("/agentcache/graph/query", methods=["POST"])
|
| 76 |
@graph_bp.route("/agentmemory/graph/query", methods=["POST"])
|
| 77 |
def api_graph_query():
|
| 78 |
auth_err = _check_auth()
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
# ---------------------------------------------------------------------------
|
| 90 |
+
# POST /agentcache/graph/build
|
| 91 |
# ---------------------------------------------------------------------------
|
| 92 |
|
| 93 |
+
@graph_bp.route("/agentcache/graph/build", methods=["POST"])
|
| 94 |
@graph_bp.route("/agentmemory/graph/build", methods=["POST"])
|
| 95 |
def api_graph_build():
|
| 96 |
auth_err = _check_auth()
|
src/routes/health.py
CHANGED
|
@@ -18,7 +18,7 @@ health_bp = Blueprint("health", __name__)
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -41,33 +41,36 @@ def _get_embedding_provider():
|
|
| 41 |
|
| 42 |
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
-
# GET /
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
|
|
|
|
| 47 |
@health_bp.route("/agentmemory/livez", methods=["GET"])
|
| 48 |
def livez():
|
| 49 |
port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
|
| 50 |
return jsonify({
|
| 51 |
"status": "ok",
|
| 52 |
-
"service": "
|
| 53 |
"viewerPort": port,
|
| 54 |
"viewerSkipped": False,
|
| 55 |
})
|
| 56 |
|
| 57 |
|
| 58 |
# ---------------------------------------------------------------------------
|
| 59 |
-
# GET /
|
| 60 |
# ---------------------------------------------------------------------------
|
| 61 |
|
|
|
|
| 62 |
@health_bp.route("/agentmemory/health", methods=["GET"])
|
| 63 |
def health():
|
| 64 |
return jsonify(functions.health_check(_get_kv()))
|
| 65 |
|
| 66 |
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
-
# GET /
|
| 69 |
# ---------------------------------------------------------------------------
|
| 70 |
|
|
|
|
| 71 |
@health_bp.route("/agentmemory/audit", methods=["GET"])
|
| 72 |
def api_audit():
|
| 73 |
auth_err = _check_auth()
|
|
@@ -81,9 +84,10 @@ def api_audit():
|
|
| 81 |
|
| 82 |
|
| 83 |
# ---------------------------------------------------------------------------
|
| 84 |
-
# GET /
|
| 85 |
# ---------------------------------------------------------------------------
|
| 86 |
|
|
|
|
| 87 |
@health_bp.route("/agentmemory/config/flags", methods=["GET"])
|
| 88 |
def config_flags():
|
| 89 |
auth_err = _check_auth()
|
|
@@ -118,14 +122,14 @@ def config_flags():
|
|
| 118 |
"docsHref": "https://github.com/rohitg00/agentmemory#consolidation",
|
| 119 |
},
|
| 120 |
{
|
| 121 |
-
"key": "
|
| 122 |
"label": "LLM-powered observation compression",
|
| 123 |
"enabled": functions.is_auto_compress_enabled(),
|
| 124 |
"default": False,
|
| 125 |
"affects": ["Memories", "Timeline"],
|
| 126 |
"needsLlm": True,
|
| 127 |
"description": "Every observation is compressed by the LLM for richer summaries. OFF uses synthetic compression.",
|
| 128 |
-
"enableHow": "Set
|
| 129 |
"docsHref": "https://github.com/rohitg00/agentmemory/issues/138",
|
| 130 |
},
|
| 131 |
]
|
|
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
+
# GET /agentcache/livez (no auth required)
|
| 45 |
# ---------------------------------------------------------------------------
|
| 46 |
|
| 47 |
+
@health_bp.route("/agentcache/livez", methods=["GET"])
|
| 48 |
@health_bp.route("/agentmemory/livez", methods=["GET"])
|
| 49 |
def livez():
|
| 50 |
port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
|
| 51 |
return jsonify({
|
| 52 |
"status": "ok",
|
| 53 |
+
"service": "agentcache",
|
| 54 |
"viewerPort": port,
|
| 55 |
"viewerSkipped": False,
|
| 56 |
})
|
| 57 |
|
| 58 |
|
| 59 |
# ---------------------------------------------------------------------------
|
| 60 |
+
# GET /agentcache/health
|
| 61 |
# ---------------------------------------------------------------------------
|
| 62 |
|
| 63 |
+
@health_bp.route("/agentcache/health", methods=["GET"])
|
| 64 |
@health_bp.route("/agentmemory/health", methods=["GET"])
|
| 65 |
def health():
|
| 66 |
return jsonify(functions.health_check(_get_kv()))
|
| 67 |
|
| 68 |
|
| 69 |
# ---------------------------------------------------------------------------
|
| 70 |
+
# GET /agentcache/audit
|
| 71 |
# ---------------------------------------------------------------------------
|
| 72 |
|
| 73 |
+
@health_bp.route("/agentcache/audit", methods=["GET"])
|
| 74 |
@health_bp.route("/agentmemory/audit", methods=["GET"])
|
| 75 |
def api_audit():
|
| 76 |
auth_err = _check_auth()
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
# ---------------------------------------------------------------------------
|
| 87 |
+
# GET /agentcache/config/flags
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
|
| 90 |
+
@health_bp.route("/agentcache/config/flags", methods=["GET"])
|
| 91 |
@health_bp.route("/agentmemory/config/flags", methods=["GET"])
|
| 92 |
def config_flags():
|
| 93 |
auth_err = _check_auth()
|
|
|
|
| 122 |
"docsHref": "https://github.com/rohitg00/agentmemory#consolidation",
|
| 123 |
},
|
| 124 |
{
|
| 125 |
+
"key": "AGENTCACHE_AUTO_COMPRESS",
|
| 126 |
"label": "LLM-powered observation compression",
|
| 127 |
"enabled": functions.is_auto_compress_enabled(),
|
| 128 |
"default": False,
|
| 129 |
"affects": ["Memories", "Timeline"],
|
| 130 |
"needsLlm": True,
|
| 131 |
"description": "Every observation is compressed by the LLM for richer summaries. OFF uses synthetic compression.",
|
| 132 |
+
"enableHow": "Set AGENTCACHE_AUTO_COMPRESS=true.",
|
| 133 |
"docsHref": "https://github.com/rohitg00/agentmemory/issues/138",
|
| 134 |
},
|
| 135 |
]
|
src/routes/mcp.py
CHANGED
|
@@ -18,7 +18,7 @@ mcp_bp = Blueprint("mcp", __name__)
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -49,9 +49,10 @@ def _parse_mcp_list_arg(arg_val):
|
|
| 49 |
|
| 50 |
|
| 51 |
# ---------------------------------------------------------------------------
|
| 52 |
-
# GET /
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
|
|
|
|
| 55 |
@mcp_bp.route("/agentmemory/mcp/tools", methods=["GET"])
|
| 56 |
def mcp_tools_list():
|
| 57 |
auth_err = _check_auth()
|
|
@@ -60,7 +61,7 @@ def mcp_tools_list():
|
|
| 60 |
|
| 61 |
tools = [
|
| 62 |
{
|
| 63 |
-
"name": "
|
| 64 |
"description": "Search past folder observations and global memories. Use when you need to recall what happened in a folder.",
|
| 65 |
"inputSchema": {
|
| 66 |
"type": "object",
|
|
@@ -74,7 +75,7 @@ def mcp_tools_list():
|
|
| 74 |
},
|
| 75 |
},
|
| 76 |
{
|
| 77 |
-
"name": "
|
| 78 |
"description": "Hybrid semantic+keyword search across folder observations and global memories.",
|
| 79 |
"inputSchema": {
|
| 80 |
"type": "object",
|
|
@@ -88,7 +89,7 @@ def mcp_tools_list():
|
|
| 88 |
},
|
| 89 |
},
|
| 90 |
{
|
| 91 |
-
"name": "
|
| 92 |
"description": "Explicitly save an important insight, decision, or pattern to long-term memory.",
|
| 93 |
"inputSchema": {
|
| 94 |
"type": "object",
|
|
@@ -113,12 +114,12 @@ def mcp_tools_list():
|
|
| 113 |
},
|
| 114 |
},
|
| 115 |
{
|
| 116 |
-
"name": "
|
| 117 |
"description": "Health check — returns folder, agent, observation and memory counts.",
|
| 118 |
"inputSchema": {"type": "object", "properties": {}},
|
| 119 |
},
|
| 120 |
{
|
| 121 |
-
"name": "
|
| 122 |
"description": "Delete a global memory or all observations for a (folderPath, agentId) pair.",
|
| 123 |
"inputSchema": {
|
| 124 |
"type": "object",
|
|
@@ -136,7 +137,7 @@ def mcp_tools_list():
|
|
| 136 |
},
|
| 137 |
},
|
| 138 |
{
|
| 139 |
-
"name": "
|
| 140 |
"description": "Export all folder observations and global memories as JSON (v2 format).",
|
| 141 |
"inputSchema": {"type": "object", "properties": {}},
|
| 142 |
},
|
|
@@ -161,7 +162,7 @@ def mcp_tools_list():
|
|
| 161 |
},
|
| 162 |
},
|
| 163 |
{
|
| 164 |
-
"name": "
|
| 165 |
"description": "Explicitly save a key insight, fact, or architecture decision to long-term memory.",
|
| 166 |
"inputSchema": {
|
| 167 |
"type": "object",
|
|
@@ -187,12 +188,12 @@ def mcp_tools_list():
|
|
| 187 |
},
|
| 188 |
},
|
| 189 |
{
|
| 190 |
-
"name": "
|
| 191 |
"description": "List all (folder, agent) pairs that have memory observations.",
|
| 192 |
"inputSchema": {"type": "object", "properties": {}},
|
| 193 |
},
|
| 194 |
{
|
| 195 |
-
"name": "
|
| 196 |
"description": "Get all observations for a specific (folderPath, agentId) pair.",
|
| 197 |
"inputSchema": {
|
| 198 |
"type": "object",
|
|
@@ -204,7 +205,7 @@ def mcp_tools_list():
|
|
| 204 |
},
|
| 205 |
},
|
| 206 |
{
|
| 207 |
-
"name": "
|
| 208 |
"description": "Get folder activity feed — observations sorted by time, filterable by folder/agent.",
|
| 209 |
"inputSchema": {
|
| 210 |
"type": "object",
|
|
@@ -218,7 +219,7 @@ def mcp_tools_list():
|
|
| 218 |
},
|
| 219 |
},
|
| 220 |
{
|
| 221 |
-
"name": "
|
| 222 |
"description": "Remove duplicate observations from a (folderPath, agentId) pair or all pairs. Keeps the earliest observation per unique text fingerprint.",
|
| 223 |
"inputSchema": {
|
| 224 |
"type": "object",
|
|
@@ -233,9 +234,10 @@ def mcp_tools_list():
|
|
| 233 |
|
| 234 |
|
| 235 |
# ---------------------------------------------------------------------------
|
| 236 |
-
# POST /
|
| 237 |
# ---------------------------------------------------------------------------
|
| 238 |
|
|
|
|
| 239 |
@mcp_bp.route("/agentmemory/mcp/tools", methods=["POST"])
|
| 240 |
def mcp_tools_call():
|
| 241 |
auth_err = _check_auth()
|
|
@@ -253,7 +255,7 @@ def mcp_tools_call():
|
|
| 253 |
print(f"[mcp] Calling tool {name} with args: {args}")
|
| 254 |
text_out = ""
|
| 255 |
|
| 256 |
-
if name
|
| 257 |
q = args.get("query")
|
| 258 |
limit = int(args.get("limit") or 10)
|
| 259 |
folder_path = args.get("folderPath")
|
|
@@ -267,7 +269,7 @@ def mcp_tools_call():
|
|
| 267 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 268 |
text_out = json.dumps(res, indent=2)
|
| 269 |
|
| 270 |
-
elif name
|
| 271 |
content = args.get("content")
|
| 272 |
concepts = _parse_mcp_list_arg(args.get("concepts"))
|
| 273 |
files = _parse_mcp_list_arg(args.get("files"))
|
|
@@ -281,7 +283,7 @@ def mcp_tools_call():
|
|
| 281 |
})
|
| 282 |
text_out = json.dumps(res)
|
| 283 |
|
| 284 |
-
elif name
|
| 285 |
q = args.get("query")
|
| 286 |
limit = int(args.get("limit") or 10)
|
| 287 |
folder_path = args.get("folderPath")
|
|
@@ -295,11 +297,11 @@ def mcp_tools_call():
|
|
| 295 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 296 |
text_out = json.dumps(res, indent=2)
|
| 297 |
|
| 298 |
-
elif name
|
| 299 |
res = functions.health_check(kv)
|
| 300 |
text_out = json.dumps(res, indent=2)
|
| 301 |
|
| 302 |
-
elif name
|
| 303 |
obs_ids = _parse_mcp_list_arg(args.get("observationIds"))
|
| 304 |
res = functions.forget(kv, {
|
| 305 |
"memoryId": args.get("memoryId"),
|
|
@@ -309,7 +311,7 @@ def mcp_tools_call():
|
|
| 309 |
})
|
| 310 |
text_out = json.dumps(res, indent=2)
|
| 311 |
|
| 312 |
-
elif name
|
| 313 |
res = functions.export_data(kv, {})
|
| 314 |
text_out = json.dumps(res, indent=2)
|
| 315 |
|
|
@@ -325,6 +327,7 @@ def mcp_tools_call():
|
|
| 325 |
folder_path = (
|
| 326 |
args.get("cwd")
|
| 327 |
or args.get("project")
|
|
|
|
| 328 |
or os.getenv("AGENTMEMORY_CWD")
|
| 329 |
or "/unknown"
|
| 330 |
)
|
|
@@ -361,7 +364,7 @@ def mcp_tools_call():
|
|
| 361 |
res = functions.folder_observe(kv, payload)
|
| 362 |
text_out = json.dumps(res)
|
| 363 |
|
| 364 |
-
elif name
|
| 365 |
agent_id = args.get("agentId") or functions.get_agent_id() or "agent"
|
| 366 |
content = args.get("content")
|
| 367 |
project = args.get("project")
|
|
@@ -389,7 +392,7 @@ def mcp_tools_call():
|
|
| 389 |
res = functions.remember(kv, payload)
|
| 390 |
text_out = json.dumps(res)
|
| 391 |
|
| 392 |
-
elif name
|
| 393 |
pairs = sorted(
|
| 394 |
kv.list(KV.folders),
|
| 395 |
key=lambda x: x.get("lastUpdated", ""),
|
|
@@ -401,7 +404,7 @@ def mcp_tools_call():
|
|
| 401 |
pairs = [p for p in pairs if p.get("agentId") == aid]
|
| 402 |
text_out = json.dumps(pairs, indent=2)
|
| 403 |
|
| 404 |
-
elif name
|
| 405 |
fp = args.get("folderPath", "")
|
| 406 |
aid = args.get("agentId", "")
|
| 407 |
if not fp or not aid:
|
|
@@ -417,7 +420,7 @@ def mcp_tools_call():
|
|
| 417 |
)
|
| 418 |
text_out = json.dumps(obs, indent=2)
|
| 419 |
|
| 420 |
-
elif name
|
| 421 |
request_aid = args.get("agentId")
|
| 422 |
if functions.is_agent_scope_isolated():
|
| 423 |
current_aid = functions.get_agent_id()
|
|
@@ -435,7 +438,7 @@ def mcp_tools_call():
|
|
| 435 |
)
|
| 436 |
text_out = json.dumps(res, indent=2)
|
| 437 |
|
| 438 |
-
elif name
|
| 439 |
res = functions.dedup_folder_observations(
|
| 440 |
kv,
|
| 441 |
args.get("folderPath") or None,
|
|
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
# ---------------------------------------------------------------------------
|
| 52 |
+
# GET /agentcache/mcp/tools
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
|
| 55 |
+
@mcp_bp.route("/agentcache/mcp/tools", methods=["GET"])
|
| 56 |
@mcp_bp.route("/agentmemory/mcp/tools", methods=["GET"])
|
| 57 |
def mcp_tools_list():
|
| 58 |
auth_err = _check_auth()
|
|
|
|
| 61 |
|
| 62 |
tools = [
|
| 63 |
{
|
| 64 |
+
"name": "cache_recall",
|
| 65 |
"description": "Search past folder observations and global memories. Use when you need to recall what happened in a folder.",
|
| 66 |
"inputSchema": {
|
| 67 |
"type": "object",
|
|
|
|
| 75 |
},
|
| 76 |
},
|
| 77 |
{
|
| 78 |
+
"name": "cache_smart_search",
|
| 79 |
"description": "Hybrid semantic+keyword search across folder observations and global memories.",
|
| 80 |
"inputSchema": {
|
| 81 |
"type": "object",
|
|
|
|
| 89 |
},
|
| 90 |
},
|
| 91 |
{
|
| 92 |
+
"name": "cache_save",
|
| 93 |
"description": "Explicitly save an important insight, decision, or pattern to long-term memory.",
|
| 94 |
"inputSchema": {
|
| 95 |
"type": "object",
|
|
|
|
| 114 |
},
|
| 115 |
},
|
| 116 |
{
|
| 117 |
+
"name": "cache_diagnose",
|
| 118 |
"description": "Health check — returns folder, agent, observation and memory counts.",
|
| 119 |
"inputSchema": {"type": "object", "properties": {}},
|
| 120 |
},
|
| 121 |
{
|
| 122 |
+
"name": "cache_forget",
|
| 123 |
"description": "Delete a global memory or all observations for a (folderPath, agentId) pair.",
|
| 124 |
"inputSchema": {
|
| 125 |
"type": "object",
|
|
|
|
| 137 |
},
|
| 138 |
},
|
| 139 |
{
|
| 140 |
+
"name": "cache_export",
|
| 141 |
"description": "Export all folder observations and global memories as JSON (v2 format).",
|
| 142 |
"inputSchema": {"type": "object", "properties": {}},
|
| 143 |
},
|
|
|
|
| 162 |
},
|
| 163 |
},
|
| 164 |
{
|
| 165 |
+
"name": "agent_cache",
|
| 166 |
"description": "Explicitly save a key insight, fact, or architecture decision to long-term memory.",
|
| 167 |
"inputSchema": {
|
| 168 |
"type": "object",
|
|
|
|
| 188 |
},
|
| 189 |
},
|
| 190 |
{
|
| 191 |
+
"name": "cache_folders",
|
| 192 |
"description": "List all (folder, agent) pairs that have memory observations.",
|
| 193 |
"inputSchema": {"type": "object", "properties": {}},
|
| 194 |
},
|
| 195 |
{
|
| 196 |
+
"name": "cache_folder_observations",
|
| 197 |
"description": "Get all observations for a specific (folderPath, agentId) pair.",
|
| 198 |
"inputSchema": {
|
| 199 |
"type": "object",
|
|
|
|
| 205 |
},
|
| 206 |
},
|
| 207 |
{
|
| 208 |
+
"name": "cache_timeline",
|
| 209 |
"description": "Get folder activity feed — observations sorted by time, filterable by folder/agent.",
|
| 210 |
"inputSchema": {
|
| 211 |
"type": "object",
|
|
|
|
| 219 |
},
|
| 220 |
},
|
| 221 |
{
|
| 222 |
+
"name": "cache_dedup",
|
| 223 |
"description": "Remove duplicate observations from a (folderPath, agentId) pair or all pairs. Keeps the earliest observation per unique text fingerprint.",
|
| 224 |
"inputSchema": {
|
| 225 |
"type": "object",
|
|
|
|
| 234 |
|
| 235 |
|
| 236 |
# ---------------------------------------------------------------------------
|
| 237 |
+
# POST /agentcache/mcp/tools
|
| 238 |
# ---------------------------------------------------------------------------
|
| 239 |
|
| 240 |
+
@mcp_bp.route("/agentcache/mcp/tools", methods=["POST"])
|
| 241 |
@mcp_bp.route("/agentmemory/mcp/tools", methods=["POST"])
|
| 242 |
def mcp_tools_call():
|
| 243 |
auth_err = _check_auth()
|
|
|
|
| 255 |
print(f"[mcp] Calling tool {name} with args: {args}")
|
| 256 |
text_out = ""
|
| 257 |
|
| 258 |
+
if name in ("cache_recall", "memory_recall"):
|
| 259 |
q = args.get("query")
|
| 260 |
limit = int(args.get("limit") or 10)
|
| 261 |
folder_path = args.get("folderPath")
|
|
|
|
| 269 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 270 |
text_out = json.dumps(res, indent=2)
|
| 271 |
|
| 272 |
+
elif name in ("cache_save", "memory_save"):
|
| 273 |
content = args.get("content")
|
| 274 |
concepts = _parse_mcp_list_arg(args.get("concepts"))
|
| 275 |
files = _parse_mcp_list_arg(args.get("files"))
|
|
|
|
| 283 |
})
|
| 284 |
text_out = json.dumps(res)
|
| 285 |
|
| 286 |
+
elif name in ("cache_smart_search", "memory_smart_search"):
|
| 287 |
q = args.get("query")
|
| 288 |
limit = int(args.get("limit") or 10)
|
| 289 |
folder_path = args.get("folderPath")
|
|
|
|
| 297 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 298 |
text_out = json.dumps(res, indent=2)
|
| 299 |
|
| 300 |
+
elif name in ("cache_diagnose", "memory_diagnose"):
|
| 301 |
res = functions.health_check(kv)
|
| 302 |
text_out = json.dumps(res, indent=2)
|
| 303 |
|
| 304 |
+
elif name in ("cache_forget", "memory_forget"):
|
| 305 |
obs_ids = _parse_mcp_list_arg(args.get("observationIds"))
|
| 306 |
res = functions.forget(kv, {
|
| 307 |
"memoryId": args.get("memoryId"),
|
|
|
|
| 311 |
})
|
| 312 |
text_out = json.dumps(res, indent=2)
|
| 313 |
|
| 314 |
+
elif name in ("cache_export", "memory_export"):
|
| 315 |
res = functions.export_data(kv, {})
|
| 316 |
text_out = json.dumps(res, indent=2)
|
| 317 |
|
|
|
|
| 327 |
folder_path = (
|
| 328 |
args.get("cwd")
|
| 329 |
or args.get("project")
|
| 330 |
+
or os.getenv("AGENTCACHE_CWD")
|
| 331 |
or os.getenv("AGENTMEMORY_CWD")
|
| 332 |
or "/unknown"
|
| 333 |
)
|
|
|
|
| 364 |
res = functions.folder_observe(kv, payload)
|
| 365 |
text_out = json.dumps(res)
|
| 366 |
|
| 367 |
+
elif name in ("agent_cache", "agent_remember"):
|
| 368 |
agent_id = args.get("agentId") or functions.get_agent_id() or "agent"
|
| 369 |
content = args.get("content")
|
| 370 |
project = args.get("project")
|
|
|
|
| 392 |
res = functions.remember(kv, payload)
|
| 393 |
text_out = json.dumps(res)
|
| 394 |
|
| 395 |
+
elif name in ("cache_folders", "memory_folders"):
|
| 396 |
pairs = sorted(
|
| 397 |
kv.list(KV.folders),
|
| 398 |
key=lambda x: x.get("lastUpdated", ""),
|
|
|
|
| 404 |
pairs = [p for p in pairs if p.get("agentId") == aid]
|
| 405 |
text_out = json.dumps(pairs, indent=2)
|
| 406 |
|
| 407 |
+
elif name in ("cache_folder_observations", "memory_folder_observations"):
|
| 408 |
fp = args.get("folderPath", "")
|
| 409 |
aid = args.get("agentId", "")
|
| 410 |
if not fp or not aid:
|
|
|
|
| 420 |
)
|
| 421 |
text_out = json.dumps(obs, indent=2)
|
| 422 |
|
| 423 |
+
elif name in ("cache_timeline", "memory_timeline"):
|
| 424 |
request_aid = args.get("agentId")
|
| 425 |
if functions.is_agent_scope_isolated():
|
| 426 |
current_aid = functions.get_agent_id()
|
|
|
|
| 438 |
)
|
| 439 |
text_out = json.dumps(res, indent=2)
|
| 440 |
|
| 441 |
+
elif name in ("cache_dedup", "memory_dedup"):
|
| 442 |
res = functions.dedup_folder_observations(
|
| 443 |
kv,
|
| 444 |
args.get("folderPath") or None,
|
src/routes/memories.py
CHANGED
|
@@ -18,7 +18,7 @@ memories_bp = Blueprint("memories", __name__)
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -39,6 +39,7 @@ def _get_kv():
|
|
| 39 |
# POST /agentmemory/remember
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
|
|
|
| 42 |
@memories_bp.route("/agentmemory/remember", methods=["POST"])
|
| 43 |
def api_remember():
|
| 44 |
auth_err = _check_auth()
|
|
@@ -57,6 +58,7 @@ def api_remember():
|
|
| 57 |
# POST /agentmemory/agent/remember
|
| 58 |
# ---------------------------------------------------------------------------
|
| 59 |
|
|
|
|
| 60 |
@memories_bp.route("/agentmemory/agent/remember", methods=["POST"])
|
| 61 |
def api_agent_remember():
|
| 62 |
auth_err = _check_auth()
|
|
@@ -101,6 +103,7 @@ def api_agent_remember():
|
|
| 101 |
# GET /agentmemory/memories
|
| 102 |
# ---------------------------------------------------------------------------
|
| 103 |
|
|
|
|
| 104 |
@memories_bp.route("/agentmemory/memories", methods=["GET"])
|
| 105 |
def api_memories_list():
|
| 106 |
auth_err = _check_auth()
|
|
@@ -120,6 +123,7 @@ def api_memories_list():
|
|
| 120 |
# POST /agentmemory/forget
|
| 121 |
# ---------------------------------------------------------------------------
|
| 122 |
|
|
|
|
| 123 |
@memories_bp.route("/agentmemory/forget", methods=["POST"])
|
| 124 |
def api_forget():
|
| 125 |
auth_err = _check_auth()
|
|
|
|
| 18 |
|
| 19 |
def _check_auth():
|
| 20 |
import hmac
|
| 21 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 22 |
if not secret:
|
| 23 |
return None
|
| 24 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 39 |
# POST /agentmemory/remember
|
| 40 |
# ---------------------------------------------------------------------------
|
| 41 |
|
| 42 |
+
@memories_bp.route("/agentcache/remember", methods=["POST"])
|
| 43 |
@memories_bp.route("/agentmemory/remember", methods=["POST"])
|
| 44 |
def api_remember():
|
| 45 |
auth_err = _check_auth()
|
|
|
|
| 58 |
# POST /agentmemory/agent/remember
|
| 59 |
# ---------------------------------------------------------------------------
|
| 60 |
|
| 61 |
+
@memories_bp.route("/agentcache/agent/remember", methods=["POST"])
|
| 62 |
@memories_bp.route("/agentmemory/agent/remember", methods=["POST"])
|
| 63 |
def api_agent_remember():
|
| 64 |
auth_err = _check_auth()
|
|
|
|
| 103 |
# GET /agentmemory/memories
|
| 104 |
# ---------------------------------------------------------------------------
|
| 105 |
|
| 106 |
+
@memories_bp.route("/agentcache/memories", methods=["GET"])
|
| 107 |
@memories_bp.route("/agentmemory/memories", methods=["GET"])
|
| 108 |
def api_memories_list():
|
| 109 |
auth_err = _check_auth()
|
|
|
|
| 123 |
# POST /agentmemory/forget
|
| 124 |
# ---------------------------------------------------------------------------
|
| 125 |
|
| 126 |
+
@memories_bp.route("/agentcache/forget", methods=["POST"])
|
| 127 |
@memories_bp.route("/agentmemory/forget", methods=["POST"])
|
| 128 |
def api_forget():
|
| 129 |
auth_err = _check_auth()
|
src/routes/migration.py
CHANGED
|
@@ -14,7 +14,7 @@ migration_bp = Blueprint("migration", __name__)
|
|
| 14 |
|
| 15 |
def _check_auth():
|
| 16 |
import hmac
|
| 17 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 18 |
if not secret:
|
| 19 |
return None
|
| 20 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -32,9 +32,10 @@ def _get_kv():
|
|
| 32 |
|
| 33 |
|
| 34 |
# ---------------------------------------------------------------------------
|
| 35 |
-
# POST /
|
| 36 |
# ---------------------------------------------------------------------------
|
| 37 |
|
|
|
|
| 38 |
@migration_bp.route("/agentmemory/migrate", methods=["POST"])
|
| 39 |
def api_migrate():
|
| 40 |
auth_err = _check_auth()
|
|
|
|
| 14 |
|
| 15 |
def _check_auth():
|
| 16 |
import hmac
|
| 17 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 18 |
if not secret:
|
| 19 |
return None
|
| 20 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
# ---------------------------------------------------------------------------
|
| 35 |
+
# POST /agentcache/migrate
|
| 36 |
# ---------------------------------------------------------------------------
|
| 37 |
|
| 38 |
+
@migration_bp.route("/agentcache/migrate", methods=["POST"])
|
| 39 |
@migration_bp.route("/agentmemory/migrate", methods=["POST"])
|
| 40 |
def api_migrate():
|
| 41 |
auth_err = _check_auth()
|
src/routes/observations.py
CHANGED
|
@@ -24,7 +24,7 @@ def _datetime_now_iso() -> str:
|
|
| 24 |
def _check_auth():
|
| 25 |
"""Replicate the check_auth() pattern from app.py."""
|
| 26 |
import hmac
|
| 27 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 28 |
if not secret:
|
| 29 |
return None
|
| 30 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -46,6 +46,7 @@ def _get_kv():
|
|
| 46 |
# POST /agentmemory/observe (legacy raw hook endpoint + auto-compat shim)
|
| 47 |
# ---------------------------------------------------------------------------
|
| 48 |
|
|
|
|
| 49 |
@observations_bp.route("/agentmemory/observe", methods=["POST"])
|
| 50 |
def api_observe():
|
| 51 |
auth_err = _check_auth()
|
|
@@ -63,6 +64,7 @@ def api_observe():
|
|
| 63 |
body.get("folderPath")
|
| 64 |
or body.get("cwd")
|
| 65 |
or body.get("project")
|
|
|
|
| 66 |
or os.getenv("AGENTMEMORY_CWD")
|
| 67 |
or "/unknown"
|
| 68 |
)
|
|
@@ -114,6 +116,7 @@ def api_observe():
|
|
| 114 |
# POST /agentmemory/agent/observe
|
| 115 |
# ---------------------------------------------------------------------------
|
| 116 |
|
|
|
|
| 117 |
@observations_bp.route("/agentmemory/agent/observe", methods=["POST"])
|
| 118 |
def api_agent_observe():
|
| 119 |
auth_err = _check_auth()
|
|
@@ -161,6 +164,7 @@ def api_agent_observe():
|
|
| 161 |
# GET /agentmemory/folders
|
| 162 |
# ---------------------------------------------------------------------------
|
| 163 |
|
|
|
|
| 164 |
@observations_bp.route("/agentmemory/folders", methods=["GET"])
|
| 165 |
def api_folders():
|
| 166 |
auth_err = _check_auth()
|
|
@@ -182,6 +186,7 @@ def api_folders():
|
|
| 182 |
# GET /agentmemory/folder/observations
|
| 183 |
# ---------------------------------------------------------------------------
|
| 184 |
|
|
|
|
| 185 |
@observations_bp.route("/agentmemory/folder/observations", methods=["GET"])
|
| 186 |
def api_folder_observations():
|
| 187 |
auth_err = _check_auth()
|
|
@@ -207,6 +212,7 @@ def api_folder_observations():
|
|
| 207 |
# POST /agentmemory/session/start (legacy compat shim → 200 no-op)
|
| 208 |
# ---------------------------------------------------------------------------
|
| 209 |
|
|
|
|
| 210 |
@observations_bp.route("/agentmemory/session/start", methods=["POST"])
|
| 211 |
def api_session_start():
|
| 212 |
"""Legacy session/start — clients in the wild still call this.
|
|
@@ -230,6 +236,7 @@ def api_session_start():
|
|
| 230 |
# POST /agentmemory/session/end (legacy compat shim → 200 no-op)
|
| 231 |
# ---------------------------------------------------------------------------
|
| 232 |
|
|
|
|
| 233 |
@observations_bp.route("/agentmemory/session/end", methods=["POST"])
|
| 234 |
def api_session_end():
|
| 235 |
auth_err = _check_auth()
|
|
@@ -242,6 +249,7 @@ def api_session_end():
|
|
| 242 |
# GET /agentmemory/observations (legacy compat shim)
|
| 243 |
# ---------------------------------------------------------------------------
|
| 244 |
|
|
|
|
| 245 |
@observations_bp.route("/agentmemory/folder/dedup", methods=["POST"])
|
| 246 |
def api_folder_dedup():
|
| 247 |
"""POST /agentmemory/folder/dedup — remove duplicate observations.
|
|
@@ -270,6 +278,7 @@ def api_folder_dedup():
|
|
| 270 |
# GET /agentmemory/observations (legacy compat shim)
|
| 271 |
# ---------------------------------------------------------------------------
|
| 272 |
|
|
|
|
| 273 |
@observations_bp.route("/agentmemory/observations", methods=["GET"])
|
| 274 |
def api_observations_legacy():
|
| 275 |
"""Legacy /observations?sessionId=... shim.
|
|
|
|
| 24 |
def _check_auth():
|
| 25 |
"""Replicate the check_auth() pattern from app.py."""
|
| 26 |
import hmac
|
| 27 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 28 |
if not secret:
|
| 29 |
return None
|
| 30 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 46 |
# POST /agentmemory/observe (legacy raw hook endpoint + auto-compat shim)
|
| 47 |
# ---------------------------------------------------------------------------
|
| 48 |
|
| 49 |
+
@observations_bp.route("/agentcache/observe", methods=["POST"])
|
| 50 |
@observations_bp.route("/agentmemory/observe", methods=["POST"])
|
| 51 |
def api_observe():
|
| 52 |
auth_err = _check_auth()
|
|
|
|
| 64 |
body.get("folderPath")
|
| 65 |
or body.get("cwd")
|
| 66 |
or body.get("project")
|
| 67 |
+
or os.getenv("AGENTCACHE_CWD")
|
| 68 |
or os.getenv("AGENTMEMORY_CWD")
|
| 69 |
or "/unknown"
|
| 70 |
)
|
|
|
|
| 116 |
# POST /agentmemory/agent/observe
|
| 117 |
# ---------------------------------------------------------------------------
|
| 118 |
|
| 119 |
+
@observations_bp.route("/agentcache/agent/observe", methods=["POST"])
|
| 120 |
@observations_bp.route("/agentmemory/agent/observe", methods=["POST"])
|
| 121 |
def api_agent_observe():
|
| 122 |
auth_err = _check_auth()
|
|
|
|
| 164 |
# GET /agentmemory/folders
|
| 165 |
# ---------------------------------------------------------------------------
|
| 166 |
|
| 167 |
+
@observations_bp.route("/agentcache/folders", methods=["GET"])
|
| 168 |
@observations_bp.route("/agentmemory/folders", methods=["GET"])
|
| 169 |
def api_folders():
|
| 170 |
auth_err = _check_auth()
|
|
|
|
| 186 |
# GET /agentmemory/folder/observations
|
| 187 |
# ---------------------------------------------------------------------------
|
| 188 |
|
| 189 |
+
@observations_bp.route("/agentcache/folder/observations", methods=["GET"])
|
| 190 |
@observations_bp.route("/agentmemory/folder/observations", methods=["GET"])
|
| 191 |
def api_folder_observations():
|
| 192 |
auth_err = _check_auth()
|
|
|
|
| 212 |
# POST /agentmemory/session/start (legacy compat shim → 200 no-op)
|
| 213 |
# ---------------------------------------------------------------------------
|
| 214 |
|
| 215 |
+
@observations_bp.route("/agentcache/session/start", methods=["POST"])
|
| 216 |
@observations_bp.route("/agentmemory/session/start", methods=["POST"])
|
| 217 |
def api_session_start():
|
| 218 |
"""Legacy session/start — clients in the wild still call this.
|
|
|
|
| 236 |
# POST /agentmemory/session/end (legacy compat shim → 200 no-op)
|
| 237 |
# ---------------------------------------------------------------------------
|
| 238 |
|
| 239 |
+
@observations_bp.route("/agentcache/session/end", methods=["POST"])
|
| 240 |
@observations_bp.route("/agentmemory/session/end", methods=["POST"])
|
| 241 |
def api_session_end():
|
| 242 |
auth_err = _check_auth()
|
|
|
|
| 249 |
# GET /agentmemory/observations (legacy compat shim)
|
| 250 |
# ---------------------------------------------------------------------------
|
| 251 |
|
| 252 |
+
@observations_bp.route("/agentcache/folder/dedup", methods=["POST"])
|
| 253 |
@observations_bp.route("/agentmemory/folder/dedup", methods=["POST"])
|
| 254 |
def api_folder_dedup():
|
| 255 |
"""POST /agentmemory/folder/dedup — remove duplicate observations.
|
|
|
|
| 278 |
# GET /agentmemory/observations (legacy compat shim)
|
| 279 |
# ---------------------------------------------------------------------------
|
| 280 |
|
| 281 |
+
@observations_bp.route("/agentcache/observations", methods=["GET"])
|
| 282 |
@observations_bp.route("/agentmemory/observations", methods=["GET"])
|
| 283 |
def api_observations_legacy():
|
| 284 |
"""Legacy /observations?sessionId=... shim.
|
src/routes/search.py
CHANGED
|
@@ -15,7 +15,7 @@ search_bp = Blueprint("search", __name__)
|
|
| 15 |
|
| 16 |
def _check_auth():
|
| 17 |
import hmac
|
| 18 |
-
secret = os.getenv("AGENTMEMORY_SECRET")
|
| 19 |
if not secret:
|
| 20 |
return None
|
| 21 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
@@ -33,9 +33,10 @@ def _get_kv():
|
|
| 33 |
|
| 34 |
|
| 35 |
# ---------------------------------------------------------------------------
|
| 36 |
-
# POST /
|
| 37 |
# ---------------------------------------------------------------------------
|
| 38 |
|
|
|
|
| 39 |
@search_bp.route("/agentmemory/search", methods=["POST"])
|
| 40 |
def api_search():
|
| 41 |
auth_err = _check_auth()
|
|
@@ -58,9 +59,10 @@ def api_search():
|
|
| 58 |
|
| 59 |
|
| 60 |
# ---------------------------------------------------------------------------
|
| 61 |
-
# POST /
|
| 62 |
# ---------------------------------------------------------------------------
|
| 63 |
|
|
|
|
| 64 |
@search_bp.route("/agentmemory/timeline", methods=["POST"])
|
| 65 |
def api_timeline():
|
| 66 |
auth_err = _check_auth()
|
|
|
|
| 15 |
|
| 16 |
def _check_auth():
|
| 17 |
import hmac
|
| 18 |
+
secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
|
| 19 |
if not secret:
|
| 20 |
return None
|
| 21 |
auth = request.headers.get("Authorization") or request.headers.get("authorization")
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
# ---------------------------------------------------------------------------
|
| 36 |
+
# POST /agentcache/search
|
| 37 |
# ---------------------------------------------------------------------------
|
| 38 |
|
| 39 |
+
@search_bp.route("/agentcache/search", methods=["POST"])
|
| 40 |
@search_bp.route("/agentmemory/search", methods=["POST"])
|
| 41 |
def api_search():
|
| 42 |
auth_err = _check_auth()
|
|
|
|
| 59 |
|
| 60 |
|
| 61 |
# ---------------------------------------------------------------------------
|
| 62 |
+
# POST /agentcache/timeline
|
| 63 |
# ---------------------------------------------------------------------------
|
| 64 |
|
| 65 |
+
@search_bp.route("/agentcache/timeline", methods=["POST"])
|
| 66 |
@search_bp.route("/agentmemory/timeline", methods=["POST"])
|
| 67 |
def api_timeline():
|
| 68 |
auth_err = _check_auth()
|
src/storage/scopes.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
src/storage/scopes.py — KV scope registry (A2.3).
|
| 3 |
|
| 4 |
Copied from src/functions.py — do NOT delete the original (backward compat).
|
| 5 |
-
The KV class defines all storage scope keys used across
|
| 6 |
"""
|
| 7 |
|
| 8 |
|
|
|
|
| 2 |
src/storage/scopes.py — KV scope registry (A2.3).
|
| 3 |
|
| 4 |
Copied from src/functions.py — do NOT delete the original (backward compat).
|
| 5 |
+
The KV class defines all storage scope keys used across agentcache-python.
|
| 6 |
"""
|
| 7 |
|
| 8 |
|
src/viewer/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
| 8 |
<!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
|
| 9 |
(default-src 'none', style-src 'unsafe-inline', font-src 'self')
|
|
@@ -1287,8 +1287,8 @@
|
|
| 1287 |
<body>
|
| 1288 |
<div class="app-header">
|
| 1289 |
<a class="brand" href="#folders" data-tab-link="folders" aria-label="Open folders view">
|
| 1290 |
-
<h1>
|
| 1291 |
-
<span class="version">
|
| 1292 |
</a>
|
| 1293 |
<div class="header-right">
|
| 1294 |
<span class="dateline" id="dateline"></span>
|
|
@@ -1329,7 +1329,7 @@
|
|
| 1329 |
</div>
|
| 1330 |
|
| 1331 |
<footer id="viewer-footer" class="viewer-footer">
|
| 1332 |
-
<span>
|
| 1333 |
<span class="footer-sep">·</span>
|
| 1334 |
<a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
|
| 1335 |
<span class="footer-sep">·</span>
|
|
@@ -1338,7 +1338,7 @@
|
|
| 1338 |
<a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue →</a>
|
| 1339 |
</footer>
|
| 1340 |
|
| 1341 |
-
<script nonce="
|
| 1342 |
var safeStorage = {
|
| 1343 |
_mem: {},
|
| 1344 |
getItem: function(key) {
|
|
@@ -1385,10 +1385,10 @@
|
|
| 1385 |
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
| 1386 |
var btn = document.getElementById('theme-toggle');
|
| 1387 |
if (btn) btn.textContent = dark ? 'LIGHT' : 'DARK';
|
| 1388 |
-
if (persist) safeStorage.setItem('
|
| 1389 |
}
|
| 1390 |
window.toggleTheme = function() { applyTheme(!isDarkMode(), true); };
|
| 1391 |
-
var savedTheme = safeStorage.getItem('
|
| 1392 |
if (savedTheme) {
|
| 1393 |
applyTheme(savedTheme === 'dark', false);
|
| 1394 |
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
@@ -1442,7 +1442,7 @@
|
|
| 1442 |
};
|
| 1443 |
var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
|
| 1444 |
var TAB_IDS = ['folders', 'memories', 'graph', 'timeline', 'tools'];
|
| 1445 |
-
var VIEWER_TOKEN_STORAGE_KEY = '
|
| 1446 |
|
| 1447 |
var state = {
|
| 1448 |
activeTab: 'folders',
|
|
@@ -1539,8 +1539,8 @@
|
|
| 1539 |
}
|
| 1540 |
}
|
| 1541 |
(function() {
|
| 1542 |
-
var autoToken = '
|
| 1543 |
-
if (autoToken && autoToken.length > 0 && autoToken !== '
|
| 1544 |
try { sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, autoToken); } catch (_) {}
|
| 1545 |
}
|
| 1546 |
})();
|
|
@@ -1561,9 +1561,9 @@
|
|
| 1561 |
'<div class="viewer-auth-panel">' +
|
| 1562 |
'<div>' +
|
| 1563 |
'<div class="viewer-auth-title">Viewer authorization required</div>' +
|
| 1564 |
-
'<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>
|
| 1565 |
'</div>' +
|
| 1566 |
-
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="
|
| 1567 |
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
|
| 1568 |
'</div>';
|
| 1569 |
var input = document.getElementById('viewer-auth-token');
|
|
@@ -1578,7 +1578,7 @@
|
|
| 1578 |
|
| 1579 |
async function api(path, opts) {
|
| 1580 |
try {
|
| 1581 |
-
var url = REST + '/
|
| 1582 |
var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
|
| 1583 |
var viewerToken = getViewerToken();
|
| 1584 |
if (viewerToken && !headers.Authorization && !headers.authorization) {
|
|
@@ -1673,7 +1673,7 @@
|
|
| 1673 |
|
| 1674 |
async function loadDashboard() {
|
| 1675 |
// Dashboard tab was removed; redirect to the Folders tab which is the
|
| 1676 |
-
// active landing page backed by real endpoints (/
|
| 1677 |
switchTab('folders');
|
| 1678 |
}
|
| 1679 |
|
|
@@ -1746,7 +1746,7 @@
|
|
| 1746 |
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
| 1747 |
try {
|
| 1748 |
var qp = 'folderPath=' + encodeURIComponent(folderPath) + '&agentId=' + encodeURIComponent(agentId);
|
| 1749 |
-
var result = await apiFetchRaw('/
|
| 1750 |
var obs = (result && result.observations) || [];
|
| 1751 |
|
| 1752 |
var html = '<div id="folder-detail-root" style="padding:20px 24px;">';
|
|
@@ -1926,7 +1926,7 @@
|
|
| 1926 |
if (viewerToken && !headers.Authorization) headers['Authorization'] = 'Bearer ' + viewerToken;
|
| 1927 |
// Always use REST base so cross-origin ?port= param works correctly
|
| 1928 |
var url = path.startsWith('http') ? path
|
| 1929 |
-
: REST + (path.startsWith('/') ? path : '/
|
| 1930 |
var res = await fetch(url, Object.assign({}, opts, { headers: headers }));
|
| 1931 |
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
| 1932 |
return res.json();
|
|
@@ -1955,7 +1955,7 @@
|
|
| 1955 |
state.graph.queryError = null;
|
| 1956 |
var graphData;
|
| 1957 |
try {
|
| 1958 |
-
graphData = await apiFetchRaw('/
|
| 1959 |
} catch(err) {
|
| 1960 |
state.graph.queryError = String(err);
|
| 1961 |
graphData = { nodes: [], edges: [] };
|
|
@@ -2715,7 +2715,7 @@
|
|
| 2715 |
|
| 2716 |
var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
|
| 2717 |
html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
|
| 2718 |
-
html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>
|
| 2719 |
html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
|
| 2720 |
html += '</div></div>';
|
| 2721 |
|
|
@@ -2740,8 +2740,8 @@
|
|
| 2740 |
html += '<div class="empty-state">' +
|
| 2741 |
'<div class="empty-icon">📚</div>' +
|
| 2742 |
'<div class="empty-title">No memories yet</div>' +
|
| 2743 |
-
'<div class="empty-lead">Memories are the distilled facts
|
| 2744 |
-
'<pre class="empty-cmd">
|
| 2745 |
'<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types →</a></div>' +
|
| 2746 |
'</div>';
|
| 2747 |
} else {
|
|
@@ -2833,7 +2833,7 @@
|
|
| 2833 |
var body = { limit: 100 };
|
| 2834 |
if (folderPath) body.folderPath = folderPath;
|
| 2835 |
if (agentId) body.agentId = agentId;
|
| 2836 |
-
var result = await apiFetchRaw('/
|
| 2837 |
method: 'POST',
|
| 2838 |
body: JSON.stringify(body)
|
| 2839 |
});
|
|
@@ -3114,11 +3114,11 @@
|
|
| 3114 |
renderObservations();
|
| 3115 |
}
|
| 3116 |
|
| 3117 |
-
// loadActivity removed — /
|
| 3118 |
async function loadActivity() { return; }
|
| 3119 |
function renderActivity() { return; }
|
| 3120 |
|
| 3121 |
-
// loadSessions removed — /
|
| 3122 |
async function loadSessions() { return; }
|
| 3123 |
function renderSessions() { return; }
|
| 3124 |
function selectSession(id) { return; }
|
|
@@ -3405,7 +3405,7 @@
|
|
| 3405 |
title: 'No LLM provider key set',
|
| 3406 |
keyLabel: 'ANTHROPIC_API_KEY',
|
| 3407 |
desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
|
| 3408 |
-
enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @
|
| 3409 |
docs: 'https://github.com/rohitg00/agentmemory#quick-start',
|
| 3410 |
dismissKey: '__provider_noop',
|
| 3411 |
});
|
|
@@ -3478,7 +3478,7 @@
|
|
| 3478 |
'### Steps to reproduce\n\n' +
|
| 3479 |
'1. \n2. \n3. \n'
|
| 3480 |
);
|
| 3481 |
-
fbEl.href = 'https://github.com/rohitg00/
|
| 3482 |
encodeURIComponent('[viewer] ') + '&body=' + body;
|
| 3483 |
}
|
| 3484 |
}
|
|
@@ -3727,7 +3727,7 @@
|
|
| 3727 |
// B4 -- Onboarding card (empty state detection)
|
| 3728 |
// ==========================================================================
|
| 3729 |
function maybeShowOnboarding(health, flags) {
|
| 3730 |
-
if (safeStorage.getItem('
|
| 3731 |
if ((health.observationCount || 0) > 0 || (health.memoryCount || 0) > 0) return;
|
| 3732 |
var el = document.getElementById('view-folders');
|
| 3733 |
if (!el) return;
|
|
@@ -3748,20 +3748,20 @@
|
|
| 3748 |
'</div></div>';
|
| 3749 |
}
|
| 3750 |
|
| 3751 |
-
var secretSet = false; //
|
| 3752 |
var geminiSet = !!(flags && flags.embeddingProvider && flags.embeddingProvider !== 'none');
|
| 3753 |
|
| 3754 |
var card = document.createElement('div');
|
| 3755 |
card.className = 'onboarding-card';
|
| 3756 |
card.innerHTML =
|
| 3757 |
-
'<h2>Welcome to
|
| 3758 |
-
'<p>No observations yet. Complete these steps to start recording agent
|
| 3759 |
-
step(1, secretSet, 'Set
|
| 3760 |
-
'echo "
|
| 3761 |
step(2, geminiSet, 'Add GEMINI_API_KEY for semantic search',
|
| 3762 |
-
'echo "GEMINI_API_KEY=your-key" >> ~/.
|
| 3763 |
-
step(3, false, 'Point your agent at
|
| 3764 |
-
'curl -X POST http://localhost:3111/
|
| 3765 |
' -H "Content-Type: application/json" \\\n' +
|
| 3766 |
' -d \'{"folderPath":"/your/project","agentId":"kiro","text":"Hello",' +
|
| 3767 |
'"timestamp":"' + new Date().toISOString() + '"}\'') +
|
|
@@ -3774,7 +3774,7 @@
|
|
| 3774 |
});
|
| 3775 |
});
|
| 3776 |
card.querySelector('.onboarding-dismiss').addEventListener('click', function() {
|
| 3777 |
-
safeStorage.setItem('
|
| 3778 |
card.remove();
|
| 3779 |
});
|
| 3780 |
|
|
@@ -3789,7 +3789,7 @@
|
|
| 3789 |
await _origLoadFolders.apply(this, arguments);
|
| 3790 |
// After load, check health for onboarding card
|
| 3791 |
try {
|
| 3792 |
-
var h = await apiFetchRaw('/
|
| 3793 |
var f = state.flagsConfig;
|
| 3794 |
maybeShowOnboarding(h, f);
|
| 3795 |
} catch(_) {}
|
|
@@ -3812,7 +3812,7 @@
|
|
| 3812 |
'<div class="skeleton skeleton-card"></div>' +
|
| 3813 |
'</div>';
|
| 3814 |
try {
|
| 3815 |
-
var data = await apiFetch('/
|
| 3816 |
var tools = data.tools || [];
|
| 3817 |
renderToolsTab(el, tools);
|
| 3818 |
} catch(err) {
|
|
@@ -3890,7 +3890,7 @@
|
|
| 3890 |
var _toolToken = getViewerToken();
|
| 3891 |
var _toolHeaders = { 'Content-Type': 'application/json' };
|
| 3892 |
if (_toolToken) _toolHeaders['Authorization'] = 'Bearer ' + _toolToken;
|
| 3893 |
-
var rawRes = await fetch(REST + '/
|
| 3894 |
method: 'POST',
|
| 3895 |
headers: _toolHeaders,
|
| 3896 |
body: JSON.stringify({ name: name, arguments: args })
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>agentcache viewer</title>
|
| 7 |
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
| 8 |
<!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
|
| 9 |
(default-src 'none', style-src 'unsafe-inline', font-src 'self')
|
|
|
|
| 1287 |
<body>
|
| 1288 |
<div class="app-header">
|
| 1289 |
<a class="brand" href="#folders" data-tab-link="folders" aria-label="Open folders view">
|
| 1290 |
+
<h1>agentcache</h1>
|
| 1291 |
+
<span class="version">v__AGENTCACHE_VERSION__</span>
|
| 1292 |
</a>
|
| 1293 |
<div class="header-right">
|
| 1294 |
<span class="dateline" id="dateline"></span>
|
|
|
|
| 1329 |
</div>
|
| 1330 |
|
| 1331 |
<footer id="viewer-footer" class="viewer-footer">
|
| 1332 |
+
<span>agentcache viewer · <span id="footer-version">loading...</span></span>
|
| 1333 |
<span class="footer-sep">·</span>
|
| 1334 |
<a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
|
| 1335 |
<span class="footer-sep">·</span>
|
|
|
|
| 1338 |
<a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue →</a>
|
| 1339 |
</footer>
|
| 1340 |
|
| 1341 |
+
<script nonce="__AGENTCACHE_VIEWER_NONCE__">
|
| 1342 |
var safeStorage = {
|
| 1343 |
_mem: {},
|
| 1344 |
getItem: function(key) {
|
|
|
|
| 1385 |
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
| 1386 |
var btn = document.getElementById('theme-toggle');
|
| 1387 |
if (btn) btn.textContent = dark ? 'LIGHT' : 'DARK';
|
| 1388 |
+
if (persist) safeStorage.setItem('agentcache-theme', dark ? 'dark' : 'light');
|
| 1389 |
}
|
| 1390 |
window.toggleTheme = function() { applyTheme(!isDarkMode(), true); };
|
| 1391 |
+
var savedTheme = safeStorage.getItem('agentcache-theme');
|
| 1392 |
if (savedTheme) {
|
| 1393 |
applyTheme(savedTheme === 'dark', false);
|
| 1394 |
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
|
|
| 1442 |
};
|
| 1443 |
var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
|
| 1444 |
var TAB_IDS = ['folders', 'memories', 'graph', 'timeline', 'tools'];
|
| 1445 |
+
var VIEWER_TOKEN_STORAGE_KEY = 'agentcache-viewer-token';
|
| 1446 |
|
| 1447 |
var state = {
|
| 1448 |
activeTab: 'folders',
|
|
|
|
| 1539 |
}
|
| 1540 |
}
|
| 1541 |
(function() {
|
| 1542 |
+
var autoToken = '__AGENTCACHE_AUTO_TOKEN__';
|
| 1543 |
+
if (autoToken && autoToken.length > 0 && autoToken !== '__AGENTCACHE_AUTO_TOKEN__') {
|
| 1544 |
try { sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, autoToken); } catch (_) {}
|
| 1545 |
}
|
| 1546 |
})();
|
|
|
|
| 1561 |
'<div class="viewer-auth-panel">' +
|
| 1562 |
'<div>' +
|
| 1563 |
'<div class="viewer-auth-title">Viewer authorization required</div>' +
|
| 1564 |
+
'<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTCACHE_SECRET</code> to unlock viewer API access.</div>' +
|
| 1565 |
'</div>' +
|
| 1566 |
+
'<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTCACHE_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTCACHE_SECRET" />' +
|
| 1567 |
'<button class="btn" data-action="save-viewer-token">Unlock</button>' +
|
| 1568 |
'</div>';
|
| 1569 |
var input = document.getElementById('viewer-auth-token');
|
|
|
|
| 1578 |
|
| 1579 |
async function api(path, opts) {
|
| 1580 |
try {
|
| 1581 |
+
var url = REST + '/agentcache/' + path;
|
| 1582 |
var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
|
| 1583 |
var viewerToken = getViewerToken();
|
| 1584 |
if (viewerToken && !headers.Authorization && !headers.authorization) {
|
|
|
|
| 1673 |
|
| 1674 |
async function loadDashboard() {
|
| 1675 |
// Dashboard tab was removed; redirect to the Folders tab which is the
|
| 1676 |
+
// active landing page backed by real endpoints (/agentcache/folders).
|
| 1677 |
switchTab('folders');
|
| 1678 |
}
|
| 1679 |
|
|
|
|
| 1746 |
el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
|
| 1747 |
try {
|
| 1748 |
var qp = 'folderPath=' + encodeURIComponent(folderPath) + '&agentId=' + encodeURIComponent(agentId);
|
| 1749 |
+
var result = await apiFetchRaw('/agentcache/folder/observations?' + qp);
|
| 1750 |
var obs = (result && result.observations) || [];
|
| 1751 |
|
| 1752 |
var html = '<div id="folder-detail-root" style="padding:20px 24px;">';
|
|
|
|
| 1926 |
if (viewerToken && !headers.Authorization) headers['Authorization'] = 'Bearer ' + viewerToken;
|
| 1927 |
// Always use REST base so cross-origin ?port= param works correctly
|
| 1928 |
var url = path.startsWith('http') ? path
|
| 1929 |
+
: REST + (path.startsWith('/') ? path : '/agentcache/' + path);
|
| 1930 |
var res = await fetch(url, Object.assign({}, opts, { headers: headers }));
|
| 1931 |
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
| 1932 |
return res.json();
|
|
|
|
| 1955 |
state.graph.queryError = null;
|
| 1956 |
var graphData;
|
| 1957 |
try {
|
| 1958 |
+
graphData = await apiFetchRaw('/agentcache/graph');
|
| 1959 |
} catch(err) {
|
| 1960 |
state.graph.queryError = String(err);
|
| 1961 |
graphData = { nodes: [], edges: [] };
|
|
|
|
| 2715 |
|
| 2716 |
var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
|
| 2717 |
html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
|
| 2718 |
+
html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>agent_cache</code> MCP tool or the <code>/agentcache/remember</code> endpoint. They survive across sessions and supersede each other as v1, v2, etc. ';
|
| 2719 |
html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
|
| 2720 |
html += '</div></div>';
|
| 2721 |
|
|
|
|
| 2740 |
html += '<div class="empty-state">' +
|
| 2741 |
'<div class="empty-icon">📚</div>' +
|
| 2742 |
'<div class="empty-title">No memories yet</div>' +
|
| 2743 |
+
'<div class="empty-lead">Memories are the distilled facts agentcache keeps across sessions — things like file paths, architectural decisions, and user preferences. Hooks capture them automatically during coding sessions; you can also save one directly.</div>' +
|
| 2744 |
+
'<pre class="empty-cmd">agent_cache {\n title: "auth uses jose middleware",\n content: "src/middleware/auth.ts handles JWT validation",\n type: "architecture"\n}</pre>' +
|
| 2745 |
'<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types →</a></div>' +
|
| 2746 |
'</div>';
|
| 2747 |
} else {
|
|
|
|
| 2833 |
var body = { limit: 100 };
|
| 2834 |
if (folderPath) body.folderPath = folderPath;
|
| 2835 |
if (agentId) body.agentId = agentId;
|
| 2836 |
+
var result = await apiFetchRaw('/agentcache/timeline', {
|
| 2837 |
method: 'POST',
|
| 2838 |
body: JSON.stringify(body)
|
| 2839 |
});
|
|
|
|
| 3114 |
renderObservations();
|
| 3115 |
}
|
| 3116 |
|
| 3117 |
+
// loadActivity removed — /agentcache/sessions endpoint does not exist.
|
| 3118 |
async function loadActivity() { return; }
|
| 3119 |
function renderActivity() { return; }
|
| 3120 |
|
| 3121 |
+
// loadSessions removed — /agentcache/sessions endpoint does not exist.
|
| 3122 |
async function loadSessions() { return; }
|
| 3123 |
function renderSessions() { return; }
|
| 3124 |
function selectSession(id) { return; }
|
|
|
|
| 3405 |
title: 'No LLM provider key set',
|
| 3406 |
keyLabel: 'ANTHROPIC_API_KEY',
|
| 3407 |
desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
|
| 3408 |
+
enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @agentcache/agentcache',
|
| 3409 |
docs: 'https://github.com/rohitg00/agentmemory#quick-start',
|
| 3410 |
dismissKey: '__provider_noop',
|
| 3411 |
});
|
|
|
|
| 3478 |
'### Steps to reproduce\n\n' +
|
| 3479 |
'1. \n2. \n3. \n'
|
| 3480 |
);
|
| 3481 |
+
fbEl.href = 'https://github.com/rohitg00/agentcache/issues/new?title=' +
|
| 3482 |
encodeURIComponent('[viewer] ') + '&body=' + body;
|
| 3483 |
}
|
| 3484 |
}
|
|
|
|
| 3727 |
// B4 -- Onboarding card (empty state detection)
|
| 3728 |
// ==========================================================================
|
| 3729 |
function maybeShowOnboarding(health, flags) {
|
| 3730 |
+
if (safeStorage.getItem('agentcache_onboarding_dismissed')) return;
|
| 3731 |
if ((health.observationCount || 0) > 0 || (health.memoryCount || 0) > 0) return;
|
| 3732 |
var el = document.getElementById('view-folders');
|
| 3733 |
if (!el) return;
|
|
|
|
| 3748 |
'</div></div>';
|
| 3749 |
}
|
| 3750 |
|
| 3751 |
+
var secretSet = false; // AGENTCACHE_SECRET status not exposed by flags endpoint
|
| 3752 |
var geminiSet = !!(flags && flags.embeddingProvider && flags.embeddingProvider !== 'none');
|
| 3753 |
|
| 3754 |
var card = document.createElement('div');
|
| 3755 |
card.className = 'onboarding-card';
|
| 3756 |
card.innerHTML =
|
| 3757 |
+
'<h2>Welcome to agentcache</h2>' +
|
| 3758 |
+
'<p>No observations yet. Complete these steps to start recording agent cache.</p>' +
|
| 3759 |
+
step(1, secretSet, 'Set AGENTCACHE_SECRET (optional auth)',
|
| 3760 |
+
'echo "AGENTCACHE_SECRET=your-secret-here" >> ~/.agentcache/.env') +
|
| 3761 |
step(2, geminiSet, 'Add GEMINI_API_KEY for semantic search',
|
| 3762 |
+
'echo "GEMINI_API_KEY=your-key" >> ~/.agentcache/.env') +
|
| 3763 |
+
step(3, false, 'Point your agent at agentcache',
|
| 3764 |
+
'curl -X POST http://localhost:3111/agentcache/agent/observe \\\n' +
|
| 3765 |
' -H "Content-Type: application/json" \\\n' +
|
| 3766 |
' -d \'{"folderPath":"/your/project","agentId":"kiro","text":"Hello",' +
|
| 3767 |
'"timestamp":"' + new Date().toISOString() + '"}\'') +
|
|
|
|
| 3774 |
});
|
| 3775 |
});
|
| 3776 |
card.querySelector('.onboarding-dismiss').addEventListener('click', function() {
|
| 3777 |
+
safeStorage.setItem('agentcache_onboarding_dismissed', '1');
|
| 3778 |
card.remove();
|
| 3779 |
});
|
| 3780 |
|
|
|
|
| 3789 |
await _origLoadFolders.apply(this, arguments);
|
| 3790 |
// After load, check health for onboarding card
|
| 3791 |
try {
|
| 3792 |
+
var h = await apiFetchRaw('/agentcache/health');
|
| 3793 |
var f = state.flagsConfig;
|
| 3794 |
maybeShowOnboarding(h, f);
|
| 3795 |
} catch(_) {}
|
|
|
|
| 3812 |
'<div class="skeleton skeleton-card"></div>' +
|
| 3813 |
'</div>';
|
| 3814 |
try {
|
| 3815 |
+
var data = await apiFetch('/agentcache/mcp/tools');
|
| 3816 |
var tools = data.tools || [];
|
| 3817 |
renderToolsTab(el, tools);
|
| 3818 |
} catch(err) {
|
|
|
|
| 3890 |
var _toolToken = getViewerToken();
|
| 3891 |
var _toolHeaders = { 'Content-Type': 'application/json' };
|
| 3892 |
if (_toolToken) _toolHeaders['Authorization'] = 'Bearer ' + _toolToken;
|
| 3893 |
+
var rawRes = await fetch(REST + '/agentcache/mcp/tools', {
|
| 3894 |
method: 'POST',
|
| 3895 |
headers: _toolHeaders,
|
| 3896 |
body: JSON.stringify({ name: name, arguments: args })
|
src/viewer_helpers.py
CHANGED
|
@@ -23,14 +23,14 @@ def make_viewer_response(base_dir: str):
|
|
| 23 |
|
| 24 |
nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode("utf-8").rstrip("=")
|
| 25 |
|
| 26 |
-
# D2.2: Never embed the raw
|
| 27 |
# Replace the placeholder with an empty string — the viewer authenticates
|
| 28 |
# via the Authorization header set programmatically after load.
|
| 29 |
html = (
|
| 30 |
template
|
| 31 |
-
.replace("
|
| 32 |
-
.replace("
|
| 33 |
-
.replace("
|
| 34 |
)
|
| 35 |
|
| 36 |
csp = "; ".join([
|
|
|
|
| 23 |
|
| 24 |
nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode("utf-8").rstrip("=")
|
| 25 |
|
| 26 |
+
# D2.2: Never embed the raw AGENTCACHE_SECRET in page source.
|
| 27 |
# Replace the placeholder with an empty string — the viewer authenticates
|
| 28 |
# via the Authorization header set programmatically after load.
|
| 29 |
html = (
|
| 30 |
template
|
| 31 |
+
.replace("__AGENTCACHE_VIEWER_NONCE__", nonce)
|
| 32 |
+
.replace("__AGENTCACHE_VERSION__", "0.9.8")
|
| 33 |
+
.replace("__AGENTCACHE_AUTO_TOKEN__", "")
|
| 34 |
)
|
| 35 |
|
| 36 |
csp = "; ".join([
|
src/workers.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Background worker threads for
|
| 3 |
|
| 4 |
Start all workers via start_background_workers(kv).
|
| 5 |
Each worker runs as a daemon thread so it exits automatically when the process dies.
|
|
|
|
| 1 |
"""
|
| 2 |
+
Background worker threads for agentcache-python.
|
| 3 |
|
| 4 |
Start all workers via start_background_workers(kv).
|
| 5 |
Each worker runs as a daemon thread so it exits automatically when the process dies.
|
start.sh
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
# Note: no set -e — sync failures must not kill the container
|
| 3 |
|
| 4 |
-
# Create
|
| 5 |
-
mkdir -p /home/user/.
|
| 6 |
|
| 7 |
# =============================================================================
|
| 8 |
# Persistent storage via HF Dataset repo
|
| 9 |
# Secrets to set in HF Space settings:
|
| 10 |
# HF_TOKEN — write access to the dataset repo
|
| 11 |
# GEMINI_API_KEY — powers graph, embeddings, compression, crystals
|
| 12 |
-
#
|
| 13 |
# =============================================================================
|
| 14 |
-
export
|
| 15 |
|
| 16 |
echo "[start] Restoring data from HF Dataset..."
|
| 17 |
python3 /app/sync.py restore
|
|
@@ -25,32 +25,32 @@ python3 /app/sync.py restore
|
|
| 25 |
) &
|
| 26 |
|
| 27 |
# Generate HMAC secret on first boot, persist it so it survives dataset restore
|
| 28 |
-
HMAC_FILE="/home/user/.
|
| 29 |
if [ ! -s "$HMAC_FILE" ]; then
|
| 30 |
SECRET="$(openssl rand -hex 32)"
|
| 31 |
printf '%s\n' "$SECRET" > "$HMAC_FILE"
|
| 32 |
chmod 600 "$HMAC_FILE"
|
| 33 |
echo "================================================================"
|
| 34 |
-
echo "
|
| 35 |
-
echo "
|
| 36 |
-
echo "Copy this to your Space secrets as
|
| 37 |
echo "It will not be printed again."
|
| 38 |
echo "================================================================"
|
| 39 |
fi
|
| 40 |
-
export
|
| 41 |
|
| 42 |
# Write .env config for the daemon so it is loaded by src/app.py
|
| 43 |
-
cat > /home/user/.
|
| 44 |
GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
III_ENGINE_URL=ws://localhost:49134
|
| 48 |
GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}
|
| 49 |
EMBEDDING_PROVIDER=gemini
|
| 50 |
CONSOLIDATION_ENABLED=true
|
| 51 |
GRAPH_EXTRACTION_ENABLED=true
|
| 52 |
-
|
| 53 |
-
|
| 54 |
EOF
|
| 55 |
|
| 56 |
# Set the port for Flask application to run on (Hugging Face Space expects 7860)
|
|
@@ -64,3 +64,4 @@ python3 src/app.py
|
|
| 64 |
# Perform a final backup on shutdown
|
| 65 |
echo "[start] Flask application exited. Performing final backup..."
|
| 66 |
python3 /app/sync.py backup
|
|
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
# Note: no set -e — sync failures must not kill the container
|
| 3 |
|
| 4 |
+
# Create agentcache data directories
|
| 5 |
+
mkdir -p /home/user/.agentcache
|
| 6 |
|
| 7 |
# =============================================================================
|
| 8 |
# Persistent storage via HF Dataset repo
|
| 9 |
# Secrets to set in HF Space settings:
|
| 10 |
# HF_TOKEN — write access to the dataset repo
|
| 11 |
# GEMINI_API_KEY — powers graph, embeddings, compression, crystals
|
| 12 |
+
# AGENTCACHE_DATASET_REPO — optional override (default: Yash030/agentmemory-python-data)
|
| 13 |
# =============================================================================
|
| 14 |
+
export AGENTCACHE_DATASET_REPO="${AGENTCACHE_DATASET_REPO:-${AGENTMEMORY_DATASET_REPO:-Yash030/agentmemory-python-data}}"
|
| 15 |
|
| 16 |
echo "[start] Restoring data from HF Dataset..."
|
| 17 |
python3 /app/sync.py restore
|
|
|
|
| 25 |
) &
|
| 26 |
|
| 27 |
# Generate HMAC secret on first boot, persist it so it survives dataset restore
|
| 28 |
+
HMAC_FILE="/home/user/.agentcache/.hmac"
|
| 29 |
if [ ! -s "$HMAC_FILE" ]; then
|
| 30 |
SECRET="$(openssl rand -hex 32)"
|
| 31 |
printf '%s\n' "$SECRET" > "$HMAC_FILE"
|
| 32 |
chmod 600 "$HMAC_FILE"
|
| 33 |
echo "================================================================"
|
| 34 |
+
echo "agentcache: generated HMAC secret on first boot"
|
| 35 |
+
echo "AGENTCACHE_SECRET=$SECRET"
|
| 36 |
+
echo "Copy this to your Space secrets as AGENTCACHE_SECRET."
|
| 37 |
echo "It will not be printed again."
|
| 38 |
echo "================================================================"
|
| 39 |
fi
|
| 40 |
+
export AGENTCACHE_SECRET="${AGENTCACHE_SECRET:-${AGENTMEMORY_SECRET:-$(cat "$HMAC_FILE")}}"
|
| 41 |
|
| 42 |
# Write .env config for the daemon so it is loaded by src/app.py
|
| 43 |
+
cat > /home/user/.agentcache/.env <<EOF
|
| 44 |
GEMINI_API_KEY=${GEMINI_API_KEY}
|
| 45 |
+
AGENTCACHE_SECRET=${AGENTCACHE_SECRET}
|
| 46 |
+
AGENTCACHE_URL=http://localhost:7860
|
| 47 |
III_ENGINE_URL=ws://localhost:49134
|
| 48 |
GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}
|
| 49 |
EMBEDDING_PROVIDER=gemini
|
| 50 |
CONSOLIDATION_ENABLED=true
|
| 51 |
GRAPH_EXTRACTION_ENABLED=true
|
| 52 |
+
AGENTCACHE_REFLECT=true
|
| 53 |
+
AGENTCACHE_AUTO_COMPRESS=true
|
| 54 |
EOF
|
| 55 |
|
| 56 |
# Set the port for Flask application to run on (Hugging Face Space expects 7860)
|
|
|
|
| 64 |
# Perform a final backup on shutdown
|
| 65 |
echo "[start] Flask application exited. Performing final backup..."
|
| 66 |
python3 /app/sync.py backup
|
| 67 |
+
|
sync.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Sync
|
| 4 |
Usage:
|
| 5 |
python3 sync.py restore -- download DB from HF on startup
|
| 6 |
python3 sync.py backup -- upload DB to HF (called in loop)
|
|
@@ -24,13 +24,13 @@ except ImportError:
|
|
| 24 |
sys.exit(0)
|
| 25 |
|
| 26 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 27 |
-
REPO_ID = os.environ.get("AGENTMEMORY_DATASET_REPO"
|
| 28 |
-
DATA_DIR = os.path.expanduser("~/.
|
| 29 |
-
DB_PATH = os.path.join(DATA_DIR, "
|
| 30 |
|
| 31 |
# Only these paths are backed up/restored — everything else is ephemeral
|
| 32 |
SYNC_FILES = [
|
| 33 |
-
"
|
| 34 |
".hmac",
|
| 35 |
]
|
| 36 |
SYNC_DIRS = [
|
|
@@ -232,7 +232,7 @@ def backup():
|
|
| 232 |
return
|
| 233 |
|
| 234 |
# Stage only the targeted files
|
| 235 |
-
staging = tempfile.mkdtemp(prefix="
|
| 236 |
try:
|
| 237 |
for full, rel in targets:
|
| 238 |
dest = os.path.join(staging, rel.replace("/", os.sep))
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Sync agentcache data to/from a private HF Dataset repo.
|
| 4 |
Usage:
|
| 5 |
python3 sync.py restore -- download DB from HF on startup
|
| 6 |
python3 sync.py backup -- upload DB to HF (called in loop)
|
|
|
|
| 24 |
sys.exit(0)
|
| 25 |
|
| 26 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 27 |
+
REPO_ID = os.environ.get("AGENTCACHE_DATASET_REPO") or os.environ.get("AGENTMEMORY_DATASET_REPO") or "Yash030/agentmemory-python-data"
|
| 28 |
+
DATA_DIR = os.path.expanduser("~/.agentcache")
|
| 29 |
+
DB_PATH = os.path.join(DATA_DIR, "agentcache.db")
|
| 30 |
|
| 31 |
# Only these paths are backed up/restored — everything else is ephemeral
|
| 32 |
SYNC_FILES = [
|
| 33 |
+
"agentcache.db",
|
| 34 |
".hmac",
|
| 35 |
]
|
| 36 |
SYNC_DIRS = [
|
|
|
|
| 232 |
return
|
| 233 |
|
| 234 |
# Stage only the targeted files
|
| 235 |
+
staging = tempfile.mkdtemp(prefix="agentcache_sync_")
|
| 236 |
try:
|
| 237 |
for full, rel in targets:
|
| 238 |
dest = os.path.join(staging, rel.replace("/", os.sep))
|
tests/test_api.py
CHANGED
|
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
| 21 |
def flask_app(tmp_path_factory):
|
| 22 |
tmp_dir = tmp_path_factory.mktemp("api_test_db")
|
| 23 |
db_path = str(tmp_dir / "test.db")
|
|
|
|
| 24 |
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 25 |
|
| 26 |
from db import StateKV
|
|
@@ -31,6 +32,8 @@ def flask_app(tmp_path_factory):
|
|
| 31 |
|
| 32 |
StateKV.__init__ = patched_init
|
| 33 |
import app as app_module
|
|
|
|
|
|
|
| 34 |
flask_application = app_module.create_app()
|
| 35 |
StateKV.__init__ = original_init
|
| 36 |
flask_application.config["TESTING"] = True
|
|
@@ -51,12 +54,12 @@ def _post(client, url, payload):
|
|
| 51 |
|
| 52 |
|
| 53 |
# ---------------------------------------------------------------------------
|
| 54 |
-
# POST /
|
| 55 |
# ---------------------------------------------------------------------------
|
| 56 |
|
| 57 |
class TestAgentObserve:
|
| 58 |
def test_valid_payload_returns_201(self, client):
|
| 59 |
-
resp = _post(client, "/
|
| 60 |
"folderPath": "/home/user/test-project",
|
| 61 |
"agentId": "kiro",
|
| 62 |
"text": "Implemented new authentication middleware",
|
|
@@ -68,7 +71,7 @@ class TestAgentObserve:
|
|
| 68 |
assert data["observationId"].startswith("fobs_")
|
| 69 |
|
| 70 |
def test_missing_folder_path_returns_400(self, client):
|
| 71 |
-
resp = _post(client, "/
|
| 72 |
"agentId": "kiro",
|
| 73 |
"text": "Some work",
|
| 74 |
"timestamp": _now(),
|
|
@@ -76,7 +79,7 @@ class TestAgentObserve:
|
|
| 76 |
assert resp.status_code == 400
|
| 77 |
|
| 78 |
def test_missing_agent_id_returns_400(self, client):
|
| 79 |
-
resp = _post(client, "/
|
| 80 |
"folderPath": "/home/user/proj",
|
| 81 |
"text": "Some work",
|
| 82 |
"timestamp": _now(),
|
|
@@ -84,7 +87,7 @@ class TestAgentObserve:
|
|
| 84 |
assert resp.status_code == 400
|
| 85 |
|
| 86 |
def test_missing_text_returns_400(self, client):
|
| 87 |
-
resp = _post(client, "/
|
| 88 |
"folderPath": "/home/user/proj",
|
| 89 |
"agentId": "kiro",
|
| 90 |
"timestamp": _now(),
|
|
@@ -93,46 +96,46 @@ class TestAgentObserve:
|
|
| 93 |
|
| 94 |
|
| 95 |
# ---------------------------------------------------------------------------
|
| 96 |
-
# POST /
|
| 97 |
# ---------------------------------------------------------------------------
|
| 98 |
|
| 99 |
class TestSearch:
|
| 100 |
def test_search_with_query_returns_200(self, client):
|
| 101 |
# Seed data first
|
| 102 |
-
_post(client, "/
|
| 103 |
"folderPath": "/home/user/search-proj",
|
| 104 |
"agentId": "kiro",
|
| 105 |
"text": "Refactored the authentication system",
|
| 106 |
"timestamp": _now(),
|
| 107 |
})
|
| 108 |
-
resp = _post(client, "/
|
| 109 |
assert resp.status_code == 200
|
| 110 |
data = resp.get_json()
|
| 111 |
assert isinstance(data, list) or isinstance(data, dict)
|
| 112 |
|
| 113 |
def test_search_missing_query_returns_400(self, client):
|
| 114 |
-
resp = _post(client, "/
|
| 115 |
assert resp.status_code == 400
|
| 116 |
|
| 117 |
def test_search_empty_query_returns_400(self, client):
|
| 118 |
-
resp = _post(client, "/
|
| 119 |
assert resp.status_code == 400
|
| 120 |
|
| 121 |
|
| 122 |
# ---------------------------------------------------------------------------
|
| 123 |
-
# GET /
|
| 124 |
# ---------------------------------------------------------------------------
|
| 125 |
|
| 126 |
class TestFolders:
|
| 127 |
def test_get_folders_returns_200(self, client):
|
| 128 |
# Ensure at least one folder exists from earlier tests
|
| 129 |
-
_post(client, "/
|
| 130 |
"folderPath": "/home/user/folders-check",
|
| 131 |
"agentId": "kiro",
|
| 132 |
"text": "Check folders endpoint",
|
| 133 |
"timestamp": _now(),
|
| 134 |
})
|
| 135 |
-
resp = client.get("/
|
| 136 |
assert resp.status_code == 200
|
| 137 |
data = resp.get_json()
|
| 138 |
assert "folders" in data
|
|
@@ -140,12 +143,12 @@ class TestFolders:
|
|
| 140 |
|
| 141 |
|
| 142 |
# ---------------------------------------------------------------------------
|
| 143 |
-
# GET /
|
| 144 |
# ---------------------------------------------------------------------------
|
| 145 |
|
| 146 |
class TestHealth:
|
| 147 |
def test_health_returns_200(self, client):
|
| 148 |
-
resp = client.get("/
|
| 149 |
assert resp.status_code == 200
|
| 150 |
data = resp.get_json()
|
| 151 |
assert "folderCount" in data
|
|
@@ -153,29 +156,29 @@ class TestHealth:
|
|
| 153 |
assert "memoryCount" in data
|
| 154 |
|
| 155 |
def test_health_status_ok(self, client):
|
| 156 |
-
resp = client.get("/
|
| 157 |
data = resp.get_json()
|
| 158 |
assert data.get("status") in ("ok", "degraded")
|
| 159 |
|
| 160 |
|
| 161 |
# ---------------------------------------------------------------------------
|
| 162 |
-
# GET /
|
| 163 |
# ---------------------------------------------------------------------------
|
| 164 |
|
| 165 |
class TestLivez:
|
| 166 |
def test_livez_returns_200_no_auth(self, client):
|
| 167 |
-
resp = client.get("/
|
| 168 |
assert resp.status_code == 200
|
| 169 |
data = resp.get_json()
|
| 170 |
assert data["status"] == "ok"
|
| 171 |
|
| 172 |
def test_livez_open_with_secret_set(self, client):
|
| 173 |
-
os.environ["
|
| 174 |
try:
|
| 175 |
-
resp = client.get("/
|
| 176 |
assert resp.status_code == 200
|
| 177 |
finally:
|
| 178 |
-
del os.environ["
|
| 179 |
|
| 180 |
|
| 181 |
# ---------------------------------------------------------------------------
|
|
@@ -184,35 +187,35 @@ class TestLivez:
|
|
| 184 |
|
| 185 |
class TestAuthentication:
|
| 186 |
def test_protected_endpoint_returns_401_with_wrong_token(self, client):
|
| 187 |
-
os.environ["
|
| 188 |
try:
|
| 189 |
resp = client.get(
|
| 190 |
-
"/
|
| 191 |
headers={"Authorization": "Bearer wrong-token"},
|
| 192 |
)
|
| 193 |
assert resp.status_code == 401
|
| 194 |
finally:
|
| 195 |
-
del os.environ["
|
| 196 |
|
| 197 |
def test_protected_endpoint_passes_with_correct_token(self, client):
|
| 198 |
secret = "my-test-secret-xyz"
|
| 199 |
-
os.environ["
|
| 200 |
try:
|
| 201 |
resp = client.get(
|
| 202 |
-
"/
|
| 203 |
headers={"Authorization": f"Bearer {secret}"},
|
| 204 |
)
|
| 205 |
assert resp.status_code == 200
|
| 206 |
finally:
|
| 207 |
-
del os.environ["
|
| 208 |
|
| 209 |
def test_livez_always_open_regardless_of_secret(self, client):
|
| 210 |
-
os.environ["
|
| 211 |
try:
|
| 212 |
-
resp = client.get("/
|
| 213 |
assert resp.status_code == 200
|
| 214 |
finally:
|
| 215 |
-
del os.environ["
|
| 216 |
|
| 217 |
|
| 218 |
# ---------------------------------------------------------------------------
|
|
@@ -221,34 +224,34 @@ class TestAuthentication:
|
|
| 221 |
|
| 222 |
class TestMemoriesEndpoint:
|
| 223 |
def test_memories_list_returns_200(self, client):
|
| 224 |
-
resp = client.get("/
|
| 225 |
assert resp.status_code == 200
|
| 226 |
data = resp.get_json()
|
| 227 |
assert "memories" in data
|
| 228 |
|
| 229 |
def test_remember_valid_payload_returns_201(self, client):
|
| 230 |
-
resp = _post(client, "/
|
| 231 |
"content": "API test memory content",
|
| 232 |
"type": "fact",
|
| 233 |
})
|
| 234 |
assert resp.status_code == 201
|
| 235 |
|
| 236 |
def test_forget_nonexistent_id(self, client):
|
| 237 |
-
resp = _post(client, "/
|
| 238 |
assert resp.status_code == 200
|
| 239 |
|
| 240 |
def test_graph_endpoint_returns_200(self, client):
|
| 241 |
-
resp = client.get("/
|
| 242 |
assert resp.status_code == 200
|
| 243 |
data = resp.get_json()
|
| 244 |
assert "nodes" in data
|
| 245 |
assert "edges" in data
|
| 246 |
|
| 247 |
def test_mcp_tools_list_returns_200(self, client):
|
| 248 |
-
resp = client.get("/
|
| 249 |
assert resp.status_code == 200
|
| 250 |
data = resp.get_json()
|
| 251 |
assert "tools" in data
|
| 252 |
tool_names = {t["name"] for t in data["tools"]}
|
| 253 |
assert "agent_observe" in tool_names
|
| 254 |
-
assert "
|
|
|
|
| 21 |
def flask_app(tmp_path_factory):
|
| 22 |
tmp_dir = tmp_path_factory.mktemp("api_test_db")
|
| 23 |
db_path = str(tmp_dir / "test.db")
|
| 24 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 25 |
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 26 |
|
| 27 |
from db import StateKV
|
|
|
|
| 32 |
|
| 33 |
StateKV.__init__ = patched_init
|
| 34 |
import app as app_module
|
| 35 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 36 |
+
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 37 |
flask_application = app_module.create_app()
|
| 38 |
StateKV.__init__ = original_init
|
| 39 |
flask_application.config["TESTING"] = True
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
# ---------------------------------------------------------------------------
|
| 57 |
+
# POST /agentcache/agent/observe
|
| 58 |
# ---------------------------------------------------------------------------
|
| 59 |
|
| 60 |
class TestAgentObserve:
|
| 61 |
def test_valid_payload_returns_201(self, client):
|
| 62 |
+
resp = _post(client, "/agentcache/agent/observe", {
|
| 63 |
"folderPath": "/home/user/test-project",
|
| 64 |
"agentId": "kiro",
|
| 65 |
"text": "Implemented new authentication middleware",
|
|
|
|
| 71 |
assert data["observationId"].startswith("fobs_")
|
| 72 |
|
| 73 |
def test_missing_folder_path_returns_400(self, client):
|
| 74 |
+
resp = _post(client, "/agentcache/agent/observe", {
|
| 75 |
"agentId": "kiro",
|
| 76 |
"text": "Some work",
|
| 77 |
"timestamp": _now(),
|
|
|
|
| 79 |
assert resp.status_code == 400
|
| 80 |
|
| 81 |
def test_missing_agent_id_returns_400(self, client):
|
| 82 |
+
resp = _post(client, "/agentcache/agent/observe", {
|
| 83 |
"folderPath": "/home/user/proj",
|
| 84 |
"text": "Some work",
|
| 85 |
"timestamp": _now(),
|
|
|
|
| 87 |
assert resp.status_code == 400
|
| 88 |
|
| 89 |
def test_missing_text_returns_400(self, client):
|
| 90 |
+
resp = _post(client, "/agentcache/agent/observe", {
|
| 91 |
"folderPath": "/home/user/proj",
|
| 92 |
"agentId": "kiro",
|
| 93 |
"timestamp": _now(),
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
# ---------------------------------------------------------------------------
|
| 99 |
+
# POST /agentcache/search
|
| 100 |
# ---------------------------------------------------------------------------
|
| 101 |
|
| 102 |
class TestSearch:
|
| 103 |
def test_search_with_query_returns_200(self, client):
|
| 104 |
# Seed data first
|
| 105 |
+
_post(client, "/agentcache/agent/observe", {
|
| 106 |
"folderPath": "/home/user/search-proj",
|
| 107 |
"agentId": "kiro",
|
| 108 |
"text": "Refactored the authentication system",
|
| 109 |
"timestamp": _now(),
|
| 110 |
})
|
| 111 |
+
resp = _post(client, "/agentcache/search", {"query": "authentication"})
|
| 112 |
assert resp.status_code == 200
|
| 113 |
data = resp.get_json()
|
| 114 |
assert isinstance(data, list) or isinstance(data, dict)
|
| 115 |
|
| 116 |
def test_search_missing_query_returns_400(self, client):
|
| 117 |
+
resp = _post(client, "/agentcache/search", {})
|
| 118 |
assert resp.status_code == 400
|
| 119 |
|
| 120 |
def test_search_empty_query_returns_400(self, client):
|
| 121 |
+
resp = _post(client, "/agentcache/search", {"query": " "})
|
| 122 |
assert resp.status_code == 400
|
| 123 |
|
| 124 |
|
| 125 |
# ---------------------------------------------------------------------------
|
| 126 |
+
# GET /agentcache/folders
|
| 127 |
# ---------------------------------------------------------------------------
|
| 128 |
|
| 129 |
class TestFolders:
|
| 130 |
def test_get_folders_returns_200(self, client):
|
| 131 |
# Ensure at least one folder exists from earlier tests
|
| 132 |
+
_post(client, "/agentcache/agent/observe", {
|
| 133 |
"folderPath": "/home/user/folders-check",
|
| 134 |
"agentId": "kiro",
|
| 135 |
"text": "Check folders endpoint",
|
| 136 |
"timestamp": _now(),
|
| 137 |
})
|
| 138 |
+
resp = client.get("/agentcache/folders")
|
| 139 |
assert resp.status_code == 200
|
| 140 |
data = resp.get_json()
|
| 141 |
assert "folders" in data
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
# ---------------------------------------------------------------------------
|
| 146 |
+
# GET /agentcache/health
|
| 147 |
# ---------------------------------------------------------------------------
|
| 148 |
|
| 149 |
class TestHealth:
|
| 150 |
def test_health_returns_200(self, client):
|
| 151 |
+
resp = client.get("/agentcache/health")
|
| 152 |
assert resp.status_code == 200
|
| 153 |
data = resp.get_json()
|
| 154 |
assert "folderCount" in data
|
|
|
|
| 156 |
assert "memoryCount" in data
|
| 157 |
|
| 158 |
def test_health_status_ok(self, client):
|
| 159 |
+
resp = client.get("/agentcache/health")
|
| 160 |
data = resp.get_json()
|
| 161 |
assert data.get("status") in ("ok", "degraded")
|
| 162 |
|
| 163 |
|
| 164 |
# ---------------------------------------------------------------------------
|
| 165 |
+
# GET /agentcache/livez
|
| 166 |
# ---------------------------------------------------------------------------
|
| 167 |
|
| 168 |
class TestLivez:
|
| 169 |
def test_livez_returns_200_no_auth(self, client):
|
| 170 |
+
resp = client.get("/agentcache/livez")
|
| 171 |
assert resp.status_code == 200
|
| 172 |
data = resp.get_json()
|
| 173 |
assert data["status"] == "ok"
|
| 174 |
|
| 175 |
def test_livez_open_with_secret_set(self, client):
|
| 176 |
+
os.environ["AGENTCACHE_SECRET"] = "test-secret-123"
|
| 177 |
try:
|
| 178 |
+
resp = client.get("/agentcache/livez")
|
| 179 |
assert resp.status_code == 200
|
| 180 |
finally:
|
| 181 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 182 |
|
| 183 |
|
| 184 |
# ---------------------------------------------------------------------------
|
|
|
|
| 187 |
|
| 188 |
class TestAuthentication:
|
| 189 |
def test_protected_endpoint_returns_401_with_wrong_token(self, client):
|
| 190 |
+
os.environ["AGENTCACHE_SECRET"] = "correct-secret"
|
| 191 |
try:
|
| 192 |
resp = client.get(
|
| 193 |
+
"/agentcache/audit",
|
| 194 |
headers={"Authorization": "Bearer wrong-token"},
|
| 195 |
)
|
| 196 |
assert resp.status_code == 401
|
| 197 |
finally:
|
| 198 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 199 |
|
| 200 |
def test_protected_endpoint_passes_with_correct_token(self, client):
|
| 201 |
secret = "my-test-secret-xyz"
|
| 202 |
+
os.environ["AGENTCACHE_SECRET"] = secret
|
| 203 |
try:
|
| 204 |
resp = client.get(
|
| 205 |
+
"/agentcache/audit",
|
| 206 |
headers={"Authorization": f"Bearer {secret}"},
|
| 207 |
)
|
| 208 |
assert resp.status_code == 200
|
| 209 |
finally:
|
| 210 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 211 |
|
| 212 |
def test_livez_always_open_regardless_of_secret(self, client):
|
| 213 |
+
os.environ["AGENTCACHE_SECRET"] = "any-secret"
|
| 214 |
try:
|
| 215 |
+
resp = client.get("/agentcache/livez")
|
| 216 |
assert resp.status_code == 200
|
| 217 |
finally:
|
| 218 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 219 |
|
| 220 |
|
| 221 |
# ---------------------------------------------------------------------------
|
|
|
|
| 224 |
|
| 225 |
class TestMemoriesEndpoint:
|
| 226 |
def test_memories_list_returns_200(self, client):
|
| 227 |
+
resp = client.get("/agentcache/memories")
|
| 228 |
assert resp.status_code == 200
|
| 229 |
data = resp.get_json()
|
| 230 |
assert "memories" in data
|
| 231 |
|
| 232 |
def test_remember_valid_payload_returns_201(self, client):
|
| 233 |
+
resp = _post(client, "/agentcache/remember", {
|
| 234 |
"content": "API test memory content",
|
| 235 |
"type": "fact",
|
| 236 |
})
|
| 237 |
assert resp.status_code == 201
|
| 238 |
|
| 239 |
def test_forget_nonexistent_id(self, client):
|
| 240 |
+
resp = _post(client, "/agentcache/forget", {"memoryId": "mem_nonexistent"})
|
| 241 |
assert resp.status_code == 200
|
| 242 |
|
| 243 |
def test_graph_endpoint_returns_200(self, client):
|
| 244 |
+
resp = client.get("/agentcache/graph")
|
| 245 |
assert resp.status_code == 200
|
| 246 |
data = resp.get_json()
|
| 247 |
assert "nodes" in data
|
| 248 |
assert "edges" in data
|
| 249 |
|
| 250 |
def test_mcp_tools_list_returns_200(self, client):
|
| 251 |
+
resp = client.get("/agentcache/mcp/tools")
|
| 252 |
assert resp.status_code == 200
|
| 253 |
data = resp.get_json()
|
| 254 |
assert "tools" in data
|
| 255 |
tool_names = {t["name"] for t in data["tools"]}
|
| 256 |
assert "agent_observe" in tool_names
|
| 257 |
+
assert "cache_recall" in tool_names
|
tests/test_context.py
CHANGED
|
@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
| 18 |
|
| 19 |
def _make_kv(tmp_path):
|
| 20 |
from db import StateKV
|
| 21 |
-
os.environ.pop("
|
| 22 |
return StateKV(db_path=str(tmp_path / "test.db"))
|
| 23 |
|
| 24 |
|
|
@@ -83,7 +83,7 @@ class TestContext:
|
|
| 83 |
assert result["tokens"] <= budget + 50 # small headroom for header/footer
|
| 84 |
|
| 85 |
def test_context_includes_xml_wrapper(self, tmp_path):
|
| 86 |
-
"""Non-empty context should be wrapped in <
|
| 87 |
from functions import context, lesson_save
|
| 88 |
kv = _make_kv(tmp_path)
|
| 89 |
project = "/home/user/xml-test"
|
|
@@ -101,8 +101,8 @@ class TestContext:
|
|
| 101 |
})
|
| 102 |
|
| 103 |
if result["blocks"] > 0:
|
| 104 |
-
assert "<
|
| 105 |
-
assert "</
|
| 106 |
|
| 107 |
def test_token_budget_env_var_respected(self, tmp_path, monkeypatch):
|
| 108 |
"""TOKEN_BUDGET env var should be used when no budget param given."""
|
|
|
|
| 18 |
|
| 19 |
def _make_kv(tmp_path):
|
| 20 |
from db import StateKV
|
| 21 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 22 |
return StateKV(db_path=str(tmp_path / "test.db"))
|
| 23 |
|
| 24 |
|
|
|
|
| 83 |
assert result["tokens"] <= budget + 50 # small headroom for header/footer
|
| 84 |
|
| 85 |
def test_context_includes_xml_wrapper(self, tmp_path):
|
| 86 |
+
"""Non-empty context should be wrapped in <agentcache-context>."""
|
| 87 |
from functions import context, lesson_save
|
| 88 |
kv = _make_kv(tmp_path)
|
| 89 |
project = "/home/user/xml-test"
|
|
|
|
| 101 |
})
|
| 102 |
|
| 103 |
if result["blocks"] > 0:
|
| 104 |
+
assert "<agentcache-context" in result["context"]
|
| 105 |
+
assert "</agentcache-context>" in result["context"]
|
| 106 |
|
| 107 |
def test_token_budget_env_var_respected(self, tmp_path, monkeypatch):
|
| 108 |
"""TOKEN_BUDGET env var should be used when no budget param given."""
|
tests/test_folder_observe.py
CHANGED
|
@@ -70,8 +70,8 @@ class TestFolderObserveSuccess:
|
|
| 70 |
|
| 71 |
def test_obs_count_incremented(self, tmp_path):
|
| 72 |
kv = make_kv(tmp_path)
|
| 73 |
-
folder_observe(kv, base_payload())
|
| 74 |
-
folder_observe(kv, base_payload())
|
| 75 |
fp = 'home/user/projects/myapp'
|
| 76 |
meta = kv.get(KV.folder_meta(fp, 'kiro'), 'meta')
|
| 77 |
assert meta is not None
|
|
@@ -99,10 +99,10 @@ class TestFolderObserveCap:
|
|
| 99 |
def test_cap_enforced(self, tmp_path, monkeypatch):
|
| 100 |
monkeypatch.setenv('MAX_OBS_PER_FOLDER', '3')
|
| 101 |
kv = make_kv(tmp_path)
|
| 102 |
-
for
|
| 103 |
-
folder_observe(kv, base_payload())
|
| 104 |
with pytest.raises(ValueError, match='limit'):
|
| 105 |
-
folder_observe(kv, base_payload())
|
| 106 |
|
| 107 |
|
| 108 |
class TestFolderObservePairIsolation:
|
|
|
|
| 70 |
|
| 71 |
def test_obs_count_incremented(self, tmp_path):
|
| 72 |
kv = make_kv(tmp_path)
|
| 73 |
+
folder_observe(kv, base_payload(text="First observation"))
|
| 74 |
+
folder_observe(kv, base_payload(text="Second observation"))
|
| 75 |
fp = 'home/user/projects/myapp'
|
| 76 |
meta = kv.get(KV.folder_meta(fp, 'kiro'), 'meta')
|
| 77 |
assert meta is not None
|
|
|
|
| 99 |
def test_cap_enforced(self, tmp_path, monkeypatch):
|
| 100 |
monkeypatch.setenv('MAX_OBS_PER_FOLDER', '3')
|
| 101 |
kv = make_kv(tmp_path)
|
| 102 |
+
for i in range(3):
|
| 103 |
+
folder_observe(kv, base_payload(text=f"observation {i}"))
|
| 104 |
with pytest.raises(ValueError, match='limit'):
|
| 105 |
+
folder_observe(kv, base_payload(text="observation 4"))
|
| 106 |
|
| 107 |
|
| 108 |
class TestFolderObservePairIsolation:
|
tests/test_properties.py
CHANGED
|
@@ -38,7 +38,7 @@ _counter = [0]
|
|
| 38 |
def _fresh_kv():
|
| 39 |
"""Create a brand-new isolated StateKV in a temp directory."""
|
| 40 |
from db import StateKV
|
| 41 |
-
os.environ.pop("
|
| 42 |
_counter[0] += 1
|
| 43 |
d = tempfile.mkdtemp(prefix=f"agmem_prop_{_counter[0]}_")
|
| 44 |
return StateKV(db_path=os.path.join(d, "test.db"))
|
|
|
|
| 38 |
def _fresh_kv():
|
| 39 |
"""Create a brand-new isolated StateKV in a temp directory."""
|
| 40 |
from db import StateKV
|
| 41 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 42 |
_counter[0] += 1
|
| 43 |
d = tempfile.mkdtemp(prefix=f"agmem_prop_{_counter[0]}_")
|
| 44 |
return StateKV(db_path=os.path.join(d, "test.db"))
|
tests/test_remember.py
CHANGED
|
@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
| 18 |
|
| 19 |
def _make_kv(tmp_path):
|
| 20 |
from db import StateKV
|
| 21 |
-
os.environ.pop("
|
| 22 |
return StateKV(db_path=str(tmp_path / "test.db"))
|
| 23 |
|
| 24 |
|
|
|
|
| 18 |
|
| 19 |
def _make_kv(tmp_path):
|
| 20 |
from db import StateKV
|
| 21 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 22 |
return StateKV(db_path=str(tmp_path / "test.db"))
|
| 23 |
|
| 24 |
|
tests/test_route_regressions.py
CHANGED
|
@@ -6,7 +6,7 @@ the expected HTTP status codes after the monolithic app.py was split into
|
|
| 6 |
src/routes/{observations, memories, search, graph, health, mcp, migration}.py.
|
| 7 |
|
| 8 |
Tests use the Flask test client — no running server required.
|
| 9 |
-
The
|
| 10 |
"""
|
| 11 |
import sys
|
| 12 |
import os
|
|
@@ -36,11 +36,14 @@ def flask_app(tmp_path_factory):
|
|
| 36 |
db_path = str(tmp_dir / "test.db")
|
| 37 |
|
| 38 |
# Point the server at an isolated DB
|
| 39 |
-
os.environ["
|
| 40 |
# Ensure no auth requirement during tests
|
|
|
|
| 41 |
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 42 |
|
| 43 |
import app as app_module
|
|
|
|
|
|
|
| 44 |
from db import StateKV
|
| 45 |
|
| 46 |
# Patch StateKV to use tmp db before create_app() initialises it
|
|
@@ -81,28 +84,28 @@ def _post_json(client, url, payload):
|
|
| 81 |
|
| 82 |
# ===========================================================================
|
| 83 |
# Blueprint 1: health.py
|
| 84 |
-
# GET /
|
| 85 |
-
# GET /
|
| 86 |
-
# GET /
|
| 87 |
-
# GET /
|
| 88 |
# ===========================================================================
|
| 89 |
|
| 90 |
class TestHealthBlueprint:
|
| 91 |
def test_livez_no_auth_required(self, client):
|
| 92 |
"""GET /livez must respond 200 without any auth token (always open)."""
|
| 93 |
-
resp = client.get("/
|
| 94 |
assert resp.status_code == 200
|
| 95 |
data = resp.get_json()
|
| 96 |
assert data["status"] == "ok"
|
| 97 |
assert "service" in data
|
| 98 |
|
| 99 |
def test_livez_returns_service_name(self, client):
|
| 100 |
-
resp = client.get("/
|
| 101 |
data = resp.get_json()
|
| 102 |
-
assert data["service"] == "
|
| 103 |
|
| 104 |
def test_health_returns_200(self, client):
|
| 105 |
-
resp = client.get("/
|
| 106 |
assert resp.status_code == 200
|
| 107 |
data = resp.get_json()
|
| 108 |
# Folder-based health check fields (REQ-047)
|
|
@@ -111,13 +114,13 @@ class TestHealthBlueprint:
|
|
| 111 |
assert "memoryCount" in data
|
| 112 |
|
| 113 |
def test_audit_returns_200(self, client):
|
| 114 |
-
resp = client.get("/
|
| 115 |
assert resp.status_code == 200
|
| 116 |
data = resp.get_json()
|
| 117 |
assert "entries" in data
|
| 118 |
|
| 119 |
def test_config_flags_returns_200(self, client):
|
| 120 |
-
resp = client.get("/
|
| 121 |
assert resp.status_code == 200
|
| 122 |
data = resp.get_json()
|
| 123 |
assert "flags" in data
|
|
@@ -126,10 +129,10 @@ class TestHealthBlueprint:
|
|
| 126 |
|
| 127 |
# ===========================================================================
|
| 128 |
# Blueprint 2: observations.py
|
| 129 |
-
# POST /
|
| 130 |
-
# POST /
|
| 131 |
-
# GET /
|
| 132 |
-
# GET /
|
| 133 |
# ===========================================================================
|
| 134 |
|
| 135 |
class TestObservationsBlueprint:
|
|
@@ -141,7 +144,7 @@ class TestObservationsBlueprint:
|
|
| 141 |
"text": "Added a new feature to the app",
|
| 142 |
"timestamp": _now_iso(),
|
| 143 |
}
|
| 144 |
-
resp = _post_json(client, "/
|
| 145 |
assert resp.status_code == 201
|
| 146 |
data = resp.get_json()
|
| 147 |
assert "observationId" in data
|
|
@@ -150,31 +153,31 @@ class TestObservationsBlueprint:
|
|
| 150 |
def test_agent_observe_missing_folder_path_returns_400(self, client):
|
| 151 |
"""POST /agent/observe missing folderPath returns 400."""
|
| 152 |
payload = {"agentId": "kiro", "text": "some work", "timestamp": _now_iso()}
|
| 153 |
-
resp = _post_json(client, "/
|
| 154 |
assert resp.status_code == 400
|
| 155 |
|
| 156 |
def test_agent_observe_missing_agent_id_returns_400(self, client):
|
| 157 |
"""POST /agent/observe missing agentId returns 400."""
|
| 158 |
payload = {"folderPath": "/home/user/proj", "text": "some work", "timestamp": _now_iso()}
|
| 159 |
-
resp = _post_json(client, "/
|
| 160 |
assert resp.status_code == 400
|
| 161 |
|
| 162 |
def test_agent_observe_missing_text_returns_400(self, client):
|
| 163 |
"""POST /agent/observe missing text returns 400."""
|
| 164 |
payload = {"folderPath": "/home/user/proj", "agentId": "kiro", "timestamp": _now_iso()}
|
| 165 |
-
resp = _post_json(client, "/
|
| 166 |
assert resp.status_code == 400
|
| 167 |
|
| 168 |
def test_folders_list_returns_200(self, client):
|
| 169 |
"""GET /folders returns 200 with a folders list."""
|
| 170 |
# Seed at least one observation first
|
| 171 |
-
_post_json(client, "/
|
| 172 |
"folderPath": "/home/user/proj-folders-test",
|
| 173 |
"agentId": "kiro",
|
| 174 |
"text": "Folders test observation",
|
| 175 |
"timestamp": _now_iso(),
|
| 176 |
})
|
| 177 |
-
resp = client.get("/
|
| 178 |
assert resp.status_code == 200
|
| 179 |
data = resp.get_json()
|
| 180 |
assert "folders" in data
|
|
@@ -184,20 +187,20 @@ class TestObservationsBlueprint:
|
|
| 184 |
"""GET /folder/observations with valid params returns 200."""
|
| 185 |
# Seed an observation first
|
| 186 |
fp = "/home/user/proj-obs-test"
|
| 187 |
-
_post_json(client, "/
|
| 188 |
"folderPath": fp,
|
| 189 |
"agentId": "kiro",
|
| 190 |
"text": "Obs for folder/observations test",
|
| 191 |
"timestamp": _now_iso(),
|
| 192 |
})
|
| 193 |
-
resp = client.get("/
|
| 194 |
assert resp.status_code == 200
|
| 195 |
data = resp.get_json()
|
| 196 |
assert "observations" in data
|
| 197 |
|
| 198 |
def test_folder_observations_missing_params_returns_400(self, client):
|
| 199 |
"""GET /folder/observations without required params returns 400."""
|
| 200 |
-
resp = client.get("/
|
| 201 |
assert resp.status_code == 400
|
| 202 |
|
| 203 |
def test_legacy_observe_endpoint_returns_400_or_201(self, client):
|
|
@@ -208,17 +211,17 @@ class TestObservationsBlueprint:
|
|
| 208 |
"text": "legacy observe call",
|
| 209 |
"timestamp": _now_iso(),
|
| 210 |
}
|
| 211 |
-
resp = _post_json(client, "/
|
| 212 |
# The legacy endpoint exists — expect either success or a controlled error, never 404
|
| 213 |
assert resp.status_code != 404
|
| 214 |
|
| 215 |
|
| 216 |
# ===========================================================================
|
| 217 |
# Blueprint 3: memories.py
|
| 218 |
-
# POST /
|
| 219 |
-
# POST /
|
| 220 |
-
# GET /
|
| 221 |
-
# POST /
|
| 222 |
# ===========================================================================
|
| 223 |
|
| 224 |
class TestMemoriesBlueprint:
|
|
@@ -229,12 +232,12 @@ class TestMemoriesBlueprint:
|
|
| 229 |
"type": "fact",
|
| 230 |
"concepts": ["sql", "security"],
|
| 231 |
}
|
| 232 |
-
resp = _post_json(client, "/
|
| 233 |
assert resp.status_code == 201
|
| 234 |
data = resp.get_json()
|
| 235 |
assert "memory" in data
|
| 236 |
|
| 237 |
-
def
|
| 238 |
"""POST /agent/remember with content returns 201."""
|
| 239 |
payload = {
|
| 240 |
"content": "The project uses SQLite with WAL mode",
|
|
@@ -242,17 +245,17 @@ class TestMemoriesBlueprint:
|
|
| 242 |
"type": "architecture",
|
| 243 |
"concepts": ["sqlite", "wal"],
|
| 244 |
}
|
| 245 |
-
resp = _post_json(client, "/
|
| 246 |
assert resp.status_code == 201
|
| 247 |
|
| 248 |
-
def
|
| 249 |
"""POST /agent/remember without content returns 400."""
|
| 250 |
-
resp = _post_json(client, "/
|
| 251 |
assert resp.status_code == 400
|
| 252 |
|
| 253 |
def test_memories_list_returns_200(self, client):
|
| 254 |
"""GET /memories returns 200 with memories list."""
|
| 255 |
-
resp = client.get("/
|
| 256 |
assert resp.status_code == 200
|
| 257 |
data = resp.get_json()
|
| 258 |
assert "memories" in data
|
|
@@ -260,7 +263,7 @@ class TestMemoriesBlueprint:
|
|
| 260 |
|
| 261 |
def test_memories_list_latest_only(self, client):
|
| 262 |
"""GET /memories?latest=true filters to only latest memories."""
|
| 263 |
-
resp = client.get("/
|
| 264 |
assert resp.status_code == 200
|
| 265 |
data = resp.get_json()
|
| 266 |
# All returned memories should be latest=True or isLatest not False
|
|
@@ -270,13 +273,13 @@ class TestMemoriesBlueprint:
|
|
| 270 |
def test_forget_memory_by_id(self, client):
|
| 271 |
"""POST /forget with memoryId deletes a global memory."""
|
| 272 |
# Create a memory first
|
| 273 |
-
create_resp = _post_json(client, "/
|
| 274 |
"content": "This memory will be forgotten",
|
| 275 |
"type": "fact",
|
| 276 |
})
|
| 277 |
mem_id = create_resp.get_json()["memory"]["id"]
|
| 278 |
# Forget it
|
| 279 |
-
resp = _post_json(client, "/
|
| 280 |
assert resp.status_code == 200
|
| 281 |
data = resp.get_json()
|
| 282 |
assert data["deleted"] >= 1
|
|
@@ -285,13 +288,13 @@ class TestMemoriesBlueprint:
|
|
| 285 |
"""POST /forget with folderPath+agentId clears folder observations."""
|
| 286 |
# Seed an observation
|
| 287 |
fp = "/home/user/proj-to-forget"
|
| 288 |
-
_post_json(client, "/
|
| 289 |
"folderPath": fp,
|
| 290 |
"agentId": "kiro",
|
| 291 |
"text": "Some work that will be forgotten",
|
| 292 |
"timestamp": _now_iso(),
|
| 293 |
})
|
| 294 |
-
resp = _post_json(client, "/
|
| 295 |
"folderPath": fp,
|
| 296 |
"agentId": "kiro",
|
| 297 |
})
|
|
@@ -303,31 +306,31 @@ class TestMemoriesBlueprint:
|
|
| 303 |
|
| 304 |
# ===========================================================================
|
| 305 |
# Blueprint 4: search.py
|
| 306 |
-
# POST /
|
| 307 |
-
# POST /
|
| 308 |
# ===========================================================================
|
| 309 |
|
| 310 |
class TestSearchBlueprint:
|
| 311 |
def test_search_with_query_returns_200(self, client):
|
| 312 |
"""POST /search with a query returns 200."""
|
| 313 |
# Seed something searchable first
|
| 314 |
-
_post_json(client, "/
|
| 315 |
"folderPath": "/home/user/proj-search-test",
|
| 316 |
"agentId": "kiro",
|
| 317 |
"text": "Implemented authentication middleware for the app",
|
| 318 |
"timestamp": _now_iso(),
|
| 319 |
})
|
| 320 |
-
resp = _post_json(client, "/
|
| 321 |
assert resp.status_code == 200
|
| 322 |
|
| 323 |
def test_search_missing_query_returns_400(self, client):
|
| 324 |
"""POST /search without query returns 400."""
|
| 325 |
-
resp = _post_json(client, "/
|
| 326 |
assert resp.status_code == 400
|
| 327 |
|
| 328 |
def test_search_with_folder_filter(self, client):
|
| 329 |
"""POST /search with folderPath filter returns 200."""
|
| 330 |
-
resp = _post_json(client, "/
|
| 331 |
"query": "test",
|
| 332 |
"folderPath": "/home/user/proj-search-test",
|
| 333 |
"agentId": "kiro",
|
|
@@ -336,7 +339,7 @@ class TestSearchBlueprint:
|
|
| 336 |
|
| 337 |
def test_timeline_returns_200(self, client):
|
| 338 |
"""POST /timeline returns 200 with observations list."""
|
| 339 |
-
resp = _post_json(client, "/
|
| 340 |
assert resp.status_code == 200
|
| 341 |
data = resp.get_json()
|
| 342 |
assert "observations" in data
|
|
@@ -344,7 +347,7 @@ class TestSearchBlueprint:
|
|
| 344 |
|
| 345 |
def test_timeline_with_filters_returns_200(self, client):
|
| 346 |
"""POST /timeline with folder/agent filters returns 200."""
|
| 347 |
-
resp = _post_json(client, "/
|
| 348 |
"folderPath": "/home/user/proj-search-test",
|
| 349 |
"agentId": "kiro",
|
| 350 |
"limit": 10,
|
|
@@ -357,11 +360,11 @@ class TestSearchBlueprint:
|
|
| 357 |
# Seed observations with distinct timestamps
|
| 358 |
for i in range(3):
|
| 359 |
ts = datetime.datetime(2025, 6, 1, 10, i, 0).isoformat() + "Z"
|
| 360 |
-
_post_json(client, "/
|
| 361 |
"folderPath": fp, "agentId": "kiro",
|
| 362 |
"text": f"Observation {i}", "timestamp": ts,
|
| 363 |
})
|
| 364 |
-
resp = _post_json(client, "/
|
| 365 |
"folderPath": fp, "agentId": "kiro",
|
| 366 |
})
|
| 367 |
assert resp.status_code == 200
|
|
@@ -372,16 +375,16 @@ class TestSearchBlueprint:
|
|
| 372 |
|
| 373 |
# ===========================================================================
|
| 374 |
# Blueprint 5: graph.py
|
| 375 |
-
# GET /
|
| 376 |
-
# GET /
|
| 377 |
-
# POST /
|
| 378 |
-
# POST /
|
| 379 |
# ===========================================================================
|
| 380 |
|
| 381 |
class TestGraphBlueprint:
|
| 382 |
def test_graph_returns_200(self, client):
|
| 383 |
"""GET /graph returns 200 with nodes and edges."""
|
| 384 |
-
resp = client.get("/
|
| 385 |
assert resp.status_code == 200
|
| 386 |
data = resp.get_json()
|
| 387 |
assert "nodes" in data
|
|
@@ -389,7 +392,7 @@ class TestGraphBlueprint:
|
|
| 389 |
|
| 390 |
def test_graph_stats_returns_200(self, client):
|
| 391 |
"""GET /graph/stats returns 200."""
|
| 392 |
-
resp = client.get("/
|
| 393 |
assert resp.status_code == 200
|
| 394 |
data = resp.get_json()
|
| 395 |
assert "nodes" in data
|
|
@@ -397,24 +400,24 @@ class TestGraphBlueprint:
|
|
| 397 |
|
| 398 |
def test_graph_query_returns_200(self, client):
|
| 399 |
"""POST /graph/query returns 200."""
|
| 400 |
-
resp = _post_json(client, "/
|
| 401 |
assert resp.status_code == 200
|
| 402 |
|
| 403 |
def test_graph_build_returns_200(self, client):
|
| 404 |
"""POST /graph/build returns 200."""
|
| 405 |
-
resp = _post_json(client, "/
|
| 406 |
assert resp.status_code == 200
|
| 407 |
|
| 408 |
def test_graph_nodes_have_required_fields(self, client):
|
| 409 |
"""Graph nodes contain id, label, folderPath, agentIds, obsCount, color (REQ-024)."""
|
| 410 |
# Seed some data
|
| 411 |
-
_post_json(client, "/
|
| 412 |
"folderPath": "/home/user/proj-graph-check",
|
| 413 |
"agentId": "kiro",
|
| 414 |
"text": "Graph node field check",
|
| 415 |
"timestamp": _now_iso(),
|
| 416 |
})
|
| 417 |
-
resp = client.get("/
|
| 418 |
data = resp.get_json()
|
| 419 |
if data["nodes"]:
|
| 420 |
node = data["nodes"][0]
|
|
@@ -424,14 +427,14 @@ class TestGraphBlueprint:
|
|
| 424 |
|
| 425 |
# ===========================================================================
|
| 426 |
# Blueprint 6: mcp.py
|
| 427 |
-
# GET /
|
| 428 |
-
# POST /
|
| 429 |
# ===========================================================================
|
| 430 |
|
| 431 |
class TestMcpBlueprint:
|
| 432 |
def test_mcp_tools_list_returns_200(self, client):
|
| 433 |
"""GET /mcp/tools returns 200 with a tools list."""
|
| 434 |
-
resp = client.get("/
|
| 435 |
assert resp.status_code == 200
|
| 436 |
data = resp.get_json()
|
| 437 |
assert "tools" in data
|
|
@@ -440,20 +443,20 @@ class TestMcpBlueprint:
|
|
| 440 |
|
| 441 |
def test_mcp_tools_contains_required_tools(self, client):
|
| 442 |
"""The expected MCP tools are all present in the schema."""
|
| 443 |
-
resp = client.get("/
|
| 444 |
tool_names = {t["name"] for t in resp.get_json()["tools"]}
|
| 445 |
required_tools = {
|
| 446 |
"agent_observe",
|
| 447 |
-
"
|
| 448 |
-
"
|
| 449 |
-
"
|
| 450 |
-
"
|
| 451 |
-
"
|
| 452 |
-
"
|
| 453 |
-
"
|
| 454 |
-
"
|
| 455 |
-
"
|
| 456 |
-
"
|
| 457 |
}
|
| 458 |
assert required_tools.issubset(tool_names), \
|
| 459 |
f"Missing tools: {required_tools - tool_names}"
|
|
@@ -469,23 +472,23 @@ class TestMcpBlueprint:
|
|
| 469 |
"timestamp": _now_iso(),
|
| 470 |
},
|
| 471 |
}
|
| 472 |
-
resp = _post_json(client, "/
|
| 473 |
assert resp.status_code == 200
|
| 474 |
data = resp.get_json()
|
| 475 |
assert "content" in data
|
| 476 |
|
| 477 |
-
def
|
| 478 |
-
"""POST /mcp/tools
|
| 479 |
-
resp = _post_json(client, "/
|
| 480 |
-
"name": "
|
| 481 |
"arguments": {"query": "authentication"},
|
| 482 |
})
|
| 483 |
assert resp.status_code == 200
|
| 484 |
|
| 485 |
-
def
|
| 486 |
-
"""POST /mcp/tools
|
| 487 |
-
resp = _post_json(client, "/
|
| 488 |
-
"name": "
|
| 489 |
"arguments": {},
|
| 490 |
})
|
| 491 |
assert resp.status_code == 200
|
|
@@ -494,25 +497,25 @@ class TestMcpBlueprint:
|
|
| 494 |
assert "folderCount" in result
|
| 495 |
assert "observationCount" in result
|
| 496 |
|
| 497 |
-
def
|
| 498 |
-
"""POST /mcp/tools
|
| 499 |
-
resp = _post_json(client, "/
|
| 500 |
-
"name": "
|
| 501 |
"arguments": {},
|
| 502 |
})
|
| 503 |
assert resp.status_code == 200
|
| 504 |
|
| 505 |
-
def
|
| 506 |
-
"""POST /mcp/tools
|
| 507 |
-
resp = _post_json(client, "/
|
| 508 |
-
"name": "
|
| 509 |
"arguments": {"limit": 10},
|
| 510 |
})
|
| 511 |
assert resp.status_code == 200
|
| 512 |
|
| 513 |
def test_mcp_tool_unknown_name_returns_400(self, client):
|
| 514 |
"""POST /mcp/tools with unknown tool name returns 400."""
|
| 515 |
-
resp = _post_json(client, "/
|
| 516 |
"name": "nonexistent_tool_xyz",
|
| 517 |
"arguments": {},
|
| 518 |
})
|
|
@@ -520,19 +523,19 @@ class TestMcpBlueprint:
|
|
| 520 |
|
| 521 |
def test_mcp_tool_missing_name_returns_400(self, client):
|
| 522 |
"""POST /mcp/tools without name returns 400."""
|
| 523 |
-
resp = _post_json(client, "/
|
| 524 |
assert resp.status_code == 400
|
| 525 |
|
| 526 |
|
| 527 |
# ===========================================================================
|
| 528 |
# Blueprint 7: migration.py
|
| 529 |
-
# POST /
|
| 530 |
# ===========================================================================
|
| 531 |
|
| 532 |
class TestMigrationBlueprint:
|
| 533 |
def test_migrate_dry_run_returns_200(self, client):
|
| 534 |
"""POST /migrate with dry_run=true returns 200 with counts."""
|
| 535 |
-
resp = _post_json(client, "/
|
| 536 |
assert resp.status_code == 200
|
| 537 |
data = resp.get_json()
|
| 538 |
assert "migrated_sessions" in data
|
|
@@ -541,7 +544,7 @@ class TestMigrationBlueprint:
|
|
| 541 |
|
| 542 |
def test_migrate_without_body_defaults_to_no_dry_run(self, client):
|
| 543 |
"""POST /migrate with empty body returns 200."""
|
| 544 |
-
resp = _post_json(client, "/
|
| 545 |
assert resp.status_code == 200
|
| 546 |
|
| 547 |
|
|
@@ -555,48 +558,48 @@ class TestBlueprintRegistration:
|
|
| 555 |
registered_rules = {rule.rule for rule in flask_app.url_map.iter_rules()}
|
| 556 |
|
| 557 |
# observations.py
|
| 558 |
-
assert "/
|
| 559 |
-
assert "/
|
| 560 |
-
assert "/
|
| 561 |
|
| 562 |
# memories.py
|
| 563 |
-
assert "/
|
| 564 |
-
assert "/
|
| 565 |
-
assert "/
|
| 566 |
|
| 567 |
# search.py
|
| 568 |
-
assert "/
|
| 569 |
-
assert "/
|
| 570 |
|
| 571 |
# graph.py
|
| 572 |
-
assert "/
|
| 573 |
-
assert "/
|
| 574 |
-
assert "/
|
| 575 |
|
| 576 |
# health.py
|
| 577 |
-
assert "/
|
| 578 |
-
assert "/
|
| 579 |
-
assert "/
|
| 580 |
-
assert "/
|
| 581 |
|
| 582 |
# mcp.py
|
| 583 |
-
assert "/
|
| 584 |
|
| 585 |
# migration.py
|
| 586 |
-
assert "/
|
| 587 |
|
| 588 |
def test_no_routes_return_404_from_blueprints(self, client):
|
| 589 |
"""None of the expected blueprint routes return 404."""
|
| 590 |
get_routes = [
|
| 591 |
-
"/
|
| 592 |
-
"/
|
| 593 |
-
"/
|
| 594 |
-
"/
|
| 595 |
-
"/
|
| 596 |
-
"/
|
| 597 |
-
"/
|
| 598 |
-
"/
|
| 599 |
-
"/
|
| 600 |
]
|
| 601 |
for route in get_routes:
|
| 602 |
resp = client.get(route)
|
|
@@ -605,7 +608,7 @@ class TestBlueprintRegistration:
|
|
| 605 |
def test_cors_headers_present_on_responses(self, client):
|
| 606 |
"""CORS after_request hook adds the expected headers."""
|
| 607 |
resp = client.get(
|
| 608 |
-
"/
|
| 609 |
headers={"Origin": "http://localhost:3000"},
|
| 610 |
)
|
| 611 |
assert resp.status_code == 200
|
|
@@ -614,35 +617,35 @@ class TestBlueprintRegistration:
|
|
| 614 |
assert "Access-Control-Allow-Methods" in resp.headers
|
| 615 |
|
| 616 |
def test_auth_returns_401_when_secret_set(self, client, flask_app):
|
| 617 |
-
"""When
|
| 618 |
|
| 619 |
-
/
|
| 620 |
"""
|
| 621 |
-
os.environ["
|
| 622 |
try:
|
| 623 |
-
resp = client.get("/
|
| 624 |
assert resp.status_code == 401
|
| 625 |
finally:
|
| 626 |
-
del os.environ["
|
| 627 |
|
| 628 |
def test_auth_passes_with_correct_bearer_token(self, client, flask_app):
|
| 629 |
"""Correct Bearer token passes auth on protected endpoints."""
|
| 630 |
secret = "test-secret-token-correct"
|
| 631 |
-
os.environ["
|
| 632 |
try:
|
| 633 |
resp = client.get(
|
| 634 |
-
"/
|
| 635 |
headers={"Authorization": f"Bearer {secret}"},
|
| 636 |
)
|
| 637 |
assert resp.status_code == 200
|
| 638 |
finally:
|
| 639 |
-
del os.environ["
|
| 640 |
|
| 641 |
def test_livez_always_open_regardless_of_secret(self, client):
|
| 642 |
"""GET /livez is always unauthenticated, even when secret is set (REQ-057)."""
|
| 643 |
-
os.environ["
|
| 644 |
try:
|
| 645 |
-
resp = client.get("/
|
| 646 |
assert resp.status_code == 200
|
| 647 |
finally:
|
| 648 |
-
del os.environ["
|
|
|
|
| 6 |
src/routes/{observations, memories, search, graph, health, mcp, migration}.py.
|
| 7 |
|
| 8 |
Tests use the Flask test client — no running server required.
|
| 9 |
+
The AGENTCACHE_SECRET env var is NOT set so all auth checks pass through.
|
| 10 |
"""
|
| 11 |
import sys
|
| 12 |
import os
|
|
|
|
| 36 |
db_path = str(tmp_dir / "test.db")
|
| 37 |
|
| 38 |
# Point the server at an isolated DB
|
| 39 |
+
os.environ["AGENTCACHE_DB_PATH"] = db_path
|
| 40 |
# Ensure no auth requirement during tests
|
| 41 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 42 |
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 43 |
|
| 44 |
import app as app_module
|
| 45 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 46 |
+
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 47 |
from db import StateKV
|
| 48 |
|
| 49 |
# Patch StateKV to use tmp db before create_app() initialises it
|
|
|
|
| 84 |
|
| 85 |
# ===========================================================================
|
| 86 |
# Blueprint 1: health.py
|
| 87 |
+
# GET /agentcache/livez
|
| 88 |
+
# GET /agentcache/health
|
| 89 |
+
# GET /agentcache/audit
|
| 90 |
+
# GET /agentcache/config/flags
|
| 91 |
# ===========================================================================
|
| 92 |
|
| 93 |
class TestHealthBlueprint:
|
| 94 |
def test_livez_no_auth_required(self, client):
|
| 95 |
"""GET /livez must respond 200 without any auth token (always open)."""
|
| 96 |
+
resp = client.get("/agentcache/livez")
|
| 97 |
assert resp.status_code == 200
|
| 98 |
data = resp.get_json()
|
| 99 |
assert data["status"] == "ok"
|
| 100 |
assert "service" in data
|
| 101 |
|
| 102 |
def test_livez_returns_service_name(self, client):
|
| 103 |
+
resp = client.get("/agentcache/livez")
|
| 104 |
data = resp.get_json()
|
| 105 |
+
assert data["service"] == "agentcache"
|
| 106 |
|
| 107 |
def test_health_returns_200(self, client):
|
| 108 |
+
resp = client.get("/agentcache/health")
|
| 109 |
assert resp.status_code == 200
|
| 110 |
data = resp.get_json()
|
| 111 |
# Folder-based health check fields (REQ-047)
|
|
|
|
| 114 |
assert "memoryCount" in data
|
| 115 |
|
| 116 |
def test_audit_returns_200(self, client):
|
| 117 |
+
resp = client.get("/agentcache/audit")
|
| 118 |
assert resp.status_code == 200
|
| 119 |
data = resp.get_json()
|
| 120 |
assert "entries" in data
|
| 121 |
|
| 122 |
def test_config_flags_returns_200(self, client):
|
| 123 |
+
resp = client.get("/agentcache/config/flags")
|
| 124 |
assert resp.status_code == 200
|
| 125 |
data = resp.get_json()
|
| 126 |
assert "flags" in data
|
|
|
|
| 129 |
|
| 130 |
# ===========================================================================
|
| 131 |
# Blueprint 2: observations.py
|
| 132 |
+
# POST /agentcache/observe
|
| 133 |
+
# POST /agentcache/agent/observe
|
| 134 |
+
# GET /agentcache/folders
|
| 135 |
+
# GET /agentcache/folder/observations
|
| 136 |
# ===========================================================================
|
| 137 |
|
| 138 |
class TestObservationsBlueprint:
|
|
|
|
| 144 |
"text": "Added a new feature to the app",
|
| 145 |
"timestamp": _now_iso(),
|
| 146 |
}
|
| 147 |
+
resp = _post_json(client, "/agentcache/agent/observe", payload)
|
| 148 |
assert resp.status_code == 201
|
| 149 |
data = resp.get_json()
|
| 150 |
assert "observationId" in data
|
|
|
|
| 153 |
def test_agent_observe_missing_folder_path_returns_400(self, client):
|
| 154 |
"""POST /agent/observe missing folderPath returns 400."""
|
| 155 |
payload = {"agentId": "kiro", "text": "some work", "timestamp": _now_iso()}
|
| 156 |
+
resp = _post_json(client, "/agentcache/agent/observe", payload)
|
| 157 |
assert resp.status_code == 400
|
| 158 |
|
| 159 |
def test_agent_observe_missing_agent_id_returns_400(self, client):
|
| 160 |
"""POST /agent/observe missing agentId returns 400."""
|
| 161 |
payload = {"folderPath": "/home/user/proj", "text": "some work", "timestamp": _now_iso()}
|
| 162 |
+
resp = _post_json(client, "/agentcache/agent/observe", payload)
|
| 163 |
assert resp.status_code == 400
|
| 164 |
|
| 165 |
def test_agent_observe_missing_text_returns_400(self, client):
|
| 166 |
"""POST /agent/observe missing text returns 400."""
|
| 167 |
payload = {"folderPath": "/home/user/proj", "agentId": "kiro", "timestamp": _now_iso()}
|
| 168 |
+
resp = _post_json(client, "/agentcache/agent/observe", payload)
|
| 169 |
assert resp.status_code == 400
|
| 170 |
|
| 171 |
def test_folders_list_returns_200(self, client):
|
| 172 |
"""GET /folders returns 200 with a folders list."""
|
| 173 |
# Seed at least one observation first
|
| 174 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 175 |
"folderPath": "/home/user/proj-folders-test",
|
| 176 |
"agentId": "kiro",
|
| 177 |
"text": "Folders test observation",
|
| 178 |
"timestamp": _now_iso(),
|
| 179 |
})
|
| 180 |
+
resp = client.get("/agentcache/folders")
|
| 181 |
assert resp.status_code == 200
|
| 182 |
data = resp.get_json()
|
| 183 |
assert "folders" in data
|
|
|
|
| 187 |
"""GET /folder/observations with valid params returns 200."""
|
| 188 |
# Seed an observation first
|
| 189 |
fp = "/home/user/proj-obs-test"
|
| 190 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 191 |
"folderPath": fp,
|
| 192 |
"agentId": "kiro",
|
| 193 |
"text": "Obs for folder/observations test",
|
| 194 |
"timestamp": _now_iso(),
|
| 195 |
})
|
| 196 |
+
resp = client.get("/agentcache/folder/observations?folderPath=home/user/proj-obs-test&agentId=kiro")
|
| 197 |
assert resp.status_code == 200
|
| 198 |
data = resp.get_json()
|
| 199 |
assert "observations" in data
|
| 200 |
|
| 201 |
def test_folder_observations_missing_params_returns_400(self, client):
|
| 202 |
"""GET /folder/observations without required params returns 400."""
|
| 203 |
+
resp = client.get("/agentcache/folder/observations")
|
| 204 |
assert resp.status_code == 400
|
| 205 |
|
| 206 |
def test_legacy_observe_endpoint_returns_400_or_201(self, client):
|
|
|
|
| 211 |
"text": "legacy observe call",
|
| 212 |
"timestamp": _now_iso(),
|
| 213 |
}
|
| 214 |
+
resp = _post_json(client, "/agentcache/observe", payload)
|
| 215 |
# The legacy endpoint exists — expect either success or a controlled error, never 404
|
| 216 |
assert resp.status_code != 404
|
| 217 |
|
| 218 |
|
| 219 |
# ===========================================================================
|
| 220 |
# Blueprint 3: memories.py
|
| 221 |
+
# POST /agentcache/remember
|
| 222 |
+
# POST /agentcache/agent/remember
|
| 223 |
+
# GET /agentcache/memories
|
| 224 |
+
# POST /agentcache/forget
|
| 225 |
# ===========================================================================
|
| 226 |
|
| 227 |
class TestMemoriesBlueprint:
|
|
|
|
| 232 |
"type": "fact",
|
| 233 |
"concepts": ["sql", "security"],
|
| 234 |
}
|
| 235 |
+
resp = _post_json(client, "/agentcache/remember", payload)
|
| 236 |
assert resp.status_code == 201
|
| 237 |
data = resp.get_json()
|
| 238 |
assert "memory" in data
|
| 239 |
|
| 240 |
+
def test_agent_cache_valid_payload(self, client):
|
| 241 |
"""POST /agent/remember with content returns 201."""
|
| 242 |
payload = {
|
| 243 |
"content": "The project uses SQLite with WAL mode",
|
|
|
|
| 245 |
"type": "architecture",
|
| 246 |
"concepts": ["sqlite", "wal"],
|
| 247 |
}
|
| 248 |
+
resp = _post_json(client, "/agentcache/agent/remember", payload)
|
| 249 |
assert resp.status_code == 201
|
| 250 |
|
| 251 |
+
def test_agent_cache_missing_content_returns_400(self, client):
|
| 252 |
"""POST /agent/remember without content returns 400."""
|
| 253 |
+
resp = _post_json(client, "/agentcache/agent/remember", {"agentId": "kiro"})
|
| 254 |
assert resp.status_code == 400
|
| 255 |
|
| 256 |
def test_memories_list_returns_200(self, client):
|
| 257 |
"""GET /memories returns 200 with memories list."""
|
| 258 |
+
resp = client.get("/agentcache/memories")
|
| 259 |
assert resp.status_code == 200
|
| 260 |
data = resp.get_json()
|
| 261 |
assert "memories" in data
|
|
|
|
| 263 |
|
| 264 |
def test_memories_list_latest_only(self, client):
|
| 265 |
"""GET /memories?latest=true filters to only latest memories."""
|
| 266 |
+
resp = client.get("/agentcache/memories?latest=true")
|
| 267 |
assert resp.status_code == 200
|
| 268 |
data = resp.get_json()
|
| 269 |
# All returned memories should be latest=True or isLatest not False
|
|
|
|
| 273 |
def test_forget_memory_by_id(self, client):
|
| 274 |
"""POST /forget with memoryId deletes a global memory."""
|
| 275 |
# Create a memory first
|
| 276 |
+
create_resp = _post_json(client, "/agentcache/remember", {
|
| 277 |
"content": "This memory will be forgotten",
|
| 278 |
"type": "fact",
|
| 279 |
})
|
| 280 |
mem_id = create_resp.get_json()["memory"]["id"]
|
| 281 |
# Forget it
|
| 282 |
+
resp = _post_json(client, "/agentcache/forget", {"memoryId": mem_id})
|
| 283 |
assert resp.status_code == 200
|
| 284 |
data = resp.get_json()
|
| 285 |
assert data["deleted"] >= 1
|
|
|
|
| 288 |
"""POST /forget with folderPath+agentId clears folder observations."""
|
| 289 |
# Seed an observation
|
| 290 |
fp = "/home/user/proj-to-forget"
|
| 291 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 292 |
"folderPath": fp,
|
| 293 |
"agentId": "kiro",
|
| 294 |
"text": "Some work that will be forgotten",
|
| 295 |
"timestamp": _now_iso(),
|
| 296 |
})
|
| 297 |
+
resp = _post_json(client, "/agentcache/forget", {
|
| 298 |
"folderPath": fp,
|
| 299 |
"agentId": "kiro",
|
| 300 |
})
|
|
|
|
| 306 |
|
| 307 |
# ===========================================================================
|
| 308 |
# Blueprint 4: search.py
|
| 309 |
+
# POST /agentcache/search
|
| 310 |
+
# POST /agentcache/timeline
|
| 311 |
# ===========================================================================
|
| 312 |
|
| 313 |
class TestSearchBlueprint:
|
| 314 |
def test_search_with_query_returns_200(self, client):
|
| 315 |
"""POST /search with a query returns 200."""
|
| 316 |
# Seed something searchable first
|
| 317 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 318 |
"folderPath": "/home/user/proj-search-test",
|
| 319 |
"agentId": "kiro",
|
| 320 |
"text": "Implemented authentication middleware for the app",
|
| 321 |
"timestamp": _now_iso(),
|
| 322 |
})
|
| 323 |
+
resp = _post_json(client, "/agentcache/search", {"query": "authentication"})
|
| 324 |
assert resp.status_code == 200
|
| 325 |
|
| 326 |
def test_search_missing_query_returns_400(self, client):
|
| 327 |
"""POST /search without query returns 400."""
|
| 328 |
+
resp = _post_json(client, "/agentcache/search", {})
|
| 329 |
assert resp.status_code == 400
|
| 330 |
|
| 331 |
def test_search_with_folder_filter(self, client):
|
| 332 |
"""POST /search with folderPath filter returns 200."""
|
| 333 |
+
resp = _post_json(client, "/agentcache/search", {
|
| 334 |
"query": "test",
|
| 335 |
"folderPath": "/home/user/proj-search-test",
|
| 336 |
"agentId": "kiro",
|
|
|
|
| 339 |
|
| 340 |
def test_timeline_returns_200(self, client):
|
| 341 |
"""POST /timeline returns 200 with observations list."""
|
| 342 |
+
resp = _post_json(client, "/agentcache/timeline", {})
|
| 343 |
assert resp.status_code == 200
|
| 344 |
data = resp.get_json()
|
| 345 |
assert "observations" in data
|
|
|
|
| 347 |
|
| 348 |
def test_timeline_with_filters_returns_200(self, client):
|
| 349 |
"""POST /timeline with folder/agent filters returns 200."""
|
| 350 |
+
resp = _post_json(client, "/agentcache/timeline", {
|
| 351 |
"folderPath": "/home/user/proj-search-test",
|
| 352 |
"agentId": "kiro",
|
| 353 |
"limit": 10,
|
|
|
|
| 360 |
# Seed observations with distinct timestamps
|
| 361 |
for i in range(3):
|
| 362 |
ts = datetime.datetime(2025, 6, 1, 10, i, 0).isoformat() + "Z"
|
| 363 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 364 |
"folderPath": fp, "agentId": "kiro",
|
| 365 |
"text": f"Observation {i}", "timestamp": ts,
|
| 366 |
})
|
| 367 |
+
resp = _post_json(client, "/agentcache/timeline", {
|
| 368 |
"folderPath": fp, "agentId": "kiro",
|
| 369 |
})
|
| 370 |
assert resp.status_code == 200
|
|
|
|
| 375 |
|
| 376 |
# ===========================================================================
|
| 377 |
# Blueprint 5: graph.py
|
| 378 |
+
# GET /agentcache/graph
|
| 379 |
+
# GET /agentcache/graph/stats
|
| 380 |
+
# POST /agentcache/graph/query
|
| 381 |
+
# POST /agentcache/graph/build
|
| 382 |
# ===========================================================================
|
| 383 |
|
| 384 |
class TestGraphBlueprint:
|
| 385 |
def test_graph_returns_200(self, client):
|
| 386 |
"""GET /graph returns 200 with nodes and edges."""
|
| 387 |
+
resp = client.get("/agentcache/graph")
|
| 388 |
assert resp.status_code == 200
|
| 389 |
data = resp.get_json()
|
| 390 |
assert "nodes" in data
|
|
|
|
| 392 |
|
| 393 |
def test_graph_stats_returns_200(self, client):
|
| 394 |
"""GET /graph/stats returns 200."""
|
| 395 |
+
resp = client.get("/agentcache/graph/stats")
|
| 396 |
assert resp.status_code == 200
|
| 397 |
data = resp.get_json()
|
| 398 |
assert "nodes" in data
|
|
|
|
| 400 |
|
| 401 |
def test_graph_query_returns_200(self, client):
|
| 402 |
"""POST /graph/query returns 200."""
|
| 403 |
+
resp = _post_json(client, "/agentcache/graph/query", {})
|
| 404 |
assert resp.status_code == 200
|
| 405 |
|
| 406 |
def test_graph_build_returns_200(self, client):
|
| 407 |
"""POST /graph/build returns 200."""
|
| 408 |
+
resp = _post_json(client, "/agentcache/graph/build", {})
|
| 409 |
assert resp.status_code == 200
|
| 410 |
|
| 411 |
def test_graph_nodes_have_required_fields(self, client):
|
| 412 |
"""Graph nodes contain id, label, folderPath, agentIds, obsCount, color (REQ-024)."""
|
| 413 |
# Seed some data
|
| 414 |
+
_post_json(client, "/agentcache/agent/observe", {
|
| 415 |
"folderPath": "/home/user/proj-graph-check",
|
| 416 |
"agentId": "kiro",
|
| 417 |
"text": "Graph node field check",
|
| 418 |
"timestamp": _now_iso(),
|
| 419 |
})
|
| 420 |
+
resp = client.get("/agentcache/graph")
|
| 421 |
data = resp.get_json()
|
| 422 |
if data["nodes"]:
|
| 423 |
node = data["nodes"][0]
|
|
|
|
| 427 |
|
| 428 |
# ===========================================================================
|
| 429 |
# Blueprint 6: mcp.py
|
| 430 |
+
# GET /agentcache/mcp/tools
|
| 431 |
+
# POST /agentcache/mcp/tools
|
| 432 |
# ===========================================================================
|
| 433 |
|
| 434 |
class TestMcpBlueprint:
|
| 435 |
def test_mcp_tools_list_returns_200(self, client):
|
| 436 |
"""GET /mcp/tools returns 200 with a tools list."""
|
| 437 |
+
resp = client.get("/agentcache/mcp/tools")
|
| 438 |
assert resp.status_code == 200
|
| 439 |
data = resp.get_json()
|
| 440 |
assert "tools" in data
|
|
|
|
| 443 |
|
| 444 |
def test_mcp_tools_contains_required_tools(self, client):
|
| 445 |
"""The expected MCP tools are all present in the schema."""
|
| 446 |
+
resp = client.get("/agentcache/mcp/tools")
|
| 447 |
tool_names = {t["name"] for t in resp.get_json()["tools"]}
|
| 448 |
required_tools = {
|
| 449 |
"agent_observe",
|
| 450 |
+
"cache_recall",
|
| 451 |
+
"cache_save",
|
| 452 |
+
"agent_cache",
|
| 453 |
+
"cache_diagnose",
|
| 454 |
+
"cache_forget",
|
| 455 |
+
"cache_export",
|
| 456 |
+
"cache_smart_search",
|
| 457 |
+
"cache_folders",
|
| 458 |
+
"cache_folder_observations",
|
| 459 |
+
"cache_timeline",
|
| 460 |
}
|
| 461 |
assert required_tools.issubset(tool_names), \
|
| 462 |
f"Missing tools: {required_tools - tool_names}"
|
|
|
|
| 472 |
"timestamp": _now_iso(),
|
| 473 |
},
|
| 474 |
}
|
| 475 |
+
resp = _post_json(client, "/agentcache/mcp/tools", payload)
|
| 476 |
assert resp.status_code == 200
|
| 477 |
data = resp.get_json()
|
| 478 |
assert "content" in data
|
| 479 |
|
| 480 |
+
def test_mcp_tool_call_cache_recall(self, client):
|
| 481 |
+
"""POST /mcp/tools cache_recall returns 200."""
|
| 482 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {
|
| 483 |
+
"name": "cache_recall",
|
| 484 |
"arguments": {"query": "authentication"},
|
| 485 |
})
|
| 486 |
assert resp.status_code == 200
|
| 487 |
|
| 488 |
+
def test_mcp_tool_call_cache_diagnose(self, client):
|
| 489 |
+
"""POST /mcp/tools cache_diagnose returns folderCount etc."""
|
| 490 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {
|
| 491 |
+
"name": "cache_diagnose",
|
| 492 |
"arguments": {},
|
| 493 |
})
|
| 494 |
assert resp.status_code == 200
|
|
|
|
| 497 |
assert "folderCount" in result
|
| 498 |
assert "observationCount" in result
|
| 499 |
|
| 500 |
+
def test_mcp_tool_call_cache_folders(self, client):
|
| 501 |
+
"""POST /mcp/tools cache_folders returns list."""
|
| 502 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {
|
| 503 |
+
"name": "cache_folders",
|
| 504 |
"arguments": {},
|
| 505 |
})
|
| 506 |
assert resp.status_code == 200
|
| 507 |
|
| 508 |
+
def test_mcp_tool_call_cache_timeline(self, client):
|
| 509 |
+
"""POST /mcp/tools cache_timeline returns list."""
|
| 510 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {
|
| 511 |
+
"name": "cache_timeline",
|
| 512 |
"arguments": {"limit": 10},
|
| 513 |
})
|
| 514 |
assert resp.status_code == 200
|
| 515 |
|
| 516 |
def test_mcp_tool_unknown_name_returns_400(self, client):
|
| 517 |
"""POST /mcp/tools with unknown tool name returns 400."""
|
| 518 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {
|
| 519 |
"name": "nonexistent_tool_xyz",
|
| 520 |
"arguments": {},
|
| 521 |
})
|
|
|
|
| 523 |
|
| 524 |
def test_mcp_tool_missing_name_returns_400(self, client):
|
| 525 |
"""POST /mcp/tools without name returns 400."""
|
| 526 |
+
resp = _post_json(client, "/agentcache/mcp/tools", {"arguments": {}})
|
| 527 |
assert resp.status_code == 400
|
| 528 |
|
| 529 |
|
| 530 |
# ===========================================================================
|
| 531 |
# Blueprint 7: migration.py
|
| 532 |
+
# POST /agentcache/migrate
|
| 533 |
# ===========================================================================
|
| 534 |
|
| 535 |
class TestMigrationBlueprint:
|
| 536 |
def test_migrate_dry_run_returns_200(self, client):
|
| 537 |
"""POST /migrate with dry_run=true returns 200 with counts."""
|
| 538 |
+
resp = _post_json(client, "/agentcache/migrate", {"dry_run": True})
|
| 539 |
assert resp.status_code == 200
|
| 540 |
data = resp.get_json()
|
| 541 |
assert "migrated_sessions" in data
|
|
|
|
| 544 |
|
| 545 |
def test_migrate_without_body_defaults_to_no_dry_run(self, client):
|
| 546 |
"""POST /migrate with empty body returns 200."""
|
| 547 |
+
resp = _post_json(client, "/agentcache/migrate", {})
|
| 548 |
assert resp.status_code == 200
|
| 549 |
|
| 550 |
|
|
|
|
| 558 |
registered_rules = {rule.rule for rule in flask_app.url_map.iter_rules()}
|
| 559 |
|
| 560 |
# observations.py
|
| 561 |
+
assert "/agentcache/agent/observe" in registered_rules
|
| 562 |
+
assert "/agentcache/folders" in registered_rules
|
| 563 |
+
assert "/agentcache/folder/observations" in registered_rules
|
| 564 |
|
| 565 |
# memories.py
|
| 566 |
+
assert "/agentcache/remember" in registered_rules
|
| 567 |
+
assert "/agentcache/memories" in registered_rules
|
| 568 |
+
assert "/agentcache/forget" in registered_rules
|
| 569 |
|
| 570 |
# search.py
|
| 571 |
+
assert "/agentcache/search" in registered_rules
|
| 572 |
+
assert "/agentcache/timeline" in registered_rules
|
| 573 |
|
| 574 |
# graph.py
|
| 575 |
+
assert "/agentcache/graph" in registered_rules
|
| 576 |
+
assert "/agentcache/graph/stats" in registered_rules
|
| 577 |
+
assert "/agentcache/graph/query" in registered_rules
|
| 578 |
|
| 579 |
# health.py
|
| 580 |
+
assert "/agentcache/livez" in registered_rules
|
| 581 |
+
assert "/agentcache/health" in registered_rules
|
| 582 |
+
assert "/agentcache/audit" in registered_rules
|
| 583 |
+
assert "/agentcache/config/flags" in registered_rules
|
| 584 |
|
| 585 |
# mcp.py
|
| 586 |
+
assert "/agentcache/mcp/tools" in registered_rules
|
| 587 |
|
| 588 |
# migration.py
|
| 589 |
+
assert "/agentcache/migrate" in registered_rules
|
| 590 |
|
| 591 |
def test_no_routes_return_404_from_blueprints(self, client):
|
| 592 |
"""None of the expected blueprint routes return 404."""
|
| 593 |
get_routes = [
|
| 594 |
+
"/agentcache/livez",
|
| 595 |
+
"/agentcache/health",
|
| 596 |
+
"/agentcache/folders",
|
| 597 |
+
"/agentcache/memories",
|
| 598 |
+
"/agentcache/graph",
|
| 599 |
+
"/agentcache/graph/stats",
|
| 600 |
+
"/agentcache/audit",
|
| 601 |
+
"/agentcache/config/flags",
|
| 602 |
+
"/agentcache/mcp/tools",
|
| 603 |
]
|
| 604 |
for route in get_routes:
|
| 605 |
resp = client.get(route)
|
|
|
|
| 608 |
def test_cors_headers_present_on_responses(self, client):
|
| 609 |
"""CORS after_request hook adds the expected headers."""
|
| 610 |
resp = client.get(
|
| 611 |
+
"/agentcache/livez",
|
| 612 |
headers={"Origin": "http://localhost:3000"},
|
| 613 |
)
|
| 614 |
assert resp.status_code == 200
|
|
|
|
| 617 |
assert "Access-Control-Allow-Methods" in resp.headers
|
| 618 |
|
| 619 |
def test_auth_returns_401_when_secret_set(self, client, flask_app):
|
| 620 |
+
"""When AGENTCACHE_SECRET is configured, missing token returns 401 on protected routes.
|
| 621 |
|
| 622 |
+
/agentcache/audit requires auth; /agentcache/livez and /agentcache/health are open.
|
| 623 |
"""
|
| 624 |
+
os.environ["AGENTCACHE_SECRET"] = "test-secret-token"
|
| 625 |
try:
|
| 626 |
+
resp = client.get("/agentcache/audit")
|
| 627 |
assert resp.status_code == 401
|
| 628 |
finally:
|
| 629 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 630 |
|
| 631 |
def test_auth_passes_with_correct_bearer_token(self, client, flask_app):
|
| 632 |
"""Correct Bearer token passes auth on protected endpoints."""
|
| 633 |
secret = "test-secret-token-correct"
|
| 634 |
+
os.environ["AGENTCACHE_SECRET"] = secret
|
| 635 |
try:
|
| 636 |
resp = client.get(
|
| 637 |
+
"/agentcache/audit",
|
| 638 |
headers={"Authorization": f"Bearer {secret}"},
|
| 639 |
)
|
| 640 |
assert resp.status_code == 200
|
| 641 |
finally:
|
| 642 |
+
del os.environ["AGENTCACHE_SECRET"]
|
| 643 |
|
| 644 |
def test_livez_always_open_regardless_of_secret(self, client):
|
| 645 |
"""GET /livez is always unauthenticated, even when secret is set (REQ-057)."""
|
| 646 |
+
os.environ["AGENTCACHE_SECRET"] = "some-secret"
|
| 647 |
try:
|
| 648 |
+
resp = client.get("/agentcache/livez")
|
| 649 |
assert resp.status_code == 200
|
| 650 |
finally:
|
| 651 |
+
del os.environ["AGENTCACHE_SECRET"]
|
tests/test_timeline.py
CHANGED
|
@@ -48,14 +48,14 @@ class TestTimelineLimit:
|
|
| 48 |
def test_limit_respected(self, tmp_path):
|
| 49 |
kv = make_kv(tmp_path)
|
| 50 |
for i in range(10):
|
| 51 |
-
add_obs(kv, timestamp=ts(i))
|
| 52 |
results = folder_timeline(kv, limit=5)
|
| 53 |
assert len(results) == 5
|
| 54 |
|
| 55 |
def test_default_limit_100(self, tmp_path):
|
| 56 |
kv = make_kv(tmp_path)
|
| 57 |
for i in range(150):
|
| 58 |
-
add_obs(kv, timestamp=ts(i))
|
| 59 |
results = folder_timeline(kv)
|
| 60 |
assert len(results) == 100
|
| 61 |
|
|
|
|
| 48 |
def test_limit_respected(self, tmp_path):
|
| 49 |
kv = make_kv(tmp_path)
|
| 50 |
for i in range(10):
|
| 51 |
+
add_obs(kv, timestamp=ts(i), text=f"obs {i}")
|
| 52 |
results = folder_timeline(kv, limit=5)
|
| 53 |
assert len(results) == 5
|
| 54 |
|
| 55 |
def test_default_limit_100(self, tmp_path):
|
| 56 |
kv = make_kv(tmp_path)
|
| 57 |
for i in range(150):
|
| 58 |
+
add_obs(kv, timestamp=ts(i), text=f"obs {i}")
|
| 59 |
results = folder_timeline(kv)
|
| 60 |
assert len(results) == 100
|
| 61 |
|