Yash030 commited on
Commit
ba35a6d
Β·
1 Parent(s): 7c69c77

fix: Added prod level migrations

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .github/workflows/automation.yml +26 -38
  2. Dockerfile +5 -8
  3. examples/antigravity_sync.py +1 -1
  4. plugin/scripts/notification.py +2 -2
  5. plugin/scripts/post_tool_failure.py +2 -2
  6. plugin/scripts/post_tool_use.py +2 -2
  7. plugin/scripts/prompt_submit.py +1 -1
  8. plugin/scripts/subagent_start.py +2 -2
  9. plugin/scripts/subagent_stop.py +2 -2
  10. plugin/scripts/task_completed.py +2 -2
  11. pyproject.toml +38 -4
  12. requirements.txt +1 -10
  13. src/__init__.py +0 -0
  14. src/agentcache.egg-info/PKG-INFO +520 -0
  15. src/agentcache.egg-info/SOURCES.txt +59 -0
  16. src/agentcache.egg-info/dependency_links.txt +1 -0
  17. src/agentcache.egg-info/entry_points.txt +2 -0
  18. src/agentcache.egg-info/requires.txt +15 -0
  19. src/agentcache.egg-info/top_level.txt +1 -0
  20. src/agentcache/__init__.py +32 -0
  21. src/{app.py β†’ agentcache/app.py} +284 -280
  22. src/{auth.md β†’ agentcache/auth.md} +0 -0
  23. src/{cache β†’ agentcache/cache}/__init__.py +12 -13
  24. src/{cache β†’ agentcache/cache}/context.py +2 -2
  25. src/{cache β†’ agentcache/cache}/graph.py +2 -2
  26. src/{cache β†’ agentcache/cache}/health.py +2 -2
  27. src/{cache β†’ agentcache/cache}/observe.py +2 -3
  28. src/{cache β†’ agentcache/cache}/remember.py +2 -2
  29. src/{cache β†’ agentcache/cache}/timeline.py +2 -2
  30. src/{cli.py β†’ agentcache/cli.py} +6 -11
  31. src/{connect.py β†’ agentcache/connect.py} +6 -13
  32. src/{db.py β†’ agentcache/db.py} +3 -3
  33. src/{functions.py β†’ agentcache/functions.py} +107 -50
  34. src/{import_data.py β†’ agentcache/import_data.py} +3 -5
  35. src/{mcp_stdio.py β†’ agentcache/mcp_stdio.py} +2 -1
  36. src/{replay_import.py β†’ agentcache/replay_import.py} +23 -13
  37. src/{routes β†’ agentcache/routes}/__init__.py +3 -3
  38. src/{routes β†’ agentcache/routes}/graph.py +5 -3
  39. src/{routes β†’ agentcache/routes}/health.py +7 -5
  40. src/{routes β†’ agentcache/routes}/mcp.py +11 -7
  41. src/{routes β†’ agentcache/routes}/memories.py +6 -4
  42. src/{routes β†’ agentcache/routes}/migration.py +5 -3
  43. src/{routes β†’ agentcache/routes}/observations.py +10 -6
  44. src/{routes β†’ agentcache/routes}/search.py +5 -3
  45. src/{search.py β†’ agentcache/search.py} +6 -6
  46. src/{storage β†’ agentcache/storage}/__init__.py +2 -2
  47. src/{storage β†’ agentcache/storage}/images.py +1 -1
  48. src/{storage β†’ agentcache/storage}/paths.py +1 -1
  49. src/{storage β†’ agentcache/storage}/scopes.py +0 -0
  50. src/{viewer β†’ agentcache/viewer}/favicon.svg +0 -0
.github/workflows/automation.yml CHANGED
@@ -1,26 +1,13 @@
1
- name: Test Automation Pipeline
2
 
3
  on:
4
  push:
5
  branches: [main]
6
- paths-ignore:
7
- - "README.md"
8
- - "CHANGELOG.md"
9
- - "AGENTS.md"
10
- - "ROADMAP.md"
11
- - "**/*.md"
12
  pull_request:
13
  branches: [main]
14
- paths-ignore:
15
- - "README.md"
16
- - "CHANGELOG.md"
17
- - "AGENTS.md"
18
- - "ROADMAP.md"
19
- - "**/*.md"
20
  workflow_dispatch:
21
- schedule:
22
- # Run every Sunday at midnight to check for dependency updates and slow/regression bugs
23
- - cron: '0 0 * * 0'
24
 
25
  concurrency:
26
  group: ${{ github.workflow }}-${{ github.ref }}
@@ -55,7 +42,7 @@ jobs:
55
  strategy:
56
  fail-fast: false
57
  matrix:
58
- os: [ubuntu-latest, windows-latest]
59
  python-version: ["3.10", "3.11", "3.12"]
60
 
61
  steps:
@@ -68,7 +55,7 @@ jobs:
68
  python-version: ${{ matrix.python-version }}
69
  cache: 'pip'
70
 
71
- - name: Install Dependencies
72
  run: |
73
  python -m pip install --upgrade pip
74
  pip install -e ".[dev]"
@@ -79,7 +66,7 @@ jobs:
79
  - name: Run Hypothesis Property-Based Tests
80
  run: pytest tests/test_properties.py -v --tb=short
81
 
82
- build-verification:
83
  name: Build & Package Verification
84
  needs: automated-testing
85
  runs-on: ubuntu-latest
@@ -92,12 +79,15 @@ jobs:
92
  with:
93
  python-version: "3.11"
94
 
95
- - name: Install Build Tool
96
- run: pip install build
97
 
98
  - name: Build Package (wheel & sdist)
99
  run: python -m build
100
 
 
 
 
101
  - name: Upload Build Artifacts
102
  uses: actions/upload-artifact@v4
103
  with:
@@ -105,24 +95,22 @@ jobs:
105
  path: dist/
106
  retention-days: 5
107
 
108
- deploy-to-huggingface:
109
- name: Deploy to Hugging Face Spaces
110
- needs: [lint-and-format, automated-testing, build-verification]
111
- if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
 
112
  runs-on: ubuntu-latest
 
 
113
  steps:
114
- - name: Checkout Code
115
- uses: actions/checkout@v4
116
  with:
117
- fetch-depth: 0
118
- lfs: true
119
-
120
- - name: Push to Hugging Face Space
121
- env:
122
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
123
- run: |
124
- git config --global user.email "bot@github.com"
125
- git config --global user.name "GitHub Action Bot"
126
- git remote add hf https://Yash030:$HF_TOKEN@huggingface.co/spaces/Yash030/agentmemory-python
127
- git push --force hf main:main
128
 
 
 
 
 
 
1
+ name: Test & Publish Pipeline
2
 
3
  on:
4
  push:
5
  branches: [main]
6
+ tags:
7
+ - 'v*'
 
 
 
 
8
  pull_request:
9
  branches: [main]
 
 
 
 
 
 
10
  workflow_dispatch:
 
 
 
11
 
12
  concurrency:
13
  group: ${{ github.workflow }}-${{ github.ref }}
 
42
  strategy:
43
  fail-fast: false
44
  matrix:
45
+ os: [ubuntu-latest, windows-latest, macos-latest]
46
  python-version: ["3.10", "3.11", "3.12"]
47
 
48
  steps:
 
55
  python-version: ${{ matrix.python-version }}
56
  cache: 'pip'
57
 
58
+ - name: Install Package & Dev Dependencies
59
  run: |
60
  python -m pip install --upgrade pip
61
  pip install -e ".[dev]"
 
66
  - name: Run Hypothesis Property-Based Tests
67
  run: pytest tests/test_properties.py -v --tb=short
68
 
69
+ build-and-verify:
70
  name: Build & Package Verification
71
  needs: automated-testing
72
  runs-on: ubuntu-latest
 
79
  with:
80
  python-version: "3.11"
81
 
82
+ - name: Install Build and Twine
83
+ run: pip install build twine
84
 
85
  - name: Build Package (wheel & sdist)
86
  run: python -m build
87
 
88
+ - name: Verify Package Metadata
89
+ run: twine check dist/*
90
+
91
  - name: Upload Build Artifacts
92
  uses: actions/upload-artifact@v4
93
  with:
 
95
  path: dist/
96
  retention-days: 5
97
 
98
+ publish-to-pypi:
99
+ name: Publish to PyPI
100
+ needs: build-and-verify
101
+ # Only run on pushes of version tags (e.g. v0.9.8) to the main branch
102
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
103
  runs-on: ubuntu-latest
104
+ permissions:
105
+ id-token: write
106
  steps:
107
+ - name: Download Build Artifacts
108
+ uses: actions/download-artifact@v4
109
  with:
110
+ name: agentcache-dist
111
+ path: dist/
 
 
 
 
 
 
 
 
 
112
 
113
+ - name: Publish Package to PyPI
114
+ uses: pypa/gh-action-pypi-publish@release/v1
115
+ with:
116
+ print-hash: true
Dockerfile CHANGED
@@ -15,14 +15,11 @@ RUN useradd -m -u 1000 user
15
  # Set up workdir
16
  WORKDIR /app
17
 
18
- # Copy python dependencies and install
19
- COPY requirements.txt /app/requirements.txt
20
- RUN pip install --no-cache-dir -r /app/requirements.txt
21
-
22
- # Copy application files
23
- COPY --chown=user:user src /app/src
24
- COPY --chown=user:user start.sh /app/start.sh
25
- COPY --chown=user:user sync.py /app/sync.py
26
 
27
  # Give permissions
28
  RUN chmod +x /app/start.sh && chown -R user:user /app /home/user
 
15
  # Set up workdir
16
  WORKDIR /app
17
 
18
+ # Copy all application files first to support package installation
19
+ COPY --chown=user:user . /app
20
+
21
+ # Install the package directly in non-editable mode for production deployment
22
+ RUN pip install --no-cache-dir /app
 
 
 
23
 
24
  # Give permissions
25
  RUN chmod +x /app/start.sh && chown -R user:user /app /home/user
examples/antigravity_sync.py CHANGED
@@ -146,7 +146,7 @@ def perform_antigravity_sync_local(args):
146
  current_prompt = p_text.strip()
147
  current_timestamp = step.get("created_at")
148
  elif step_type == "PLANNER_RESPONSE" and current_prompt:
149
- ts = current_timestamp or step.get("created_at") or datetime.datetime.utcnow().isoformat() + "Z"
150
  turns.append({"prompt": current_prompt, "response": step.get("content", ""), "timestamp": ts})
151
  current_prompt = None
152
  current_timestamp = None
 
146
  current_prompt = p_text.strip()
147
  current_timestamp = step.get("created_at")
148
  elif step_type == "PLANNER_RESPONSE" and current_prompt:
149
+ ts = current_timestamp or step.get("created_at") or datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
150
  turns.append({"prompt": current_prompt, "response": step.get("content", ""), "timestamp": ts})
151
  current_prompt = None
152
  current_timestamp = None
plugin/scripts/notification.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
@@ -28,7 +28,7 @@ def main():
28
  "sessionId": session_id,
29
  "project": resolve_project(data.get("cwd")),
30
  "cwd": data.get("cwd") or "",
31
- "timestamp": datetime.utcnow().isoformat() + "Z",
32
  "data": {
33
  "notification_type": notification_type,
34
  "title": data.get("title"),
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
 
28
  "sessionId": session_id,
29
  "project": resolve_project(data.get("cwd")),
30
  "cwd": data.get("cwd") or "",
31
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
32
  "data": {
33
  "notification_type": notification_type,
34
  "title": data.get("title"),
plugin/scripts/post_tool_failure.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
@@ -34,7 +34,7 @@ def main():
34
  "sessionId": session_id,
35
  "project": resolve_project(data.get("cwd")),
36
  "cwd": data.get("cwd") or "",
37
- "timestamp": datetime.utcnow().isoformat() + "Z",
38
  "data": {
39
  "tool_name": tool_name,
40
  "tool_input": limit_str(tool_input),
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
 
34
  "sessionId": session_id,
35
  "project": resolve_project(data.get("cwd")),
36
  "cwd": data.get("cwd") or "",
37
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
38
  "data": {
39
  "tool_name": tool_name,
40
  "tool_input": limit_str(tool_input),
plugin/scripts/post_tool_use.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def is_base64_image(val):
@@ -80,7 +80,7 @@ def main():
80
  "sessionId": session_id,
81
  "project": resolve_project(data.get("cwd")),
82
  "cwd": data.get("cwd") or "",
83
- "timestamp": datetime.utcnow().isoformat() + "Z",
84
  "data": {
85
  "tool_name": tool_name,
86
  "tool_input": tool_input,
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def is_base64_image(val):
 
80
  "sessionId": session_id,
81
  "project": resolve_project(data.get("cwd")),
82
  "cwd": data.get("cwd") or "",
83
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
84
  "data": {
85
  "tool_name": tool_name,
86
  "tool_input": tool_input,
plugin/scripts/prompt_submit.py CHANGED
@@ -42,7 +42,7 @@ def main():
42
  "sessionId": session_id,
43
  "project": project,
44
  "cwd": cwd,
45
- "timestamp": datetime.utcnow().isoformat() + "Z",
46
  "data": {
47
  "prompt": prompt
48
  }
 
42
  "sessionId": session_id,
43
  "project": project,
44
  "cwd": cwd,
45
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
46
  "data": {
47
  "prompt": prompt
48
  }
plugin/scripts/subagent_start.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
@@ -26,7 +26,7 @@ def main():
26
  "sessionId": session_id,
27
  "project": resolve_project(data.get("cwd")),
28
  "cwd": data.get("cwd") or "",
29
- "timestamp": datetime.utcnow().isoformat() + "Z",
30
  "data": {
31
  "agent_id": agent_id,
32
  "agent_type": agent_type
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
 
26
  "sessionId": session_id,
27
  "project": resolve_project(data.get("cwd")),
28
  "cwd": data.get("cwd") or "",
29
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
30
  "data": {
31
  "agent_id": agent_id,
32
  "agent_type": agent_type
plugin/scripts/subagent_stop.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
@@ -32,7 +32,7 @@ def main():
32
  "sessionId": session_id,
33
  "project": resolve_project(data.get("cwd")),
34
  "cwd": data.get("cwd") or "",
35
- "timestamp": datetime.utcnow().isoformat() + "Z",
36
  "data": {
37
  "agent_id": agent_id,
38
  "agent_type": agent_type,
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
 
32
  "sessionId": session_id,
33
  "project": resolve_project(data.get("cwd")),
34
  "cwd": data.get("cwd") or "",
35
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
36
  "data": {
37
  "agent_id": agent_id,
38
  "agent_type": agent_type,
plugin/scripts/task_completed.py CHANGED
@@ -2,7 +2,7 @@
2
  import sys
3
  import json
4
  import time
5
- from datetime import datetime
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
@@ -29,7 +29,7 @@ def main():
29
  "sessionId": session_id,
30
  "project": resolve_project(data.get("cwd")),
31
  "cwd": data.get("cwd") or "",
32
- "timestamp": datetime.utcnow().isoformat() + "Z",
33
  "data": {
34
  "task_id": data.get("task_id"),
35
  "task_subject": data.get("task_subject"),
 
2
  import sys
3
  import json
4
  import time
5
+ from datetime import datetime, timezone
6
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
7
 
8
  def main():
 
29
  "sessionId": session_id,
30
  "project": resolve_project(data.get("cwd")),
31
  "cwd": data.get("cwd") or "",
32
+ "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
33
  "data": {
34
  "task_id": data.get("task_id"),
35
  "task_subject": data.get("task_subject"),
pyproject.toml CHANGED
@@ -7,8 +7,26 @@ 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"
 
 
 
 
 
 
 
 
 
 
 
 
12
  dependencies = [
13
  "flask>=3.0.0",
14
  "flask-sock>=0.7.0",
@@ -22,21 +40,37 @@ dependencies = [
22
  dev = [
23
  "pytest>=8.0.0",
24
  "hypothesis>=6.100.0",
 
 
25
  ]
26
  local-embeddings = [
27
  "sentence-transformers>=2.7.0",
28
  ]
29
 
 
 
 
 
 
 
30
  [project.scripts]
31
- agentcache = "src.cli:main"
32
 
33
  [tool.setuptools.packages.find]
34
- where = ["."]
35
- include = ["src*"]
36
 
37
  [tool.setuptools.package-data]
38
- "src.viewer" = ["*.html", "*.svg", "*.js", "*.css"]
39
 
40
  [tool.pytest.ini_options]
41
  testpaths = ["tests"]
42
  python_files = ["test_*.py"]
 
 
 
 
 
 
 
 
 
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
+ authors = [
11
+ { name = "Yash Wanjarwadkar", email = "yashwanjarwadkar@gmail.com" }
12
+ ]
13
+ maintainers = [
14
+ { name = "Yash Wanjarwadkar", email = "yashwanjarwadkar@gmail.com" }
15
+ ]
16
  license = { text = "MIT" }
17
  requires-python = ">=3.10"
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ ]
30
  dependencies = [
31
  "flask>=3.0.0",
32
  "flask-sock>=0.7.0",
 
40
  dev = [
41
  "pytest>=8.0.0",
42
  "hypothesis>=6.100.0",
43
+ "ruff>=0.3.0",
44
+ "twine>=5.0.0",
45
  ]
46
  local-embeddings = [
47
  "sentence-transformers>=2.7.0",
48
  ]
49
 
50
+ [project.urls]
51
+ Homepage = "https://github.com/Yash030/agentcache-python"
52
+ Repository = "https://github.com/Yash030/agentcache-python.git"
53
+ "Bug Tracker" = "https://github.com/Yash030/agentcache-python/issues"
54
+ Documentation = "https://github.com/Yash030/agentcache-python#readme"
55
+
56
  [project.scripts]
57
+ agentcache = "agentcache.cli:main"
58
 
59
  [tool.setuptools.packages.find]
60
+ where = ["src"]
61
+ include = ["agentcache*"]
62
 
63
  [tool.setuptools.package-data]
64
+ "agentcache.viewer" = ["*.html", "*.svg", "*.js", "*.css"]
65
 
66
  [tool.pytest.ini_options]
67
  testpaths = ["tests"]
68
  python_files = ["test_*.py"]
69
+
70
+ [tool.ruff]
71
+ line-length = 88
72
+ target-version = "py310"
73
+
74
+ [tool.ruff.lint]
75
+ select = ["E", "F", "I", "W"]
76
+ ignore = ["E501"]
requirements.txt CHANGED
@@ -1,10 +1 @@
1
- flask>=3.0.0
2
- flask-sock>=0.7.0
3
- requests>=2.31.0
4
- websockets>=12.0
5
- python-dateutil>=2.8.2
6
- huggingface_hub>=0.20.0
7
-
8
- # dev
9
- pytest==8.3.5
10
- hypothesis==6.131.15
 
1
+ -e .[dev,local-embeddings]
 
 
 
 
 
 
 
 
 
src/__init__.py DELETED
File without changes
src/agentcache.egg-info/PKG-INFO ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: agentcache
3
+ Version: 0.9.8
4
+ Summary: A Python REST + WebSocket + MCP cache server for AI agents, backed by SQLite
5
+ Author-email: Yash Wanjarwadkar <yashwanjarwadkar@gmail.com>
6
+ Maintainer-email: Yash Wanjarwadkar <yashwanjarwadkar@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/Yash030/agentcache-python
9
+ Project-URL: Repository, https://github.com/Yash030/agentcache-python.git
10
+ Project-URL: Bug Tracker, https://github.com/Yash030/agentcache-python/issues
11
+ Project-URL: Documentation, https://github.com/Yash030/agentcache-python#readme
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: flask>=3.0.0
26
+ Requires-Dist: flask-sock>=0.7.0
27
+ Requires-Dist: requests>=2.31.0
28
+ Requires-Dist: python-dateutil>=2.8.2
29
+ Requires-Dist: huggingface_hub>=0.20.0
30
+ Requires-Dist: websockets>=12.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
33
+ Requires-Dist: hypothesis>=6.100.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
35
+ Requires-Dist: twine>=5.0.0; extra == "dev"
36
+ Provides-Extra: local-embeddings
37
+ Requires-Dist: sentence-transformers>=2.7.0; extra == "local-embeddings"
38
+
39
+ ---
40
+ title: AgentCache Python
41
+ emoji: 🧠
42
+ colorFrom: blue
43
+ colorTo: indigo
44
+ sdk: docker
45
+ pinned: false
46
+ ---
47
+
48
+ <h1 align="center">agentcache-python</h1>
49
+
50
+ <p align="center">
51
+ <strong>Persistent memory for AI coding agents β€” pure Python, zero external databases.</strong><br/>
52
+ Works with Claude Code, Cursor, Cline, Windsurf, Gemini CLI, and any MCP client.
53
+ </p>
54
+
55
+ <p align="center">
56
+ <img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.10+" />
57
+ <img src="https://img.shields.io/badge/SQLite-WAL-003B57?style=for-the-badge&logo=sqlite&logoColor=white" alt="SQLite WAL" />
58
+ <img src="https://img.shields.io/badge/Flask-3.0-000000?style=for-the-badge&logo=flask&logoColor=white" alt="Flask 3.0" />
59
+ <img src="https://img.shields.io/badge/MCP-Compatible-6B21A8?style=for-the-badge" alt="MCP Compatible" />
60
+ <img src="https://img.shields.io/badge/HuggingFace-Space-FF9D00?style=for-the-badge&logo=huggingface&logoColor=white" alt="HuggingFace Space" />
61
+ </p>
62
+
63
+ <p align="center">
64
+ <a href="#quick-start">Quick Start</a> &bull;
65
+ <a href="#features">Features</a> &bull;
66
+ <a href="#mcp-integration">MCP</a> &bull;
67
+ <a href="#api-reference">API</a> &bull;
68
+ <a href="#configuration">Config</a> &bull;
69
+ <a href="#deploy-to-huggingface">Deploy</a> &bull;
70
+ <a href="#viewer">Viewer</a> &bull;
71
+ <a href="#architecture">Architecture</a>
72
+ </p>
73
+
74
+ ---
75
+
76
+ ## What Is This?
77
+
78
+ **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.
79
+
80
+ Key differences from the Node.js original:
81
+
82
+ - **No Node.js or iii-engine** β€” runs with plain `python src/app.py`
83
+ - **SQLite instead of Dolt** β€” single file, WAL mode, instant startup
84
+ - **HuggingFace Space ready** β€” deploys in one click, data synced to an HF dataset repo
85
+ - **Same REST + MCP wire format** β€” drop-in for any agent already wired to agentcache
86
+
87
+ 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.
88
+
89
+ ---
90
+
91
+ ## Quick Start
92
+
93
+ ### Run locally
94
+
95
+ ```bash
96
+ # Clone
97
+ git clone https://github.com/Yashwant00CR7/agentcache.git
98
+ cd agentcache
99
+
100
+ # Install dependencies (no build step)
101
+ pip install -r requirements.txt
102
+
103
+ # Start the server
104
+ python src/app.py
105
+ ```
106
+
107
+ Server starts on **http://localhost:3111**. Open the viewer at http://localhost:3111/viewer.
108
+
109
+ ### Verify it works
110
+
111
+ ```bash
112
+ # Health check
113
+ curl http://localhost:3111/agentcache/livez
114
+ # {"status": "ok"}
115
+
116
+ # Save a memory
117
+ curl -X POST http://localhost:3111/agentcache/remember \
118
+ -H "Content-Type: application/json" \
119
+ -d '{"content": "JWT auth uses jose middleware in src/middleware/auth.ts", "concepts": ["auth", "jwt"]}'
120
+
121
+ # Recall it
122
+ curl -X POST http://localhost:3111/agentcache/search \
123
+ -H "Content-Type: application/json" \
124
+ -d '{"query": "authentication middleware", "limit": 5}'
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Features
130
+
131
+ | Feature | Status | Notes |
132
+ |---------|--------|-------|
133
+ | REST API β€” sessions, memories, observations | βœ… | Full surface |
134
+ | WebSocket live stream | βœ… | `/stream/mem-live/viewer` |
135
+ | MCP tools endpoint | βœ… | 31 tools |
136
+ | Built-in HTML viewer | βœ… | Real-time dashboard at `/viewer` |
137
+ | BM25 keyword search | βœ… | Always on, no API key needed |
138
+ | Hybrid BM25 + vector search | βœ… | Requires `GEMINI_API_KEY` |
139
+ | 4-tier memory consolidation | βš™οΈ | `CONSOLIDATION_ENABLED=true` + LLM key |
140
+ | Knowledge graph extraction | βš™οΈ | `GRAPH_EXTRACTION_ENABLED=true` + LLM key |
141
+ | LLM observation compression | βš™οΈ | `AGENTCACHE_AUTO_COMPRESS=true` + LLM key |
142
+ | Lessons with confidence decay | βœ… | Fingerprinted, auto-strengthen on repeat |
143
+ | Memory slots (pinned context) | βœ… | CRUD + auto-reflect |
144
+ | Session replay | βœ… | Full timeline in viewer |
145
+ | Audit log | βœ… | Tracks every write with agent_id + timestamp |
146
+ | HuggingFace Space deploy | βœ… | One-click, data synced to dataset repo |
147
+ | Privacy filtering | βœ… | Strips API keys, tokens before storage |
148
+
149
+ ### 4-Tier Memory Model
150
+
151
+ Inspired by how human memory works β€” raw experience β†’ compressed episodes β†’ extracted facts β†’ learned patterns.
152
+
153
+ | Tier | What | When |
154
+ |------|------|------|
155
+ | **Working** | Raw observations from tool use | Every tool call |
156
+ | **Episodic** | Compressed session summaries | Session end |
157
+ | **Semantic** | Extracted facts and patterns | Consolidation |
158
+ | **Procedural** | Workflows and decision patterns | Consolidation |
159
+
160
+ ---
161
+
162
+ ## MCP Integration
163
+
164
+ Wire agentcache-python into your agent's MCP config. It speaks the same MCP protocol as the Node.js original.
165
+
166
+ ### Most agents (Cursor, Claude Desktop, Cline, Windsurf)
167
+
168
+ ```json
169
+ {
170
+ "mcpServers": {
171
+ "agentcache": {
172
+ "command": "npx",
173
+ "args": ["-y", "@agentcache/mcp"],
174
+ "env": {
175
+ "AGENTCACHE_URL": "http://localhost:3111"
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ ### Claude Code
183
+
184
+ Paste this prompt and your agent will wire everything:
185
+
186
+ ```
187
+ Start agentcache-python: run `python src/app.py` from the agentcache-python directory.
188
+ Then add this MCP server to ~/.claude.json under mcpServers:
189
+ {
190
+ "agentcache": {
191
+ "command": "npx",
192
+ "args": ["-y", "@agentcache/mcp"],
193
+ "env": { "AGENTCACHE_URL": "http://localhost:3111" }
194
+ }
195
+ }
196
+ Verify with: curl http://localhost:3111/agentcache/livez
197
+ Open the viewer at: http://localhost:3111/viewer
198
+ ```
199
+
200
+ ### Available MCP Tools (31)
201
+
202
+ | Tool | Description |
203
+ |------|-------------|
204
+ | `memory_save` | Save a long-term insight, decision, or pattern |
205
+ | `memory_recall` | Search past observations by keyword |
206
+ | `memory_smart_search` | Hybrid BM25 + vector semantic search |
207
+ | `memory_sessions` | List recent sessions |
208
+ | `memory_sessions_list` | Retrieve all memory sessions |
209
+ | `memory_timeline` | Chronological observations for a session |
210
+ | `memory_observations` | Observations for a session |
211
+ | `memory_profile` | Per-project concept + file profile |
212
+ | `memory_lessons` | List active lessons with confidence scores |
213
+ | `memory_lesson_save` | Save a lesson (duplicate saves strengthen it) |
214
+ | `memory_lesson_recall` | Search lessons by query |
215
+ | `memory_lesson_search` | Search lessons by keywords |
216
+ | `memory_consolidate` | Run 4-tier memory consolidation |
217
+ | `memory_reflect` | Reflect on session, update context |
218
+ | `memory_diagnose` | Health check across all subsystems |
219
+ | `memory_forget` | Delete memory, session, or observations |
220
+ | `memory_export` | Export all memory data as JSON |
221
+ | `agent_observe` | Log agent execution observation |
222
+ | `agent_remember` | Save agent cache to long-term storage |
223
+ | `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
224
+ | `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect |
225
+ | `memory_slot_list` | List all pinned memory slots |
226
+ | `memory_slot_get` | Retrieve a specific pinned memory slot |
227
+ | `memory_slot_create` | Create/overwrite a pinned memory slot |
228
+ | `memory_slot_append` | Append text content to a pinned memory slot |
229
+ | `memory_slot_replace` | Replace pinned memory slot content |
230
+ | `memory_slot_delete` | Delete a pinned memory slot |
231
+ | `memory_action_create` | Create a new work item / action |
232
+ | `memory_action_update` | Update fields of an existing action |
233
+ | `memory_frontier` | Get active and pending actions sorted by priority |
234
+ | `memory_crystallize` | Crystallize/summarize observations in a session |
235
+
236
+ ---
237
+
238
+ ## API Reference
239
+
240
+ Base URL: `http://localhost:3111/agentcache`
241
+
242
+ ### Health
243
+
244
+ | Method | Path | Description |
245
+ |--------|------|-------------|
246
+ | `GET` | `/livez` | Liveness probe β€” no auth required |
247
+
248
+ ### Sessions
249
+
250
+ | Method | Path | Description |
251
+ |--------|------|-------------|
252
+ | `POST` | `/session/start` | Start a new session |
253
+ | `POST` | `/session/end` | End a session |
254
+ | `POST` | `/session/commit` | Commit session with summary |
255
+ | `GET` | `/sessions` | List all sessions |
256
+
257
+ ### Observations
258
+
259
+ | Method | Path | Description |
260
+ |--------|------|-------------|
261
+ | `POST` | `/observe` | Ingest a hook event observation |
262
+ | `POST` | `/agent/observe` | Simplified observe for direct agent use |
263
+ | `GET` | `/observations` | List observations (`?session_id=`) |
264
+ | `POST` | `/timeline` | Chronological observation window |
265
+
266
+ ### Memories
267
+
268
+ | Method | Path | Description |
269
+ |--------|------|-------------|
270
+ | `POST` | `/remember` | Save long-term memory |
271
+ | `POST` | `/agent/remember` | Simplified remember |
272
+ | `POST` | `/forget` | Delete memory / session / observations |
273
+ | `POST` | `/search` | BM25 + vector search |
274
+ | `POST` | `/context` | Compile context for a session + project |
275
+ | `GET` | `/memories` | List memories (`?latest=true&limit=N`) |
276
+ | `POST` | `/evolve` | Create a new memory version |
277
+
278
+ ### Lessons
279
+
280
+ | Method | Path | Description |
281
+ |--------|------|-------------|
282
+ | `GET` | `/lessons` | List lessons |
283
+ | `POST` | `/lessons` | Create lesson |
284
+ | `POST` | `/lessons/search` | Search lessons |
285
+ | `POST` | `/lessons/strengthen` | Reinforce an existing lesson |
286
+
287
+ ### Slots
288
+
289
+ | Method | Path | Description |
290
+ |--------|------|-------------|
291
+ | `GET` | `/slots` | List all pinned slots |
292
+ | `POST` | `/slot` | Create or update a slot |
293
+ | `GET` | `/slot` | Get slot by name |
294
+ | `DELETE` | `/slot` | Delete a slot |
295
+ | `POST` | `/slot/reflect` | Auto-populate from session observations |
296
+
297
+ ### Graph + Profile
298
+
299
+ | Method | Path | Description |
300
+ |--------|------|-------------|
301
+ | `GET` | `/relations` | Knowledge graph edges |
302
+ | `POST` | `/relations` | Add a relation |
303
+ | `GET` | `/profile` | Project profile (top concepts, files) |
304
+
305
+ ### Actions
306
+
307
+ | Method | Path | Description |
308
+ |--------|------|-------------|
309
+ | `GET` | `/actions` | List actions |
310
+ | `POST` | `/actions` | Create an action |
311
+ | `PATCH` | `/actions/<id>` | Update action status / fields |
312
+ | `GET` | `/frontier` | Pending actions sorted by priority |
313
+ | `GET` | `/insights` | List insights |
314
+
315
+ ### Replay
316
+
317
+ | Method | Path | Description |
318
+ |--------|------|-------------|
319
+ | `GET` | `/replay/sessions` | Sessions list for replay tab |
320
+ | `GET` | `/replay/load` | Full session + observations (`?sessionId=`) |
321
+
322
+ ### MCP
323
+
324
+ | Method | Path | Description |
325
+ |--------|------|-------------|
326
+ | `GET` | `/mcp/tools` | MCP tool schema list |
327
+ | `POST` | `/mcp/tools` | MCP tool call dispatch |
328
+
329
+ ---
330
+
331
+ ## Configuration
332
+
333
+ Create `~/.agentcache/.env` (no `export` prefix needed):
334
+
335
+ ```env
336
+ # Server port
337
+ III_REST_PORT=3111
338
+
339
+ # Vector search β€” enables Gemini 768-dim embeddings
340
+ GEMINI_API_KEY=your-gemini-key
341
+
342
+ # LLM for compression / consolidation / graph extraction
343
+ # Any one of these enables LLM features:
344
+ ANTHROPIC_API_KEY=your-anthropic-key
345
+ # OPENAI_API_KEY=your-openai-key
346
+ # GEMINI_API_KEY=your-key (same key as above works for both)
347
+
348
+ # LLM-powered features (disabled by default β€” spend tokens)
349
+ CONSOLIDATION_ENABLED=true
350
+ GRAPH_EXTRACTION_ENABLED=true
351
+ AGENTCACHE_AUTO_COMPRESS=true
352
+
353
+ # Context injection limits
354
+ TOKEN_BUDGET=2000
355
+ MAX_OBS_PER_SESSION=500
356
+
357
+ # Auth β€” set to require Bearer token on all endpoints
358
+ AGENTCACHE_SECRET=your-secret
359
+
360
+ # Agent scope isolation
361
+ AGENT_ID=my-agent
362
+ AGENTCACHE_AGENT_SCOPE=isolated # only see this agent's data
363
+
364
+ # HuggingFace sync
365
+ HF_TOKEN=your-hf-token
366
+ AGENTCACHE_DATASET_REPO=username/agentcache-data
367
+ ```
368
+
369
+ ### Full Variable Reference
370
+
371
+ | Variable | Default | Purpose |
372
+ |----------|---------|---------|
373
+ | `III_REST_PORT` / `PORT` | `3111` | API server port |
374
+ | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | β€” | Enables 768-dim vector search |
375
+ | `AGENTCACHE_SECRET` | β€” | Bearer token auth on all endpoints |
376
+ | `AGENT_ID` | β€” | Default agent ID for scope isolation |
377
+ | `AGENTCACHE_AGENT_SCOPE=isolated` | β€” | Filters data to current `AGENT_ID` |
378
+ | `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
379
+ | `TOKEN_BUDGET` | `2000` | Max tokens in compiled context |
380
+ | `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph (needs LLM) |
381
+ | `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
382
+ | `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM observation compression |
383
+
384
+ ---
385
+
386
+ ## Viewer
387
+
388
+ Built-in dashboard at **http://localhost:3111/viewer**.
389
+
390
+ | Tab | What You See |
391
+ |-----|-------------|
392
+ | **Dashboard** | Session stats, memory counts, recent activity |
393
+ | **Sessions** | Browse sessions, inspect observations |
394
+ | **Memories** | Search, filter, and read long-term memories |
395
+ | **Graph** | Project folder visualization β€” nodes = folders, edges = shared concepts or parent path |
396
+ | **Timeline** | Per-session chronological observation view |
397
+ | **Lessons** | Confidence-scored lessons with decay tracking |
398
+ | **Slots** | Pinned memory slots editor |
399
+ | **Replay** | Scrub through past sessions frame by frame |
400
+
401
+ ---
402
+
403
+ ## Deploy to HuggingFace
404
+
405
+ This project is designed to run as a HuggingFace Space. Data is stored in an HF dataset repo and restored on every boot β€” so no persistent disk is needed.
406
+
407
+ ### Setup
408
+
409
+ 1. Fork this repo as a HuggingFace Space (SDK: Docker)
410
+ 2. Create a dataset repo (e.g. `your-username/agentcache-data`)
411
+ 3. Add Space secrets in the HF dashboard:
412
+
413
+ | Secret | Value |
414
+ |--------|-------|
415
+ | `HF_TOKEN` | Your HF write token |
416
+ | `AGENTCACHE_DATASET_REPO` | `your-username/agentcache-data` |
417
+ | `AGENTCACHE_SECRET` | A random secret (optional but recommended) |
418
+ | `GEMINI_API_KEY` | Gemini key (optional, enables vector search) |
419
+
420
+ 4. The Space boots, restores `agentcache.db` from the dataset repo, and starts the server
421
+
422
+ ### How sync works
423
+
424
+ `sync.py` uses mtime fingerprinting β€” it only uploads when the database actually changed, so there are no unnecessary uploads during idle periods.
425
+
426
+ ```bash
427
+ # Manual backup
428
+ python sync.py
429
+
430
+ # Environment for sync
431
+ HF_TOKEN=...
432
+ AGENTCACHE_DATASET_REPO=username/agentcache-data
433
+ ```
434
+
435
+ ---
436
+
437
+ ## Architecture
438
+
439
+ ```
440
+ agentcache-python/
441
+ β”œβ”€β”€ src/
442
+ β”‚ β”œβ”€β”€ app.py Flask server β€” all endpoints, WebSocket broadcaster
443
+ β”‚ β”œβ”€β”€ db.py SQLite StateKV β€” WAL mode, audit_log table
444
+ β”‚ β”œβ”€β”€ functions.py Core logic β€” observe, remember, search, context
445
+ β”‚ β”œβ”€β”€ search.py BM25 + Gemini vector index + HybridSearch (RRF)
446
+ β”‚ └── viewer/
447
+ β”‚ └── index.html Single-file HTML dashboard (no build step)
448
+ β”œβ”€β”€ sync.py HuggingFace dataset backup/restore
449
+ β”œβ”€β”€ Dockerfile HF Space container
450
+ β”œβ”€β”€ start.sh Boot script (restore β†’ start server β†’ start sync)
451
+ └── requirements.txt 6 Python dependencies, no external DB required
452
+ ```
453
+
454
+ ### Database layout
455
+
456
+ Two SQLite tables in `~/.agentcache/agentcache.db`:
457
+
458
+ ```sql
459
+ -- All data lives here, namespaced by scope
460
+ kv_store (
461
+ scope TEXT NOT NULL, -- e.g. "mem:sessions", "mem:obs:{session_id}"
462
+ key TEXT NOT NULL,
463
+ value TEXT NOT NULL, -- JSON-serialized
464
+ PRIMARY KEY (scope, key)
465
+ )
466
+
467
+ -- Audit trail replaces Dolt git versioning
468
+ audit_log (
469
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
470
+ ts INTEGER NOT NULL, -- unix millis
471
+ agent_id TEXT NOT NULL,
472
+ message TEXT NOT NULL
473
+ )
474
+ ```
475
+
476
+ ### Search pipeline
477
+
478
+ ```
479
+ Query
480
+ β†’ BM25 (always) β€” Porter-stemmed keyword matching
481
+ β†’ Vector (if Gemini key) β€” 768-dim cosine similarity
482
+ β†’ RRF fusion β€” Reciprocal Rank Fusion (k=60)
483
+ β†’ Session diversify β€” max 3 results per session
484
+ β†’ Return top-K
485
+ ```
486
+
487
+ ---
488
+
489
+ ## vs Original agentcache
490
+
491
+ | | agentcache (Node.js) | agentcache-python |
492
+ |---|---|---|
493
+ | Runtime | Node.js 20+ | Python 3.10+ |
494
+ | Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
495
+ | Engine dependency | iii-engine (separate binary) | None β€” just Flask |
496
+ | Embeddings | 6 providers + local `@xenova/transformers` | Gemini 768-dim |
497
+ | MCP tools | 53 | 31 |
498
+ | REST endpoints | 128 | ~50 |
499
+ | Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
500
+ | Cold boot | ~7s (iii engine warm-up) | <2s |
501
+ | Database size | ~232MB (417 Dolt chunk files) | ~20MB (single `.db` file) |
502
+ | Setup | `npm install -g @agentcache/agentcache` | `pip install -r requirements.txt` |
503
+
504
+ Choose the Python version for: simpler setup, HF Space deployment, single-file database, no Node.js, or Python ecosystem integration.
505
+
506
+ Choose the Node.js version for: the full 53-tool MCP surface, iii-engine observability, production multi-agent deployments, or the full auto-hook suite.
507
+
508
+ ---
509
+
510
+ ## Contributing
511
+
512
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.
513
+
514
+ Priority areas: test coverage, additional embedding providers, more agent hook scripts.
515
+
516
+ ---
517
+
518
+ ## License
519
+
520
+ Apache-2.0 β€” see [LICENSE](LICENSE).
src/agentcache.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/agentcache/__init__.py
5
+ src/agentcache/app.py
6
+ src/agentcache/cli.py
7
+ src/agentcache/connect.py
8
+ src/agentcache/db.py
9
+ src/agentcache/functions.py
10
+ src/agentcache/import_data.py
11
+ src/agentcache/mcp_stdio.py
12
+ src/agentcache/replay_import.py
13
+ src/agentcache/search.py
14
+ src/agentcache/viewer_helpers.py
15
+ src/agentcache/workers.py
16
+ src/agentcache.egg-info/PKG-INFO
17
+ src/agentcache.egg-info/SOURCES.txt
18
+ src/agentcache.egg-info/dependency_links.txt
19
+ src/agentcache.egg-info/entry_points.txt
20
+ src/agentcache.egg-info/requires.txt
21
+ src/agentcache.egg-info/top_level.txt
22
+ src/agentcache/cache/__init__.py
23
+ src/agentcache/cache/context.py
24
+ src/agentcache/cache/graph.py
25
+ src/agentcache/cache/health.py
26
+ src/agentcache/cache/observe.py
27
+ src/agentcache/cache/remember.py
28
+ src/agentcache/cache/timeline.py
29
+ src/agentcache/routes/__init__.py
30
+ src/agentcache/routes/graph.py
31
+ src/agentcache/routes/health.py
32
+ src/agentcache/routes/mcp.py
33
+ src/agentcache/routes/memories.py
34
+ src/agentcache/routes/migration.py
35
+ src/agentcache/routes/observations.py
36
+ src/agentcache/routes/search.py
37
+ src/agentcache/storage/__init__.py
38
+ src/agentcache/storage/images.py
39
+ src/agentcache/storage/paths.py
40
+ src/agentcache/storage/scopes.py
41
+ src/agentcache/viewer/favicon.svg
42
+ src/agentcache/viewer/index.html
43
+ tests/test_api.py
44
+ tests/test_auth.py
45
+ tests/test_context.py
46
+ tests/test_debounce.py
47
+ tests/test_folder_graph_build.py
48
+ tests/test_folder_observe.py
49
+ tests/test_forget.py
50
+ tests/test_graph.py
51
+ tests/test_migration.py
52
+ tests/test_normalize.py
53
+ tests/test_obs_lookup.py
54
+ tests/test_observe_core.py
55
+ tests/test_properties.py
56
+ tests/test_remember.py
57
+ tests/test_route_regressions.py
58
+ tests/test_search.py
59
+ tests/test_timeline.py
src/agentcache.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
src/agentcache.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ agentcache = agentcache.cli:main
src/agentcache.egg-info/requires.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=3.0.0
2
+ flask-sock>=0.7.0
3
+ requests>=2.31.0
4
+ python-dateutil>=2.8.2
5
+ huggingface_hub>=0.20.0
6
+ websockets>=12.0
7
+
8
+ [dev]
9
+ pytest>=8.0.0
10
+ hypothesis>=6.100.0
11
+ ruff>=0.3.0
12
+ twine>=5.0.0
13
+
14
+ [local-embeddings]
15
+ sentence-transformers>=2.7.0
src/agentcache.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ agentcache
src/agentcache/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ agentcache β€” A Python REST + WebSocket + MCP cache server for AI agents, backed by SQLite.
3
+ """
4
+
5
+ __version__ = "0.9.8"
6
+
7
+ from .app import create_app
8
+ from .connect import run_connect
9
+ from .db import StateKV
10
+ from .functions import (
11
+ folder_graph_build,
12
+ folder_observe,
13
+ folder_search,
14
+ folder_timeline,
15
+ forget,
16
+ health_check,
17
+ remember,
18
+ )
19
+
20
+ __all__ = [
21
+ "__version__",
22
+ "create_app",
23
+ "StateKV",
24
+ "run_connect",
25
+ "folder_observe",
26
+ "folder_search",
27
+ "folder_timeline",
28
+ "folder_graph_build",
29
+ "remember",
30
+ "forget",
31
+ "health_check",
32
+ ]
src/{app.py β†’ agentcache/app.py} RENAMED
@@ -1,280 +1,284 @@
1
- """
2
- agentmemory-python β€” Flask application factory.
3
-
4
- Entry point: create_app() returns a fully configured Flask app.
5
- Run directly: python src/app.py
6
- """
7
-
8
- import os
9
- import sys
10
- import json
11
- import hmac
12
- from flask import Flask, request, send_from_directory
13
- from flask_sock import Sock
14
-
15
- # Prevent double-import of app when run directly as __main__
16
- if __name__ == "__main__":
17
- sys.modules["app"] = sys.modules["__main__"]
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:
27
- with open(env_path, "r", encoding="utf-8") as f:
28
- for line in f:
29
- line = line.strip()
30
- if not line or line.startswith("#") or "=" not in line:
31
- continue
32
- k, v = line.split("=", 1)
33
- os.environ[k.strip()] = v.strip().strip('"').strip("'")
34
- print(f"[config] Loaded environment from {env_path}")
35
- except Exception as e:
36
- print(f"[config] Error reading env file: {e}")
37
-
38
-
39
- _load_env()
40
-
41
- # Module-level globals β€” set once by create_app(), read by blueprints via `import app`
42
- kv = None
43
- embedding_provider = None
44
- persistence = None
45
-
46
-
47
- def create_app() -> Flask:
48
- """Create and return a fully configured Flask application."""
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(
54
- "[security] WARNING: AGENTCACHE_SECRET/AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication."
55
- )
56
-
57
- import search as search_mod
58
- import functions
59
- from db import StateKV
60
- from viewer_helpers import make_viewer_response
61
-
62
- # 1. DB
63
- kv = StateKV()
64
-
65
- # 2. Embedding provider β€” auto-select by priority (D5.3):
66
- # GEMINI_API_KEY β†’ OPENAI_API_KEY β†’ AGENTCACHE_LOCAL_EMBEDDING_MODEL β†’ BM25-only
67
- api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
68
- openai_key = os.getenv("OPENAI_API_KEY")
69
- local_model = os.getenv("AGENTCACHE_LOCAL_EMBEDDING_MODEL") or os.getenv(
70
- "AGENTMEMORY_LOCAL_EMBEDDING_MODEL"
71
- )
72
-
73
- if api_key:
74
- try:
75
- embedding_provider = search_mod.GeminiEmbeddingProvider(api_key)
76
- functions.set_embedding_provider(embedding_provider)
77
- print(
78
- f"[search] Embedding provider active: gemini ({embedding_provider.dimensions} dims)"
79
- )
80
- except Exception as e:
81
- print(f"[search] Error initialising Gemini embedding provider: {e}")
82
- elif openai_key:
83
- try:
84
- embedding_provider = search_mod.OpenAIEmbeddingProvider(openai_key)
85
- functions.set_embedding_provider(embedding_provider)
86
- print(
87
- f"[search] Embedding provider active: openai ({embedding_provider.dimensions} dims)"
88
- )
89
- except Exception as e:
90
- print(f"[search] Error initialising OpenAI embedding provider: {e}")
91
- elif local_model:
92
- try:
93
- embedding_provider = search_mod.SentenceTransformerProvider(local_model)
94
- functions.set_embedding_provider(embedding_provider)
95
- print(
96
- f"[search] Embedding provider active: sentence-transformers/{local_model} ({embedding_provider.dimensions} dims)"
97
- )
98
- except ImportError as e:
99
- print(f"[search] sentence-transformers not installed: {e}")
100
- except Exception as e:
101
- print(f"[search] Error initialising SentenceTransformer provider: {e}")
102
- else:
103
- print("[search] No embedding API key found β€” running in BM25-only mode.")
104
-
105
- # 3. Index persistence β€” use embedding_provider variable set above
106
- has_vector = embedding_provider is not None
107
- persistence = functions.IndexPersistence(
108
- kv,
109
- functions._bm25_index,
110
- functions._vector_index if has_vector else None,
111
- )
112
- functions.set_index_persistence(persistence)
113
- loaded = persistence.load()
114
- print(
115
- f"[persistence] Load results: BM25={loaded['bm25']}, Vector={loaded['vector']}"
116
- )
117
-
118
- # Backfill coordinate lookup index if missing/incomplete
119
- try:
120
- functions.backfill_obs_lookup_if_needed(kv)
121
- except Exception as e:
122
- print(f"[db] Warning backfilling obs_lookup: {e}")
123
-
124
- # 4. Flask app + blueprints
125
- flask_app = Flask(__name__)
126
- from werkzeug.middleware.proxy_fix import ProxyFix
127
-
128
- flask_app.wsgi_app = ProxyFix(
129
- flask_app.wsgi_app, x_proto=1, x_host=1, x_port=1, x_prefix=1
130
- )
131
- from routes import register_blueprints
132
-
133
- register_blueprints(flask_app)
134
-
135
- # 5. WebSocket broadcaster
136
- sock = Sock(flask_app)
137
- _ws_clients: set = set()
138
-
139
- @sock.route("/stream/mem-live/viewer")
140
- def stream_viewer(ws):
141
- secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
142
- if secret:
143
- token = request.args.get("token") or request.args.get("secret")
144
- if not token or not hmac.compare_digest(
145
- token.encode("utf-8"), secret.encode("utf-8")
146
- ):
147
- ws.close(1008)
148
- return
149
- _ws_clients.add(ws)
150
- try:
151
- while ws.receive() is not None:
152
- pass
153
- except Exception:
154
- pass
155
- finally:
156
- _ws_clients.discard(ws)
157
-
158
- def _broadcast(payload: dict) -> None:
159
- msg = json.dumps(payload)
160
- for ws in list(_ws_clients):
161
- try:
162
- ws.send(msg)
163
- except Exception:
164
- _ws_clients.discard(ws)
165
-
166
- functions.set_stream_broadcaster(_broadcast)
167
-
168
- # 6. Viewer static routes
169
- _base_dir = os.path.dirname(os.path.abspath(__file__))
170
-
171
- @flask_app.route("/")
172
- @flask_app.route("/viewer")
173
- @flask_app.route("/agentcache/viewer")
174
- @flask_app.route("/agentmemory/viewer")
175
- def serve_viewer():
176
- try:
177
- return make_viewer_response(_base_dir)
178
- except Exception as e:
179
- return f"Viewer not found: {e}", 404
180
-
181
- @flask_app.route("/favicon.svg")
182
- def serve_favicon():
183
- return send_from_directory(os.path.join(_base_dir, "viewer"), "favicon.svg")
184
-
185
- # 7. CORS after_request β€” D2.1: configurable via AGENTCACHE_CORS_ORIGINS env var
186
- # Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
187
- # Wildcard entries like "*.hf.space" match any subdomain via suffix check.
188
- _default_cors = (
189
- "http://localhost,http://127.0.0.1,"
190
- "https://huggingface.co,https://*.hf.space,"
191
- "vscode-webview://*,chrome-extension://*"
192
- )
193
- _cors_origins_raw = (
194
- os.getenv("AGENTCACHE_CORS_ORIGINS")
195
- or os.getenv("AGENTMEMORY_CORS_ORIGINS")
196
- or _default_cors
197
- )
198
-
199
- def _parse_cors_origins(raw: str):
200
- """Return (exact_set, suffix_list) for efficient origin matching."""
201
- exact, suffixes = set(), []
202
- for entry in raw.split(","):
203
- entry = entry.strip()
204
- if not entry:
205
- continue
206
- if entry.startswith("*."):
207
- # *.hf.space β†’ match anything ending with .hf.space
208
- suffixes.append(entry[1:].lower()) # keep the leading dot: ".hf.space"
209
- elif "*" in entry:
210
- # generic prefix wildcard: strip trailing * and treat as prefix
211
- suffixes.append(("prefix:", entry.rstrip("*").lower()))
212
- else:
213
- exact.add(entry.lower())
214
- return exact, suffixes
215
-
216
- _cors_exact, _cors_suffixes = _parse_cors_origins(_cors_origins_raw)
217
-
218
- def _origin_allowed(origin: str) -> bool:
219
- lo = origin.lower()
220
- if lo in _cors_exact:
221
- return True
222
- for s in _cors_suffixes:
223
- if isinstance(s, tuple) and s[0] == "prefix:":
224
- if lo.startswith(s[1]):
225
- return True
226
- elif lo.endswith(s):
227
- return True
228
- return False
229
-
230
- @flask_app.after_request
231
- def _cors(response):
232
- origin = request.headers.get("Origin")
233
- if origin and _origin_allowed(origin):
234
- response.headers["Access-Control-Allow-Origin"] = origin
235
- response.headers["Access-Control-Allow-Credentials"] = "true"
236
- response.headers.add(
237
- "Access-Control-Allow-Headers", "Content-Type, Authorization"
238
- )
239
- response.headers.add(
240
- "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"
241
- )
242
- return response
243
-
244
- # Handle CORS preflight OPTIONS requests globally
245
- from flask import Response as _FlaskResponse
246
-
247
- @flask_app.before_request
248
- def _handle_options():
249
- if request.method == "OPTIONS":
250
- origin = request.headers.get("Origin", "")
251
- if origin and _origin_allowed(origin):
252
- resp = _FlaskResponse("", status=204)
253
- resp.headers["Access-Control-Allow-Origin"] = origin
254
- resp.headers["Access-Control-Allow-Credentials"] = "true"
255
- resp.headers["Access-Control-Allow-Headers"] = (
256
- "Content-Type, Authorization"
257
- )
258
- resp.headers["Access-Control-Allow-Methods"] = (
259
- "GET, POST, PUT, DELETE, OPTIONS"
260
- )
261
- resp.headers["Access-Control-Max-Age"] = "86400"
262
- return resp
263
-
264
- # 8. Background workers
265
- from workers import start_background_workers
266
-
267
- start_background_workers(kv)
268
-
269
- return flask_app
270
-
271
-
272
- def main() -> None:
273
- flask_app = create_app()
274
- port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
275
- print(f"[main] Starting Flask daemon on port {port}...")
276
- flask_app.run(host="0.0.0.0", port=port, debug=False)
277
-
278
-
279
- if __name__ == "__main__":
280
- main()
 
 
 
 
 
1
+ """
2
+ agentmemory-python β€” Flask application factory.
3
+
4
+ Entry point: create_app() returns a fully configured Flask app.
5
+ Run directly: python src/app.py
6
+ """
7
+
8
+ import hmac
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ from flask import Flask, request, send_from_directory
14
+ from flask_sock import Sock
15
+
16
+ # Prevent double-import of app when run directly as __main__
17
+ if __name__ == "__main__":
18
+ sys.modules["app"] = sys.modules["__main__"]
19
+
20
+
21
+ def _load_env() -> None:
22
+ env_path = os.path.join(os.path.expanduser("~"), ".agentcache", ".env")
23
+ if not os.path.exists(env_path):
24
+ env_path = os.path.join(os.path.expanduser("~"), ".agentmemory", ".env")
25
+ if not os.path.exists(env_path):
26
+ return
27
+ try:
28
+ with open(env_path, "r", encoding="utf-8") as f:
29
+ for line in f:
30
+ line = line.strip()
31
+ if not line or line.startswith("#") or "=" not in line:
32
+ continue
33
+ k, v = line.split("=", 1)
34
+ os.environ[k.strip()] = v.strip().strip('"').strip("'")
35
+ print(f"[config] Loaded environment from {env_path}")
36
+ except Exception as e:
37
+ print(f"[config] Error reading env file: {e}")
38
+
39
+
40
+ _load_env()
41
+
42
+ # Module-level globals β€” set once by create_app(), read by blueprints via `import app`
43
+ kv = None
44
+ embedding_provider = None
45
+ persistence = None
46
+
47
+
48
+ def create_app() -> Flask:
49
+ """Create and return a fully configured Flask application."""
50
+ global kv, embedding_provider, persistence
51
+
52
+ # Check security credentials
53
+ if not os.getenv("AGENTCACHE_SECRET") and not os.getenv("AGENTMEMORY_SECRET"):
54
+ print(
55
+ "[security] WARNING: AGENTCACHE_SECRET/AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication."
56
+ )
57
+
58
+ from . import functions
59
+ from . import search as search_mod
60
+ from .db import StateKV
61
+ from .viewer_helpers import make_viewer_response
62
+
63
+ # 1. DB
64
+ kv = StateKV()
65
+
66
+ # 2. Embedding provider β€” auto-select by priority (D5.3):
67
+ # GEMINI_API_KEY β†’ OPENAI_API_KEY β†’ AGENTCACHE_LOCAL_EMBEDDING_MODEL β†’ BM25-only
68
+ api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
69
+ openai_key = os.getenv("OPENAI_API_KEY")
70
+ local_model = os.getenv("AGENTCACHE_LOCAL_EMBEDDING_MODEL") or os.getenv(
71
+ "AGENTMEMORY_LOCAL_EMBEDDING_MODEL"
72
+ )
73
+
74
+ if api_key:
75
+ try:
76
+ embedding_provider = search_mod.GeminiEmbeddingProvider(api_key)
77
+ functions.set_embedding_provider(embedding_provider)
78
+ print(
79
+ f"[search] Embedding provider active: gemini ({embedding_provider.dimensions} dims)"
80
+ )
81
+ except Exception as e:
82
+ print(f"[search] Error initialising Gemini embedding provider: {e}")
83
+ elif openai_key:
84
+ try:
85
+ embedding_provider = search_mod.OpenAIEmbeddingProvider(openai_key)
86
+ functions.set_embedding_provider(embedding_provider)
87
+ print(
88
+ f"[search] Embedding provider active: openai ({embedding_provider.dimensions} dims)"
89
+ )
90
+ except Exception as e:
91
+ print(f"[search] Error initialising OpenAI embedding provider: {e}")
92
+ elif local_model:
93
+ try:
94
+ embedding_provider = search_mod.SentenceTransformerProvider(local_model)
95
+ functions.set_embedding_provider(embedding_provider)
96
+ print(
97
+ f"[search] Embedding provider active: sentence-transformers/{local_model} ({embedding_provider.dimensions} dims)"
98
+ )
99
+ except ImportError as e:
100
+ print(f"[search] sentence-transformers not installed: {e}")
101
+ except Exception as e:
102
+ print(f"[search] Error initialising SentenceTransformer provider: {e}")
103
+ else:
104
+ print("[search] No embedding API key found β€” running in BM25-only mode.")
105
+
106
+ # 3. Index persistence β€” use embedding_provider variable set above
107
+ has_vector = embedding_provider is not None
108
+ persistence = functions.IndexPersistence(
109
+ kv,
110
+ functions._bm25_index,
111
+ functions._vector_index if has_vector else None,
112
+ )
113
+ functions.set_index_persistence(persistence)
114
+ loaded = persistence.load()
115
+ print(
116
+ f"[persistence] Load results: BM25={loaded['bm25']}, Vector={loaded['vector']}"
117
+ )
118
+
119
+ # Backfill coordinate lookup index if missing/incomplete
120
+ try:
121
+ functions.backfill_obs_lookup_if_needed(kv)
122
+ except Exception as e:
123
+ print(f"[db] Warning backfilling obs_lookup: {e}")
124
+
125
+ # 4. Flask app + blueprints
126
+ flask_app = Flask(__name__)
127
+ from werkzeug.middleware.proxy_fix import ProxyFix
128
+
129
+ flask_app.wsgi_app = ProxyFix(
130
+ flask_app.wsgi_app, x_proto=1, x_host=1, x_port=1, x_prefix=1
131
+ )
132
+ from .routes import register_blueprints
133
+
134
+ register_blueprints(flask_app)
135
+
136
+ # 5. WebSocket broadcaster
137
+ sock = Sock(flask_app)
138
+ _ws_clients: set = set()
139
+
140
+ @sock.route("/stream/mem-live/viewer")
141
+ def stream_viewer(ws):
142
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
143
+ if secret:
144
+ token = request.args.get("token") or request.args.get("secret")
145
+ if not token or not hmac.compare_digest(
146
+ token.encode("utf-8"), secret.encode("utf-8")
147
+ ):
148
+ ws.close(1008)
149
+ return
150
+ _ws_clients.add(ws)
151
+ try:
152
+ while ws.receive() is not None:
153
+ pass
154
+ except Exception:
155
+ pass
156
+ finally:
157
+ _ws_clients.discard(ws)
158
+
159
+ def _broadcast(payload: dict) -> None:
160
+ msg = json.dumps(payload)
161
+ for ws in list(_ws_clients):
162
+ try:
163
+ ws.send(msg)
164
+ except Exception:
165
+ _ws_clients.discard(ws)
166
+
167
+ functions.set_stream_broadcaster(_broadcast)
168
+
169
+ # 6. Viewer static routes
170
+ from importlib.resources import files
171
+
172
+ _viewer_resources = files("agentcache").joinpath("viewer")
173
+ _base_dir = str(_viewer_resources.parent)
174
+
175
+ @flask_app.route("/")
176
+ @flask_app.route("/viewer")
177
+ @flask_app.route("/agentcache/viewer")
178
+ @flask_app.route("/agentmemory/viewer")
179
+ def serve_viewer():
180
+ try:
181
+ return make_viewer_response(_base_dir)
182
+ except Exception as e:
183
+ return f"Viewer not found: {e}", 404
184
+
185
+ @flask_app.route("/favicon.svg")
186
+ def serve_favicon():
187
+ return send_from_directory(str(_viewer_resources), "favicon.svg")
188
+
189
+ # 7. CORS after_request β€” D2.1: configurable via AGENTCACHE_CORS_ORIGINS env var
190
+ # Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
191
+ # Wildcard entries like "*.hf.space" match any subdomain via suffix check.
192
+ _default_cors = (
193
+ "http://localhost,http://127.0.0.1,"
194
+ "https://huggingface.co,https://*.hf.space,"
195
+ "vscode-webview://*,chrome-extension://*"
196
+ )
197
+ _cors_origins_raw = (
198
+ os.getenv("AGENTCACHE_CORS_ORIGINS")
199
+ or os.getenv("AGENTMEMORY_CORS_ORIGINS")
200
+ or _default_cors
201
+ )
202
+
203
+ def _parse_cors_origins(raw: str):
204
+ """Return (exact_set, suffix_list) for efficient origin matching."""
205
+ exact, suffixes = set(), []
206
+ for entry in raw.split(","):
207
+ entry = entry.strip()
208
+ if not entry:
209
+ continue
210
+ if entry.startswith("*."):
211
+ # *.hf.space β†’ match anything ending with .hf.space
212
+ suffixes.append(entry[1:].lower()) # keep the leading dot: ".hf.space"
213
+ elif "*" in entry:
214
+ # generic prefix wildcard: strip trailing * and treat as prefix
215
+ suffixes.append(("prefix:", entry.rstrip("*").lower()))
216
+ else:
217
+ exact.add(entry.lower())
218
+ return exact, suffixes
219
+
220
+ _cors_exact, _cors_suffixes = _parse_cors_origins(_cors_origins_raw)
221
+
222
+ def _origin_allowed(origin: str) -> bool:
223
+ lo = origin.lower()
224
+ if lo in _cors_exact:
225
+ return True
226
+ for s in _cors_suffixes:
227
+ if isinstance(s, tuple) and s[0] == "prefix:":
228
+ if lo.startswith(s[1]):
229
+ return True
230
+ elif lo.endswith(s):
231
+ return True
232
+ return False
233
+
234
+ @flask_app.after_request
235
+ def _cors(response):
236
+ origin = request.headers.get("Origin")
237
+ if origin and _origin_allowed(origin):
238
+ response.headers["Access-Control-Allow-Origin"] = origin
239
+ response.headers["Access-Control-Allow-Credentials"] = "true"
240
+ response.headers.add(
241
+ "Access-Control-Allow-Headers", "Content-Type, Authorization"
242
+ )
243
+ response.headers.add(
244
+ "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"
245
+ )
246
+ return response
247
+
248
+ # Handle CORS preflight OPTIONS requests globally
249
+ from flask import Response as _FlaskResponse
250
+
251
+ @flask_app.before_request
252
+ def _handle_options():
253
+ if request.method == "OPTIONS":
254
+ origin = request.headers.get("Origin", "")
255
+ if origin and _origin_allowed(origin):
256
+ resp = _FlaskResponse("", status=204)
257
+ resp.headers["Access-Control-Allow-Origin"] = origin
258
+ resp.headers["Access-Control-Allow-Credentials"] = "true"
259
+ resp.headers["Access-Control-Allow-Headers"] = (
260
+ "Content-Type, Authorization"
261
+ )
262
+ resp.headers["Access-Control-Allow-Methods"] = (
263
+ "GET, POST, PUT, DELETE, OPTIONS"
264
+ )
265
+ resp.headers["Access-Control-Max-Age"] = "86400"
266
+ return resp
267
+
268
+ # 8. Background workers
269
+ from .workers import start_background_workers
270
+
271
+ start_background_workers(kv)
272
+
273
+ return flask_app
274
+
275
+
276
+ def main() -> None:
277
+ flask_app = create_app()
278
+ port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
279
+ print(f"[main] Starting Flask daemon on port {port}...")
280
+ flask_app.run(host="0.0.0.0", port=port, debug=False)
281
+
282
+
283
+ if __name__ == "__main__":
284
+ main()
src/{auth.md β†’ agentcache/auth.md} RENAMED
File without changes
src/{cache β†’ agentcache/cache}/__init__.py RENAMED
@@ -13,24 +13,23 @@ Compatibility shim: also re-exports everything from functions.py that
13
  callers may import from this package (A2.2).
14
  """
15
 
16
- from .observe import (
17
- folder_observe,
18
- observe,
19
- build_synthetic_compression,
20
- strip_private_data,
21
- )
22
- from .remember import remember, forget, jaccard_similarity
23
- from .context import context, export_data, rebuild_index
24
- from .graph import folder_graph_build
25
- from .timeline import folder_timeline, folder_search
26
- from .health import health_check, auto_forget
27
-
28
  # ---------------------------------------------------------------------------
29
  # Compatibility shim β€” delegate additional names to functions.py (A2.2)
30
  # Each name is imported lazily via a try/except so missing items don't break
31
  # the package import on partially-initialised environments.
32
  # ---------------------------------------------------------------------------
33
- import functions as _fn # noqa: E402 (functions.py is on sys.path via src/)
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  KV = _fn.KV
36
  generate_id = _fn.generate_id
 
13
  callers may import from this package (A2.2).
14
  """
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # ---------------------------------------------------------------------------
17
  # Compatibility shim β€” delegate additional names to functions.py (A2.2)
18
  # Each name is imported lazily via a try/except so missing items don't break
19
  # the package import on partially-initialised environments.
20
  # ---------------------------------------------------------------------------
21
+ from .. import functions as _fn # noqa: E402
22
+ from .context import context, export_data, rebuild_index
23
+ from .graph import folder_graph_build
24
+ from .health import auto_forget, health_check
25
+ from .observe import (
26
+ build_synthetic_compression,
27
+ folder_observe,
28
+ observe,
29
+ strip_private_data,
30
+ )
31
+ from .remember import forget, jaccard_similarity, remember
32
+ from .timeline import folder_search, folder_timeline
33
 
34
  KV = _fn.KV
35
  generate_id = _fn.generate_id
src/{cache β†’ agentcache/cache}/context.py RENAMED
@@ -11,8 +11,8 @@ from __future__ import annotations
11
 
12
  from typing import Any, Dict
13
 
14
- from db import StateKV
15
- import functions as _fn
16
 
17
 
18
  def context(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
 
11
 
12
  from typing import Any, Dict
13
 
14
+ from .. import functions as _fn
15
+ from ..db import StateKV
16
 
17
 
18
  def context(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
src/{cache β†’ agentcache/cache}/graph.py RENAMED
@@ -9,8 +9,8 @@ from __future__ import annotations
9
 
10
  from typing import Any, Dict
11
 
12
- from db import StateKV
13
- import functions as _fn
14
 
15
 
16
  def folder_graph_build(kv: StateKV) -> Dict[str, Any]:
 
9
 
10
  from typing import Any, Dict
11
 
12
+ from .. import functions as _fn
13
+ from ..db import StateKV
14
 
15
 
16
  def folder_graph_build(kv: StateKV) -> Dict[str, Any]:
src/{cache β†’ agentcache/cache}/health.py RENAMED
@@ -10,8 +10,8 @@ from __future__ import annotations
10
 
11
  from typing import Any, Dict
12
 
13
- from db import StateKV
14
- import functions as _fn
15
 
16
 
17
  def health_check(kv: StateKV) -> Dict[str, Any]:
 
10
 
11
  from typing import Any, Dict
12
 
13
+ from .. import functions as _fn
14
+ from ..db import StateKV
15
 
16
 
17
  def health_check(kv: StateKV) -> Dict[str, Any]:
src/{cache β†’ agentcache/cache}/observe.py RENAMED
@@ -12,9 +12,8 @@ from __future__ import annotations
12
 
13
  from typing import Any, Dict
14
 
15
- from db import StateKV
16
- import functions as _fn # access module-level globals (_bm25_index, etc.)
17
-
18
 
19
  # Re-export for backward compatibility
20
  strip_private_data = _fn.strip_private_data
 
12
 
13
  from typing import Any, Dict
14
 
15
+ from .. import functions as _fn # access module-level globals (_bm25_index, etc.)
16
+ from ..db import StateKV
 
17
 
18
  # Re-export for backward compatibility
19
  strip_private_data = _fn.strip_private_data
src/{cache β†’ agentcache/cache}/remember.py RENAMED
@@ -11,8 +11,8 @@ from __future__ import annotations
11
 
12
  from typing import Any, Dict
13
 
14
- from db import StateKV
15
- import functions as _fn
16
 
17
 
18
  def remember(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
 
11
 
12
  from typing import Any, Dict
13
 
14
+ from .. import functions as _fn
15
+ from ..db import StateKV
16
 
17
 
18
  def remember(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
src/{cache β†’ agentcache/cache}/timeline.py RENAMED
@@ -10,8 +10,8 @@ from __future__ import annotations
10
 
11
  from typing import Any, Dict, List, Optional
12
 
13
- from db import StateKV
14
- import functions as _fn
15
 
16
 
17
  def folder_timeline(
 
10
 
11
  from typing import Any, Dict, List, Optional
12
 
13
+ from .. import functions as _fn
14
+ from ..db import StateKV
15
 
16
 
17
  def folder_timeline(
src/{cli.py β†’ agentcache/cli.py} RENAMED
@@ -11,14 +11,12 @@ Commands:
11
  import argparse
12
  import json
13
  import os
14
- import sys
15
 
16
 
17
  def cmd_serve(args) -> None:
18
  """Start the Flask server."""
19
  os.environ.setdefault("III_REST_PORT", str(args.port))
20
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
- from app import create_app
22
 
23
  flask_app = create_app()
24
  print(f"[cli] Starting agentcache on {args.host}:{args.port}")
@@ -27,9 +25,8 @@ def cmd_serve(args) -> None:
27
 
28
  def cmd_migrate(args) -> None:
29
  """Run session β†’ folder migration."""
30
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
- from db import StateKV
32
- from functions import migrate_sessions_to_folders
33
 
34
  kv = StateKV()
35
  result = migrate_sessions_to_folders(kv, dry_run=args.dry_run)
@@ -54,9 +51,8 @@ def cmd_migrate(args) -> None:
54
 
55
  def cmd_export(args) -> None:
56
  """Export all data as JSON."""
57
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
58
- from db import StateKV
59
- from functions import export_data
60
 
61
  kv = StateKV()
62
  data = export_data(kv, {})
@@ -78,8 +74,7 @@ def cmd_export(args) -> None:
78
 
79
  def cmd_connect(args) -> None:
80
  """Connect/wire MCP and hooks to client agents."""
81
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
82
- from connect import run_connect
83
 
84
  run_connect(args)
85
 
 
11
  import argparse
12
  import json
13
  import os
 
14
 
15
 
16
  def cmd_serve(args) -> None:
17
  """Start the Flask server."""
18
  os.environ.setdefault("III_REST_PORT", str(args.port))
19
+ from .app import create_app
 
20
 
21
  flask_app = create_app()
22
  print(f"[cli] Starting agentcache on {args.host}:{args.port}")
 
25
 
26
  def cmd_migrate(args) -> None:
27
  """Run session β†’ folder migration."""
28
+ from .db import StateKV
29
+ from .functions import migrate_sessions_to_folders
 
30
 
31
  kv = StateKV()
32
  result = migrate_sessions_to_folders(kv, dry_run=args.dry_run)
 
51
 
52
  def cmd_export(args) -> None:
53
  """Export all data as JSON."""
54
+ from .db import StateKV
55
+ from .functions import export_data
 
56
 
57
  kv = StateKV()
58
  data = export_data(kv, {})
 
74
 
75
  def cmd_connect(args) -> None:
76
  """Connect/wire MCP and hooks to client agents."""
77
+ from .connect import run_connect
 
78
 
79
  run_connect(args)
80
 
src/{connect.py β†’ agentcache/connect.py} RENAMED
@@ -1,9 +1,9 @@
1
  #!/usr/bin/env python3
2
- import os
3
- import sys
4
  import json
 
5
  import shutil
6
- import argparse
7
 
8
  # Helper functions for connect module
9
 
@@ -45,9 +45,9 @@ def backup_file(path, prefix, ext="json"):
45
 
46
 
47
  def get_plugin_root():
48
- # connect.py resides in src/, plugin/ is in the parent directory
49
  src_dir = os.path.dirname(os.path.abspath(__file__))
50
- project_root = os.path.dirname(src_dir)
51
  plugin_path = os.path.join(project_root, "plugin")
52
  if os.path.exists(os.path.join(plugin_path, "scripts")):
53
  return plugin_path
@@ -368,14 +368,7 @@ class AntigravityAdapter:
368
  os.makedirs(gemini_mcp_dir, exist_ok=True)
369
  try:
370
  # Dynamic import to avoid circular dependency
371
- try:
372
- from routes.mcp import get_mcp_tools_schemas
373
- except ImportError:
374
- try:
375
- from src.routes.mcp import get_mcp_tools_schemas
376
- except ImportError:
377
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
378
- from routes.mcp import get_mcp_tools_schemas
379
 
380
  tools = get_mcp_tools_schemas()
381
  except Exception as e:
 
1
  #!/usr/bin/env python3
2
+ import argparse
 
3
  import json
4
+ import os
5
  import shutil
6
+ import sys
7
 
8
  # Helper functions for connect module
9
 
 
45
 
46
 
47
  def get_plugin_root():
48
+ # connect.py resides in src/agentcache/, plugin/ is in the parent of src/
49
  src_dir = os.path.dirname(os.path.abspath(__file__))
50
+ project_root = os.path.dirname(os.path.dirname(src_dir))
51
  plugin_path = os.path.join(project_root, "plugin")
52
  if os.path.exists(os.path.join(plugin_path, "scripts")):
53
  return plugin_path
 
368
  os.makedirs(gemini_mcp_dir, exist_ok=True)
369
  try:
370
  # Dynamic import to avoid circular dependency
371
+ from .routes.mcp import get_mcp_tools_schemas
 
 
 
 
 
 
 
372
 
373
  tools = get_mcp_tools_schemas()
374
  except Exception as e:
src/{db.py β†’ agentcache/db.py} RENAMED
@@ -1,10 +1,10 @@
1
- import os
2
  import json
 
3
  import sqlite3
4
  import threading
5
  import time
6
- import atexit
7
- from typing import Dict, Any, List, Optional, TypeVar
8
 
9
  T = TypeVar("T")
10
 
 
1
+ import atexit
2
  import json
3
+ import os
4
  import sqlite3
5
  import threading
6
  import time
7
+ from typing import Any, Dict, List, Optional, TypeVar
 
8
 
9
  T = TypeVar("T")
10
 
src/{functions.py β†’ agentcache/functions.py} RENAMED
@@ -1,14 +1,15 @@
 
 
 
1
  import os
2
  import re
 
3
  import time
4
  import uuid
5
- import json
6
- import hashlib
7
- import datetime
8
- import threading
9
- from typing import Dict, Any, List, Optional, Tuple, Set
10
- from db import StateKV
11
- from search import SearchIndex, VectorIndex, HybridSearch
12
 
13
  # =====================================================================
14
  # Global Variables / Module State
@@ -253,7 +254,9 @@ def auto_complete_old_active_sessions(
253
  ) -> int:
254
  sessions = kv.list(KV.sessions)
255
  count = 0
256
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
257
  for s in sessions:
258
  if s.get("id") != current_session_id and s.get("status") == "active":
259
  if project and s.get("project") != project:
@@ -337,7 +340,9 @@ def record_audit(
337
  ) -> Dict[str, Any]:
338
  entry = {
339
  "id": generate_id("aud"),
340
- "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
 
 
341
  "operation": operation,
342
  "userId": user_id,
343
  "functionId": function_id,
@@ -1009,7 +1014,9 @@ def observe(kv: StateKV, payload: Dict[str, Any]) -> Dict[str, Any]:
1009
  {
1010
  "type": "set",
1011
  "path": "updatedAt",
1012
- "value": datetime.datetime.utcnow().isoformat() + "Z",
 
 
1013
  },
1014
  {
1015
  "type": "set",
@@ -1035,7 +1042,11 @@ def observe(kv: StateKV, payload: Dict[str, Any]) -> Dict[str, Any]:
1035
  trimmed_prompt = None
1036
  if isinstance(raw.get("userPrompt"), str):
1037
  trimmed_prompt = " ".join(raw["userPrompt"].split()).strip()[:200]
1038
- ts = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
1039
  new_sess = {
1040
  "id": session_id,
1041
  "project": project,
@@ -1600,7 +1611,9 @@ def remember(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
1600
  if project:
1601
  project = project.strip()
1602
 
1603
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
1604
  existing_memories = kv.list(KV.memories)
1605
  superseded_id = None
1606
  superseded_version = 1
@@ -1645,8 +1658,10 @@ def remember(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
1645
  new_mem["project"] = project
1646
 
1647
  if ttl_days and isinstance(ttl_days, (int, float)) and ttl_days > 0:
1648
- forget_time = datetime.datetime.utcnow() + datetime.timedelta(days=ttl_days)
1649
- new_mem["forgetAfter"] = forget_time.isoformat() + "Z"
 
 
1650
 
1651
  if superseded_memory:
1652
  superseded_memory["isLatest"] = False
@@ -2164,7 +2179,9 @@ DEFAULT_SLOTS = [
2164
 
2165
 
2166
  def seed_defaults(kv: StateKV) -> None:
2167
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2168
  for tmpl in DEFAULT_SLOTS:
2169
  scope = tmpl["scope"]
2170
  target = KV.globalSlots if scope == "global" else KV.slots
@@ -2265,7 +2282,9 @@ def slot_create(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
2265
  if existing:
2266
  return {"success": False, "error": f"slot already exists in {scope} scope"}
2267
 
2268
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2269
  slot = {
2270
  "label": label,
2271
  "content": content,
@@ -2327,7 +2346,9 @@ def slot_append(
2327
  }
2328
 
2329
  slot["content"] = next_content
2330
- slot["updatedAt"] = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2331
  kv.set(target_kv, label, slot)
2332
 
2333
  safe_audit(
@@ -2375,7 +2396,9 @@ def slot_replace(
2375
 
2376
  before_len = len(slot.get("content") or "")
2377
  slot["content"] = content
2378
- slot["updatedAt"] = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2379
  kv.set(target_kv, label, slot)
2380
 
2381
  safe_audit(
@@ -2455,7 +2478,9 @@ def slot_reflect(kv: StateKV, session_id: str, max_obs: int = 50) -> Dict[str, A
2455
  files.add(f)
2456
 
2457
  applied = 0
2458
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2459
 
2460
  if pending_lines:
2461
  res = slot_get(kv, "pending_items", project)
@@ -2558,7 +2583,9 @@ def slot_reflect(kv: StateKV, session_id: str, max_obs: int = 50) -> Dict[str, A
2558
 
2559
 
2560
  def reinforce_lesson(lesson: Dict[str, Any]) -> None:
2561
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2562
  lesson["reinforcements"] = lesson.get("reinforcements", 0) + 1
2563
  conf = lesson.get("confidence", 0.5)
2564
  lesson["confidence"] = min(1.0, conf + 0.1 * (1 - conf))
@@ -2595,7 +2622,9 @@ def lesson_save(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
2595
  if not isinstance(confidence, (int, float)) or confidence < 0 or confidence > 1:
2596
  confidence = 0.5
2597
 
2598
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2599
  lesson = {
2600
  "id": fp,
2601
  "content": content.strip(),
@@ -2676,7 +2705,7 @@ def lesson_recall(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
2676
 
2677
  dt = dateutil.parser.parse(baseline)
2678
  days = (
2679
- datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
2680
  - dt.replace(tzinfo=datetime.timezone.utc)
2681
  ).total_seconds() / (3600 * 24)
2682
  recency_boost = 1 / (1 + days * 0.01)
@@ -2721,8 +2750,8 @@ def lesson_decay_sweep(kv: StateKV) -> Dict[str, Any]:
2721
  all_lessons = kv.list(KV.lessons)
2722
  decayed = 0
2723
  soft_deleted = 0
2724
- now = datetime.datetime.utcnow()
2725
- timestamp = now.isoformat() + "Z"
2726
 
2727
  for les in all_lessons:
2728
  if les.get("deleted"):
@@ -2733,10 +2762,9 @@ def lesson_decay_sweep(kv: StateKV) -> Dict[str, Any]:
2733
  import dateutil.parser
2734
 
2735
  dt = dateutil.parser.parse(baseline_str)
2736
- weeks = (
2737
- now.replace(tzinfo=datetime.timezone.utc)
2738
- - dt.replace(tzinfo=datetime.timezone.utc)
2739
- ).total_seconds() / (3600 * 24 * 7)
2740
  if weeks < 1.0:
2741
  continue
2742
 
@@ -2911,7 +2939,9 @@ def create_session(kv: StateKV, session: Dict[str, Any]) -> Dict[str, Any]:
2911
 
2912
 
2913
  def end_session(kv: StateKV, session_id: str) -> bool:
2914
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
2915
  kv.update(
2916
  KV.sessions,
2917
  session_id,
@@ -2972,7 +3002,9 @@ def get_project_profile(kv: StateKV, project: str) -> Dict[str, Any]:
2972
  "topFiles": [],
2973
  "conventions": [],
2974
  "commonErrors": [],
2975
- "updatedAt": datetime.datetime.utcnow().isoformat() + "Z",
 
 
2976
  }
2977
  if not prof.get("topConcepts") and not prof.get("topFiles"):
2978
  prof = build_project_profile(kv, project)
@@ -2988,14 +3020,16 @@ def build_project_profile(kv: StateKV, project: str) -> Dict[str, Any]:
2988
  "topFiles": [],
2989
  "conventions": [],
2990
  "commonErrors": [],
2991
- "updatedAt": datetime.datetime.utcnow().isoformat() + "Z",
 
 
2992
  }
2993
 
2994
  # Stored profile may lack topConcepts/topFiles β€” compute from observations + memories if empty
2995
  if not prof.get("topConcepts") and not prof.get("topFiles"):
2996
- import re as _re
2997
  import json as _j
2998
  import os.path as _osp
 
2999
  from collections import Counter
3000
 
3001
  sessions = kv.list(KV.sessions)
@@ -3087,7 +3121,9 @@ def export_data(kv: StateKV, data: Optional[Dict[str, Any]] = None) -> Dict[str,
3087
  if data is None:
3088
  data = {}
3089
 
3090
- exported_at = datetime.datetime.utcnow().isoformat() + "Z"
 
 
3091
 
3092
  # ---- v2 folder-based export (primary path) ----
3093
  folder_pairs = kv.list(KV.folders)
@@ -3226,7 +3262,9 @@ def migrate_sessions_to_folders(kv: StateKV, dry_run: bool = False) -> Dict[str,
3226
  def set_project_profile(
3227
  kv: StateKV, project: str, profile: Dict[str, Any]
3228
  ) -> Dict[str, Any]:
3229
- profile["updatedAt"] = datetime.datetime.utcnow().isoformat() + "Z"
 
 
3230
  kv.set(KV.profiles, project, profile)
3231
 
3232
  # Commit to Dolt
@@ -3245,7 +3283,9 @@ def add_relation(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3245
  "sourceId": data["sourceId"],
3246
  "targetId": data["targetId"],
3247
  "type": data["type"],
3248
- "createdAt": datetime.datetime.utcnow().isoformat() + "Z",
 
 
3249
  }
3250
  kv.set(KV.relations, rel["id"], rel)
3251
 
@@ -3273,7 +3313,9 @@ def evolve_memory(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3273
  existing["isLatest"] = False
3274
  kv.set(KV.memories, existing["id"], existing)
3275
 
3276
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
3277
  new_mem = dict(existing)
3278
  new_mem["id"] = generate_id("mem")
3279
  new_mem["content"] = new_content
@@ -3319,8 +3361,7 @@ def evolve_memory(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3319
 
3320
 
3321
  def auto_forget(kv: StateKV, dry_run: bool = False) -> Dict[str, Any]:
3322
- now_dt = datetime.datetime.utcnow()
3323
- now_dt.isoformat() + "Z"
3324
  evicted_memories = []
3325
  evicted_observations = []
3326
 
@@ -3601,7 +3642,7 @@ def summarize(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3601
  return {"success": False, "error": "no_observations"}
3602
 
3603
  SUMMARY_SYSTEM = """You are a session summarization assistant. Your job is to read all raw tool executions and outcomes from a coding session and produce a high-fidelity summary.
3604
-
3605
  Output XML:
3606
  <summary>
3607
  <title>Concise title summarizing the session</title>
@@ -3658,7 +3699,9 @@ def summarize(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3658
  final_summary = {
3659
  "sessionId": session_id,
3660
  "project": session.get("project"),
3661
- "createdAt": datetime.datetime.utcnow().isoformat() + "Z",
 
 
3662
  "title": partial_summaries[0]["title"],
3663
  "narrative": partial_summaries[0]["narrative"],
3664
  "keyDecisions": partial_summaries[0]["keyDecisions"],
@@ -3668,7 +3711,7 @@ def summarize(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3668
  }
3669
  else:
3670
  REDUCE_SYSTEM = """You are a session summarization reducer. Reduce multiple partial chunk summaries into a single final summary.
3671
-
3672
  Output XML:
3673
  <summary>
3674
  <title>Concise final title summarizing the entire session</title>
@@ -3694,7 +3737,9 @@ def summarize(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
3694
  final_summary = {
3695
  "sessionId": session_id,
3696
  "project": session.get("project"),
3697
- "createdAt": datetime.datetime.utcnow().isoformat() + "Z",
 
 
3698
  "title": get_xml_tag(cleaned, "title") or partial_summaries[0]["title"],
3699
  "narrative": get_xml_tag(cleaned, "narrative") or "",
3700
  "keyDecisions": get_xml_children(cleaned, "decisions", "decision"),
@@ -3772,7 +3817,7 @@ def consolidate(
3772
 
3773
  # Prompt templates
3774
  CONSOLIDATION_SYSTEM = """You are a memory consolidation engine. Given a set of related observations from coding sessions, synthesize them into a single long-term memory.
3775
-
3776
  Output XML:
3777
  <memory>
3778
  <type>pattern|preference|architecture|bug|workflow|fact</type>
@@ -3829,7 +3874,11 @@ def consolidate(
3829
  concepts_list = get_xml_children(cleaned, "concepts", "concept")
3830
  files_list = get_xml_children(cleaned, "files", "file")
3831
 
3832
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
3833
 
3834
  # Find existing memory with same title
3835
  existing_match = None
@@ -3934,12 +3983,12 @@ def consolidate(
3934
  )[:20]
3935
 
3936
  SEMANTIC_MERGE_SYSTEM = """You are a memory consolidation engine. Given overlapping episodic memories (session summaries), extract stable factual knowledge.
3937
-
3938
  Output format (XML):
3939
  <facts>
3940
  <fact confidence="0.0-1.0">Concise factual statement</fact>
3941
  </facts>
3942
-
3943
  Rules:
3944
  - Extract only facts that appear in 2+ episodes or are highly confident
3945
  - Confidence reflects how well-supported the fact is across episodes
@@ -3963,7 +4012,11 @@ def consolidate(
3963
  )
3964
 
3965
  existing_semantic = kv.list(KV.semantic)
3966
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
3967
 
3968
  for conf_str, fact_text in fact_matches:
3969
  fact_text = fact_text.strip()
@@ -4018,7 +4071,7 @@ def consolidate(
4018
 
4019
  if len(patterns) >= 2:
4020
  PROCEDURAL_EXTRACTION_SYSTEM = """You are a procedural memory extractor. Given repeated patterns and workflows observed across sessions, extract reusable procedures.
4021
-
4022
  Output format (XML):
4023
  <procedures>
4024
  <procedure name="short descriptive name" trigger="when to use this procedure">
@@ -4026,7 +4079,7 @@ def consolidate(
4026
  <step>Step 2 description</step>
4027
  </procedure>
4028
  </procedures>
4029
-
4030
  Rules:
4031
  - Only extract procedures observed 2+ times
4032
  - Steps should be concrete and actionable
@@ -4051,7 +4104,11 @@ def consolidate(
4051
  )
4052
 
4053
  existing_procs = kv.list(KV.procedural)
4054
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
4055
 
4056
  for name, trigger, steps_block in proc_matches:
4057
  steps = [
 
1
+ import datetime
2
+ import hashlib
3
+ import json
4
  import os
5
  import re
6
+ import threading
7
  import time
8
  import uuid
9
+ from typing import Any, Dict, List, Optional, Set, Tuple
10
+
11
+ from .db import StateKV
12
+ from .search import HybridSearch, SearchIndex, VectorIndex
 
 
 
13
 
14
  # =====================================================================
15
  # Global Variables / Module State
 
254
  ) -> int:
255
  sessions = kv.list(KV.sessions)
256
  count = 0
257
+ now = (
258
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
259
+ )
260
  for s in sessions:
261
  if s.get("id") != current_session_id and s.get("status") == "active":
262
  if project and s.get("project") != project:
 
340
  ) -> Dict[str, Any]:
341
  entry = {
342
  "id": generate_id("aud"),
343
+ "timestamp": datetime.datetime.now(datetime.timezone.utc)
344
+ .isoformat()
345
+ .replace("+00:00", "Z"),
346
  "operation": operation,
347
  "userId": user_id,
348
  "functionId": function_id,
 
1014
  {
1015
  "type": "set",
1016
  "path": "updatedAt",
1017
+ "value": datetime.datetime.now(datetime.timezone.utc)
1018
+ .isoformat()
1019
+ .replace("+00:00", "Z"),
1020
  },
1021
  {
1022
  "type": "set",
 
1042
  trimmed_prompt = None
1043
  if isinstance(raw.get("userPrompt"), str):
1044
  trimmed_prompt = " ".join(raw["userPrompt"].split()).strip()[:200]
1045
+ ts = (
1046
+ datetime.datetime.now(datetime.timezone.utc)
1047
+ .isoformat()
1048
+ .replace("+00:00", "Z")
1049
+ )
1050
  new_sess = {
1051
  "id": session_id,
1052
  "project": project,
 
1611
  if project:
1612
  project = project.strip()
1613
 
1614
+ now = (
1615
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
1616
+ )
1617
  existing_memories = kv.list(KV.memories)
1618
  superseded_id = None
1619
  superseded_version = 1
 
1658
  new_mem["project"] = project
1659
 
1660
  if ttl_days and isinstance(ttl_days, (int, float)) and ttl_days > 0:
1661
+ forget_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
1662
+ days=ttl_days
1663
+ )
1664
+ new_mem["forgetAfter"] = forget_time.isoformat().replace("+00:00", "Z")
1665
 
1666
  if superseded_memory:
1667
  superseded_memory["isLatest"] = False
 
2179
 
2180
 
2181
  def seed_defaults(kv: StateKV) -> None:
2182
+ now = (
2183
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2184
+ )
2185
  for tmpl in DEFAULT_SLOTS:
2186
  scope = tmpl["scope"]
2187
  target = KV.globalSlots if scope == "global" else KV.slots
 
2282
  if existing:
2283
  return {"success": False, "error": f"slot already exists in {scope} scope"}
2284
 
2285
+ now = (
2286
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2287
+ )
2288
  slot = {
2289
  "label": label,
2290
  "content": content,
 
2346
  }
2347
 
2348
  slot["content"] = next_content
2349
+ slot["updatedAt"] = (
2350
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2351
+ )
2352
  kv.set(target_kv, label, slot)
2353
 
2354
  safe_audit(
 
2396
 
2397
  before_len = len(slot.get("content") or "")
2398
  slot["content"] = content
2399
+ slot["updatedAt"] = (
2400
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2401
+ )
2402
  kv.set(target_kv, label, slot)
2403
 
2404
  safe_audit(
 
2478
  files.add(f)
2479
 
2480
  applied = 0
2481
+ now = (
2482
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2483
+ )
2484
 
2485
  if pending_lines:
2486
  res = slot_get(kv, "pending_items", project)
 
2583
 
2584
 
2585
  def reinforce_lesson(lesson: Dict[str, Any]) -> None:
2586
+ now = (
2587
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2588
+ )
2589
  lesson["reinforcements"] = lesson.get("reinforcements", 0) + 1
2590
  conf = lesson.get("confidence", 0.5)
2591
  lesson["confidence"] = min(1.0, conf + 0.1 * (1 - conf))
 
2622
  if not isinstance(confidence, (int, float)) or confidence < 0 or confidence > 1:
2623
  confidence = 0.5
2624
 
2625
+ now = (
2626
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2627
+ )
2628
  lesson = {
2629
  "id": fp,
2630
  "content": content.strip(),
 
2705
 
2706
  dt = dateutil.parser.parse(baseline)
2707
  days = (
2708
+ datetime.datetime.now(datetime.timezone.utc)
2709
  - dt.replace(tzinfo=datetime.timezone.utc)
2710
  ).total_seconds() / (3600 * 24)
2711
  recency_boost = 1 / (1 + days * 0.01)
 
2750
  all_lessons = kv.list(KV.lessons)
2751
  decayed = 0
2752
  soft_deleted = 0
2753
+ now = datetime.datetime.now(datetime.timezone.utc)
2754
+ timestamp = now.isoformat().replace("+00:00", "Z")
2755
 
2756
  for les in all_lessons:
2757
  if les.get("deleted"):
 
2762
  import dateutil.parser
2763
 
2764
  dt = dateutil.parser.parse(baseline_str)
2765
+ weeks = (now - dt.replace(tzinfo=datetime.timezone.utc)).total_seconds() / (
2766
+ 3600 * 24 * 7
2767
+ )
 
2768
  if weeks < 1.0:
2769
  continue
2770
 
 
2939
 
2940
 
2941
  def end_session(kv: StateKV, session_id: str) -> bool:
2942
+ now = (
2943
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
2944
+ )
2945
  kv.update(
2946
  KV.sessions,
2947
  session_id,
 
3002
  "topFiles": [],
3003
  "conventions": [],
3004
  "commonErrors": [],
3005
+ "updatedAt": datetime.datetime.now(datetime.timezone.utc)
3006
+ .isoformat()
3007
+ .replace("+00:00", "Z"),
3008
  }
3009
  if not prof.get("topConcepts") and not prof.get("topFiles"):
3010
  prof = build_project_profile(kv, project)
 
3020
  "topFiles": [],
3021
  "conventions": [],
3022
  "commonErrors": [],
3023
+ "updatedAt": datetime.datetime.now(datetime.timezone.utc)
3024
+ .isoformat()
3025
+ .replace("+00:00", "Z"),
3026
  }
3027
 
3028
  # Stored profile may lack topConcepts/topFiles β€” compute from observations + memories if empty
3029
  if not prof.get("topConcepts") and not prof.get("topFiles"):
 
3030
  import json as _j
3031
  import os.path as _osp
3032
+ import re as _re
3033
  from collections import Counter
3034
 
3035
  sessions = kv.list(KV.sessions)
 
3121
  if data is None:
3122
  data = {}
3123
 
3124
+ exported_at = (
3125
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
3126
+ )
3127
 
3128
  # ---- v2 folder-based export (primary path) ----
3129
  folder_pairs = kv.list(KV.folders)
 
3262
  def set_project_profile(
3263
  kv: StateKV, project: str, profile: Dict[str, Any]
3264
  ) -> Dict[str, Any]:
3265
+ profile["updatedAt"] = (
3266
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
3267
+ )
3268
  kv.set(KV.profiles, project, profile)
3269
 
3270
  # Commit to Dolt
 
3283
  "sourceId": data["sourceId"],
3284
  "targetId": data["targetId"],
3285
  "type": data["type"],
3286
+ "createdAt": datetime.datetime.now(datetime.timezone.utc)
3287
+ .isoformat()
3288
+ .replace("+00:00", "Z"),
3289
  }
3290
  kv.set(KV.relations, rel["id"], rel)
3291
 
 
3313
  existing["isLatest"] = False
3314
  kv.set(KV.memories, existing["id"], existing)
3315
 
3316
+ now = (
3317
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
3318
+ )
3319
  new_mem = dict(existing)
3320
  new_mem["id"] = generate_id("mem")
3321
  new_mem["content"] = new_content
 
3361
 
3362
 
3363
  def auto_forget(kv: StateKV, dry_run: bool = False) -> Dict[str, Any]:
3364
+ now_dt = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
 
3365
  evicted_memories = []
3366
  evicted_observations = []
3367
 
 
3642
  return {"success": False, "error": "no_observations"}
3643
 
3644
  SUMMARY_SYSTEM = """You are a session summarization assistant. Your job is to read all raw tool executions and outcomes from a coding session and produce a high-fidelity summary.
3645
+
3646
  Output XML:
3647
  <summary>
3648
  <title>Concise title summarizing the session</title>
 
3699
  final_summary = {
3700
  "sessionId": session_id,
3701
  "project": session.get("project"),
3702
+ "createdAt": datetime.datetime.now(datetime.timezone.utc)
3703
+ .isoformat()
3704
+ .replace("+00:00", "Z"),
3705
  "title": partial_summaries[0]["title"],
3706
  "narrative": partial_summaries[0]["narrative"],
3707
  "keyDecisions": partial_summaries[0]["keyDecisions"],
 
3711
  }
3712
  else:
3713
  REDUCE_SYSTEM = """You are a session summarization reducer. Reduce multiple partial chunk summaries into a single final summary.
3714
+
3715
  Output XML:
3716
  <summary>
3717
  <title>Concise final title summarizing the entire session</title>
 
3737
  final_summary = {
3738
  "sessionId": session_id,
3739
  "project": session.get("project"),
3740
+ "createdAt": datetime.datetime.now(datetime.timezone.utc)
3741
+ .isoformat()
3742
+ .replace("+00:00", "Z"),
3743
  "title": get_xml_tag(cleaned, "title") or partial_summaries[0]["title"],
3744
  "narrative": get_xml_tag(cleaned, "narrative") or "",
3745
  "keyDecisions": get_xml_children(cleaned, "decisions", "decision"),
 
3817
 
3818
  # Prompt templates
3819
  CONSOLIDATION_SYSTEM = """You are a memory consolidation engine. Given a set of related observations from coding sessions, synthesize them into a single long-term memory.
3820
+
3821
  Output XML:
3822
  <memory>
3823
  <type>pattern|preference|architecture|bug|workflow|fact</type>
 
3874
  concepts_list = get_xml_children(cleaned, "concepts", "concept")
3875
  files_list = get_xml_children(cleaned, "files", "file")
3876
 
3877
+ now = (
3878
+ datetime.datetime.now(datetime.timezone.utc)
3879
+ .isoformat()
3880
+ .replace("+00:00", "Z")
3881
+ )
3882
 
3883
  # Find existing memory with same title
3884
  existing_match = None
 
3983
  )[:20]
3984
 
3985
  SEMANTIC_MERGE_SYSTEM = """You are a memory consolidation engine. Given overlapping episodic memories (session summaries), extract stable factual knowledge.
3986
+
3987
  Output format (XML):
3988
  <facts>
3989
  <fact confidence="0.0-1.0">Concise factual statement</fact>
3990
  </facts>
3991
+
3992
  Rules:
3993
  - Extract only facts that appear in 2+ episodes or are highly confident
3994
  - Confidence reflects how well-supported the fact is across episodes
 
4012
  )
4013
 
4014
  existing_semantic = kv.list(KV.semantic)
4015
+ now = (
4016
+ datetime.datetime.now(datetime.timezone.utc)
4017
+ .isoformat()
4018
+ .replace("+00:00", "Z")
4019
+ )
4020
 
4021
  for conf_str, fact_text in fact_matches:
4022
  fact_text = fact_text.strip()
 
4071
 
4072
  if len(patterns) >= 2:
4073
  PROCEDURAL_EXTRACTION_SYSTEM = """You are a procedural memory extractor. Given repeated patterns and workflows observed across sessions, extract reusable procedures.
4074
+
4075
  Output format (XML):
4076
  <procedures>
4077
  <procedure name="short descriptive name" trigger="when to use this procedure">
 
4079
  <step>Step 2 description</step>
4080
  </procedure>
4081
  </procedures>
4082
+
4083
  Rules:
4084
  - Only extract procedures observed 2+ times
4085
  - Steps should be concrete and actionable
 
4104
  )
4105
 
4106
  existing_procs = kv.list(KV.procedural)
4107
+ now = (
4108
+ datetime.datetime.now(datetime.timezone.utc)
4109
+ .isoformat()
4110
+ .replace("+00:00", "Z")
4111
+ )
4112
 
4113
  for name, trigger, steps_block in proc_matches:
4114
  steps = [
src/{import_data.py β†’ agentcache/import_data.py} RENAMED
@@ -1,11 +1,9 @@
1
- import os
2
  import json
3
- import urllib.parse
4
  import sys
 
5
 
6
- # Append local path so we can import StateKV
7
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
- from db import StateKV
9
 
10
 
11
  def import_old_data(old_db_path: str, kv: StateKV) -> bool:
 
 
1
  import json
2
+ import os
3
  import sys
4
+ import urllib.parse
5
 
6
+ from .db import StateKV
 
 
7
 
8
 
9
  def import_old_data(old_db_path: str, kv: StateKV) -> bool:
src/{mcp_stdio.py β†’ agentcache/mcp_stdio.py} RENAMED
@@ -9,9 +9,10 @@ Antigravity sync functions have been moved to examples/antigravity_sync.py (D1.3
9
  All tool calls are proxied to the HTTP API.
10
  """
11
 
12
- import sys
13
  import json
14
  import os
 
 
15
  import requests
16
 
17
  BASE = (
 
9
  All tool calls are proxied to the HTTP API.
10
  """
11
 
 
12
  import json
13
  import os
14
+ import sys
15
+
16
  import requests
17
 
18
  BASE = (
src/{replay_import.py β†’ agentcache/replay_import.py} RENAMED
@@ -1,9 +1,9 @@
 
 
 
1
  import os
2
  import re
3
- import json
4
- import hashlib
5
- import datetime
6
- from typing import List, Dict, Any, Tuple
7
 
8
  # Constants
9
  MAX_FILES_DEFAULT = 200
@@ -124,7 +124,9 @@ def parse_jsonl_text(text: str, fallback_session_id: str = None) -> Dict[str, An
124
  if entry.get("cwd") and not cwd:
125
  cwd = entry["cwd"]
126
 
127
- ts = entry.get("timestamp") or datetime.datetime.utcnow().isoformat() + "Z"
 
 
128
  if not first_ts:
129
  first_ts = ts
130
  last_ts = ts
@@ -196,7 +198,9 @@ def parse_jsonl_text(text: str, fallback_session_id: str = None) -> Dict[str, An
196
  if obs["sessionId"] == "imported":
197
  obs["sessionId"] = effective_session_id
198
 
199
- now_iso = datetime.datetime.utcnow().isoformat() + "Z"
 
 
200
  return {
201
  "sessionId": effective_session_id,
202
  "project": derive_project(cwd),
@@ -215,11 +219,13 @@ def derive_crystal_and_lessons(
215
  compressed: List[Dict[str, Any]],
216
  first_prompt: str = None,
217
  ) -> None:
218
- from functions import KV
219
 
220
  if not raw_obs:
221
  return
222
- created_at = datetime.datetime.utcnow().isoformat() + "Z"
 
 
223
 
224
  files = set()
225
  tools = set()
@@ -362,7 +368,7 @@ def find_jsonl_files(root: str, limit=200) -> Tuple[List[str], bool, int, bool]:
362
 
363
 
364
  def import_jsonl_data(kv, path: str = None, max_files: int = None) -> Dict[str, Any]:
365
- from functions import KV, build_synthetic_compression, _bm25_index
366
 
367
  default_root = os.path.expanduser(os.path.join("~", ".claude", "projects"))
368
  raw_path = path or default_root
@@ -475,7 +481,7 @@ def import_jsonl_data(kv, path: str = None, max_files: int = None) -> Dict[str,
475
  }
476
  kv.set(KV.sessions, session["id"], session)
477
 
478
- from functions import vector_index_add_guarded
479
 
480
  compressed = []
481
  for obs in parsed["observations"]:
@@ -506,7 +512,7 @@ def import_jsonl_data(kv, path: str = None, max_files: int = None) -> Dict[str,
506
  )
507
 
508
  # Save the updated persistence state
509
- import functions
510
 
511
  if functions._index_persistence:
512
  try:
@@ -516,7 +522,7 @@ def import_jsonl_data(kv, path: str = None, max_files: int = None) -> Dict[str,
516
 
517
  # Audit trail
518
  try:
519
- from functions import log_audit
520
 
521
  log_audit(
522
  kv,
@@ -613,7 +619,11 @@ def estimate_duration_ms(event: Dict[str, Any]) -> int:
613
 
614
  def project_timeline(observations: List[Dict[str, Any]]) -> Dict[str, Any]:
615
  if not observations:
616
- now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
617
  return {
618
  "sessionId": "",
619
  "startedAt": now,
 
1
+ import datetime
2
+ import hashlib
3
+ import json
4
  import os
5
  import re
6
+ from typing import Any, Dict, List, Tuple
 
 
 
7
 
8
  # Constants
9
  MAX_FILES_DEFAULT = 200
 
124
  if entry.get("cwd") and not cwd:
125
  cwd = entry["cwd"]
126
 
127
+ ts = entry.get("timestamp") or datetime.datetime.now(
128
+ datetime.timezone.utc
129
+ ).isoformat().replace("+00:00", "Z")
130
  if not first_ts:
131
  first_ts = ts
132
  last_ts = ts
 
198
  if obs["sessionId"] == "imported":
199
  obs["sessionId"] = effective_session_id
200
 
201
+ now_iso = (
202
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
203
+ )
204
  return {
205
  "sessionId": effective_session_id,
206
  "project": derive_project(cwd),
 
219
  compressed: List[Dict[str, Any]],
220
  first_prompt: str = None,
221
  ) -> None:
222
+ from .functions import KV
223
 
224
  if not raw_obs:
225
  return
226
+ created_at = (
227
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
228
+ )
229
 
230
  files = set()
231
  tools = set()
 
368
 
369
 
370
  def import_jsonl_data(kv, path: str = None, max_files: int = None) -> Dict[str, Any]:
371
+ from .functions import KV, _bm25_index, build_synthetic_compression
372
 
373
  default_root = os.path.expanduser(os.path.join("~", ".claude", "projects"))
374
  raw_path = path or default_root
 
481
  }
482
  kv.set(KV.sessions, session["id"], session)
483
 
484
+ from .functions import vector_index_add_guarded
485
 
486
  compressed = []
487
  for obs in parsed["observations"]:
 
512
  )
513
 
514
  # Save the updated persistence state
515
+ from . import functions
516
 
517
  if functions._index_persistence:
518
  try:
 
522
 
523
  # Audit trail
524
  try:
525
+ from .functions import log_audit
526
 
527
  log_audit(
528
  kv,
 
619
 
620
  def project_timeline(observations: List[Dict[str, Any]]) -> Dict[str, Any]:
621
  if not observations:
622
+ now = (
623
+ datetime.datetime.now(datetime.timezone.utc)
624
+ .isoformat()
625
+ .replace("+00:00", "Z")
626
+ )
627
  return {
628
  "sessionId": "",
629
  "startedAt": now,
src/{routes β†’ agentcache/routes}/__init__.py RENAMED
@@ -4,13 +4,13 @@ Flask blueprints for agentmemory-python.
4
  Import and register all blueprints via register_blueprints(app).
5
  """
6
 
7
- from .observations import observations_bp
8
- from .memories import memories_bp
9
- from .search import search_bp
10
  from .graph import graph_bp
11
  from .health import health_bp
12
  from .mcp import mcp_bp
 
13
  from .migration import migration_bp
 
 
14
 
15
 
16
  def register_blueprints(app):
 
4
  Import and register all blueprints via register_blueprints(app).
5
  """
6
 
 
 
 
7
  from .graph import graph_bp
8
  from .health import health_bp
9
  from .mcp import mcp_bp
10
+ from .memories import memories_bp
11
  from .migration import migration_bp
12
+ from .observations import observations_bp
13
+ from .search import search_bp
14
 
15
 
16
  def register_blueprints(app):
src/{routes β†’ agentcache/routes}/graph.py RENAMED
@@ -9,8 +9,10 @@ Handles:
9
  """
10
 
11
  import os
12
- from flask import Blueprint, request, jsonify
13
- import functions
 
 
14
 
15
  graph_bp = Blueprint("graph", __name__)
16
 
@@ -31,7 +33,7 @@ def _check_auth():
31
 
32
 
33
  def _get_kv():
34
- import app as app_module
35
 
36
  return app_module.kv
37
 
 
9
  """
10
 
11
  import os
12
+
13
+ from flask import Blueprint, jsonify, request
14
+
15
+ from .. import functions
16
 
17
  graph_bp = Blueprint("graph", __name__)
18
 
 
33
 
34
 
35
  def _get_kv():
36
+ from .. import app as app_module
37
 
38
  return app_module.kv
39
 
src/{routes β†’ agentcache/routes}/health.py RENAMED
@@ -9,9 +9,11 @@ Handles:
9
  """
10
 
11
  import os
12
- from flask import Blueprint, request, jsonify, Response
13
- import functions
14
- from functions import query_audit
 
 
15
 
16
  health_bp = Blueprint("health", __name__)
17
 
@@ -32,13 +34,13 @@ def _check_auth():
32
 
33
 
34
  def _get_kv():
35
- import app as app_module
36
 
37
  return app_module.kv
38
 
39
 
40
  def _get_embedding_provider():
41
- import app as app_module
42
 
43
  return app_module.embedding_provider
44
 
 
9
  """
10
 
11
  import os
12
+
13
+ from flask import Blueprint, Response, jsonify, request
14
+
15
+ from .. import functions
16
+ from ..functions import query_audit
17
 
18
  health_bp = Blueprint("health", __name__)
19
 
 
34
 
35
 
36
  def _get_kv():
37
+ from .. import app as app_module
38
 
39
  return app_module.kv
40
 
41
 
42
  def _get_embedding_provider():
43
+ from .. import app as app_module
44
 
45
  return app_module.embedding_provider
46
 
src/{routes β†’ agentcache/routes}/mcp.py RENAMED
@@ -6,12 +6,14 @@ Handles:
6
  POST /agentmemory/mcp/tools β€” dispatch a tool call
7
  """
8
 
9
- import os
10
- import json
11
  import datetime
12
- from flask import Blueprint, request, jsonify
13
- import functions
14
- from functions import KV
 
 
 
 
15
 
16
  mcp_bp = Blueprint("mcp", __name__)
17
 
@@ -32,13 +34,15 @@ def _check_auth():
32
 
33
 
34
  def _get_kv():
35
- import app as app_module
36
 
37
  return app_module.kv
38
 
39
 
40
  def _datetime_now_iso() -> str:
41
- return datetime.datetime.utcnow().isoformat() + "Z"
 
 
42
 
43
 
44
  def _parse_mcp_list_arg(arg_val):
 
6
  POST /agentmemory/mcp/tools β€” dispatch a tool call
7
  """
8
 
 
 
9
  import datetime
10
+ import json
11
+ import os
12
+
13
+ from flask import Blueprint, jsonify, request
14
+
15
+ from .. import functions
16
+ from ..functions import KV
17
 
18
  mcp_bp = Blueprint("mcp", __name__)
19
 
 
34
 
35
 
36
  def _get_kv():
37
+ from .. import app as app_module
38
 
39
  return app_module.kv
40
 
41
 
42
  def _datetime_now_iso() -> str:
43
+ return (
44
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
45
+ )
46
 
47
 
48
  def _parse_mcp_list_arg(arg_val):
src/{routes β†’ agentcache/routes}/memories.py RENAMED
@@ -9,9 +9,11 @@ Handles:
9
  """
10
 
11
  import os
12
- from flask import Blueprint, request, jsonify
13
- import functions
14
- from functions import KV
 
 
15
 
16
  memories_bp = Blueprint("memories", __name__)
17
 
@@ -32,7 +34,7 @@ def _check_auth():
32
 
33
 
34
  def _get_kv():
35
- import app as app_module
36
 
37
  return app_module.kv
38
 
 
9
  """
10
 
11
  import os
12
+
13
+ from flask import Blueprint, jsonify, request
14
+
15
+ from .. import functions
16
+ from ..functions import KV
17
 
18
  memories_bp = Blueprint("memories", __name__)
19
 
 
34
 
35
 
36
  def _get_kv():
37
+ from .. import app as app_module
38
 
39
  return app_module.kv
40
 
src/{routes β†’ agentcache/routes}/migration.py RENAMED
@@ -6,8 +6,10 @@ Handles:
6
  """
7
 
8
  import os
9
- from flask import Blueprint, request, jsonify
10
- import functions
 
 
11
 
12
  migration_bp = Blueprint("migration", __name__)
13
 
@@ -28,7 +30,7 @@ def _check_auth():
28
 
29
 
30
  def _get_kv():
31
- import app as app_module
32
 
33
  return app_module.kv
34
 
 
6
  """
7
 
8
  import os
9
+
10
+ from flask import Blueprint, jsonify, request
11
+
12
+ from .. import functions
13
 
14
  migration_bp = Blueprint("migration", __name__)
15
 
 
30
 
31
 
32
  def _get_kv():
33
+ from .. import app as app_module
34
 
35
  return app_module.kv
36
 
src/{routes β†’ agentcache/routes}/observations.py RENAMED
@@ -8,17 +8,21 @@ Handles:
8
  GET /agentmemory/folders
9
  """
10
 
11
- import os
12
  import datetime
13
- from flask import Blueprint, request, jsonify
14
- import functions
15
- from functions import KV
 
 
 
16
 
17
  observations_bp = Blueprint("observations", __name__)
18
 
19
 
20
  def _datetime_now_iso() -> str:
21
- return datetime.datetime.utcnow().isoformat() + "Z"
 
 
22
 
23
 
24
  def _check_auth():
@@ -39,7 +43,7 @@ def _check_auth():
39
 
40
  def _get_kv():
41
  """Retrieve the shared kv instance from the app module."""
42
- import app as app_module
43
 
44
  return app_module.kv
45
 
 
8
  GET /agentmemory/folders
9
  """
10
 
 
11
  import datetime
12
+ import os
13
+
14
+ from flask import Blueprint, jsonify, request
15
+
16
+ from .. import functions
17
+ from ..functions import KV
18
 
19
  observations_bp = Blueprint("observations", __name__)
20
 
21
 
22
  def _datetime_now_iso() -> str:
23
+ return (
24
+ datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
25
+ )
26
 
27
 
28
  def _check_auth():
 
43
 
44
  def _get_kv():
45
  """Retrieve the shared kv instance from the app module."""
46
+ from .. import app as app_module
47
 
48
  return app_module.kv
49
 
src/{routes β†’ agentcache/routes}/search.py RENAMED
@@ -7,8 +7,10 @@ Handles:
7
  """
8
 
9
  import os
10
- from flask import Blueprint, request, jsonify
11
- import functions
 
 
12
 
13
  search_bp = Blueprint("search", __name__)
14
 
@@ -29,7 +31,7 @@ def _check_auth():
29
 
30
 
31
  def _get_kv():
32
- import app as app_module
33
 
34
  return app_module.kv
35
 
 
7
  """
8
 
9
  import os
10
+
11
+ from flask import Blueprint, jsonify, request
12
+
13
+ from .. import functions
14
 
15
  search_bp = Blueprint("search", __name__)
16
 
 
31
 
32
 
33
  def _get_kv():
34
+ from .. import app as app_module
35
 
36
  return app_module.kv
37
 
src/{search.py β†’ agentcache/search.py} RENAMED
@@ -1,11 +1,11 @@
1
- import re
2
- import math
3
- import json
4
- import base64
5
  import array
6
- import urllib.request
 
 
 
7
  import urllib.parse
8
- from typing import Dict, Any, List, Optional, Tuple, Set
 
9
 
10
  # =====================================================================
11
  # Custom Porter-like Stemmer (Ported from stemmer.ts)
 
 
 
 
 
1
  import array
2
+ import base64
3
+ import json
4
+ import math
5
+ import re
6
  import urllib.parse
7
+ import urllib.request
8
+ from typing import Any, Dict, List, Optional, Set, Tuple
9
 
10
  # =====================================================================
11
  # Custom Porter-like Stemmer (Ported from stemmer.ts)
src/{storage β†’ agentcache/storage}/__init__.py RENAMED
@@ -12,9 +12,9 @@ These are compatibility copies β€” the originals in functions.py are kept intact
12
  for backward compatibility.
13
  """
14
 
 
 
15
  from .scopes import KV
16
- from .paths import normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
17
- from .images import save_image_to_disk, delete_image, touch_image, is_managed_image_path
18
 
19
  __all__ = [
20
  "KV",
 
12
  for backward compatibility.
13
  """
14
 
15
+ from .images import delete_image, is_managed_image_path, save_image_to_disk, touch_image
16
+ from .paths import fingerprint_id, generate_id, normalize_folder_path, validate_agent_id
17
  from .scopes import KV
 
 
18
 
19
  __all__ = [
20
  "KV",
src/{storage β†’ agentcache/storage}/images.py RENAMED
@@ -4,8 +4,8 @@ src/storage/images.py β€” Image-on-disk storage helpers (A2.3).
4
  Copied from src/functions.py β€” do NOT delete the originals (backward compat).
5
  """
6
 
7
- import os
8
  import hashlib
 
9
  from typing import Optional, Tuple
10
 
11
  IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentmemory", "images")
 
4
  Copied from src/functions.py β€” do NOT delete the originals (backward compat).
5
  """
6
 
 
7
  import hashlib
8
+ import os
9
  from typing import Optional, Tuple
10
 
11
  IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentmemory", "images")
src/{storage β†’ agentcache/storage}/paths.py RENAMED
@@ -4,10 +4,10 @@ src/storage/paths.py β€” Path normalisation and ID utilities (A2.3).
4
  Copied from src/functions.py β€” do NOT delete the originals (backward compat).
5
  """
6
 
 
7
  import os
8
  import time
9
  import uuid
10
- import hashlib
11
 
12
  # Maximum allowed length for folder paths and agent IDs.
13
  _MAX_PATH_LEN = 512
 
4
  Copied from src/functions.py β€” do NOT delete the originals (backward compat).
5
  """
6
 
7
+ import hashlib
8
  import os
9
  import time
10
  import uuid
 
11
 
12
  # Maximum allowed length for folder paths and agent IDs.
13
  _MAX_PATH_LEN = 512
src/{storage β†’ agentcache/storage}/scopes.py RENAMED
File without changes
src/{viewer β†’ agentcache/viewer}/favicon.svg RENAMED
File without changes