adk-mimir-memory 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adk_mimir_memory-0.2.0/.github/workflows/publish.yml +46 -0
- adk_mimir_memory-0.2.0/.gitignore +6 -0
- adk_mimir_memory-0.2.0/LICENSE +21 -0
- adk_mimir_memory-0.2.0/PKG-INFO +143 -0
- adk_mimir_memory-0.2.0/README.md +118 -0
- adk_mimir_memory-0.2.0/adk_mimir_memory/__init__.py +16 -0
- adk_mimir_memory-0.2.0/adk_mimir_memory/mimir_memory_service.py +445 -0
- adk_mimir_memory-0.2.0/adk_mimir_memory/perseus_context.py +97 -0
- adk_mimir_memory-0.2.0/pyproject.toml +36 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: '3.12'
|
|
18
|
+
|
|
19
|
+
- name: Install build
|
|
20
|
+
run: pip install build==1.2.2.post1
|
|
21
|
+
|
|
22
|
+
- name: Build
|
|
23
|
+
run: python -m build
|
|
24
|
+
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment:
|
|
34
|
+
name: pypi
|
|
35
|
+
url: https://pypi.org/p/adk-mimir-memory
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- name: Publish to PyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
46
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Perseus Computing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adk-mimir-memory
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Persistent, local, encrypted cross-session memory for Google ADK agents — backed by Mimir.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Perseus-Computing-LLC/adk-mimir-memory
|
|
6
|
+
Project-URL: Repository, https://github.com/Perseus-Computing-LLC/adk-mimir-memory
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/Perseus-Computing-LLC/adk-mimir-memory/issues
|
|
8
|
+
Author-email: Thomas Connally <51974392+tcconnally@users.noreply.github.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: google-adk>=1.0.0
|
|
22
|
+
Provides-Extra: perseus
|
|
23
|
+
Requires-Dist: perseus-ctx; extra == 'perseus'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# ADK Mimir Memory
|
|
27
|
+
|
|
28
|
+
Persistent, local, encrypted cross-session memory for [Google ADK](https://github.com/google/adk-python) agents — backed by [Mimir](https://github.com/Perseus-Computing-LLC/mimir).
|
|
29
|
+
|
|
30
|
+
## Why Mimir?
|
|
31
|
+
|
|
32
|
+
| Backend | Dependencies | Encryption | Hybrid Search | Local |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| **InMemoryMemoryService** | None | ❌ | ❌ | ✅ |
|
|
35
|
+
| **VertexAiMemoryBankService** | GCP + Gemini | ❌ | Gemini-driven | ❌ |
|
|
36
|
+
| **VertexAiRagMemoryService** | GCP + RAG | ❌ | GCP vector | ❌ |
|
|
37
|
+
| **MimirMemoryService** | **Single binary** | **✅ AES-256** | **✅ BM25+FTS5+Dense** | **✅** |
|
|
38
|
+
|
|
39
|
+
- **Zero cloud dependencies** — a single Rust binary, SQLite database, fully local
|
|
40
|
+
- **AES-256-GCM encryption** at rest — your memory data stays private
|
|
41
|
+
- **Hybrid search** — BM25 keyword + FTS5 + dense vector search
|
|
42
|
+
- **30+ MCP tools** — remember, recall, synthesize, benchmark, federate, and more
|
|
43
|
+
- **Ebbinghaus confidence decay** — memories fade naturally, important ones persist
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install adk-mimir-memory
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This package requires the `mimir` binary. Download it from:
|
|
52
|
+
https://github.com/Perseus-Computing-LLC/mimir/releases
|
|
53
|
+
|
|
54
|
+
Or build from source:
|
|
55
|
+
```bash
|
|
56
|
+
cargo install mimir
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from google.adk.agents import Agent
|
|
63
|
+
from adk_mimir_memory import MimirMemoryService
|
|
64
|
+
|
|
65
|
+
agent = Agent(
|
|
66
|
+
name="my_agent",
|
|
67
|
+
model="gemini-2.5-flash",
|
|
68
|
+
instruction="You are a helpful assistant with persistent memory.",
|
|
69
|
+
memory_service=MimirMemoryService(
|
|
70
|
+
db_path="~/.adk/mimir.db",
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
That's it. Sessions, events, and explicit memories are now persisted across restarts.
|
|
76
|
+
|
|
77
|
+
### Configuration
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Custom database location
|
|
81
|
+
MimirMemoryService(db_path="/data/agent_memory.db")
|
|
82
|
+
|
|
83
|
+
# Custom mimir binary path (if not on $PATH)
|
|
84
|
+
MimirMemoryService(mimir_binary="/usr/local/bin/mimir")
|
|
85
|
+
|
|
86
|
+
# Both
|
|
87
|
+
MimirMemoryService(
|
|
88
|
+
db_path="/data/agent_memory.db",
|
|
89
|
+
mimir_binary="/usr/local/bin/mimir",
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Perseus Live Context (Optional)
|
|
94
|
+
|
|
95
|
+
This package also includes a drop-in agent with live workspace awareness via [Perseus](https://github.com/Perseus-Computing-LLC/perseus):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from adk_mimir_memory.perseus_context import perseus_context_agent
|
|
99
|
+
|
|
100
|
+
# The agent resolves @file, @search, @memory directives at inference time
|
|
101
|
+
runner.run_async(
|
|
102
|
+
user_id="user",
|
|
103
|
+
session_id="session",
|
|
104
|
+
new_message=types.Content(role="user", parts=[types.Part.from_text(
|
|
105
|
+
text="What does the README say about deployment?"
|
|
106
|
+
)]),
|
|
107
|
+
agent=perseus_context_agent,
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install adk-mimir-memory[perseus] # installs perseus-ctx
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Set directives via session state:
|
|
116
|
+
```python
|
|
117
|
+
session = await runner.session_service.create_session(
|
|
118
|
+
app_name="my_app",
|
|
119
|
+
user_id="user",
|
|
120
|
+
state={
|
|
121
|
+
"_perseus_directives": "@file AGENTS.md @file README.md @memory deployment",
|
|
122
|
+
"_perseus_workspace": "/path/to/project",
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## How It Works
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
┌─────────────┐ JSON-RPC (MCP stdio) ┌──────────┐
|
|
131
|
+
│ ADK Agent │ ──────────────────────────▶ │ Mimir │
|
|
132
|
+
│ (Python) │ ◀────────────────────────── │ (Rust) │
|
|
133
|
+
└─────────────┘ └────┬─────┘
|
|
134
|
+
│
|
|
135
|
+
SQLite + FTS5
|
|
136
|
+
(AES-256-GCM)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `MimirMemoryService` spawns a `mimir` subprocess and communicates via JSON-RPC over stdin/stdout (MCP stdio transport). Each `add_session_to_memory`, `add_memory`, and `search_memory` call translates to a Mimir MCP tool invocation.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT — see [Mimir](https://github.com/Perseus-Computing-LLC/mimir) and [Perseus](https://github.com/Perseus-Computing-LLC/perseus) for the backing services.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ADK Mimir Memory
|
|
2
|
+
|
|
3
|
+
Persistent, local, encrypted cross-session memory for [Google ADK](https://github.com/google/adk-python) agents — backed by [Mimir](https://github.com/Perseus-Computing-LLC/mimir).
|
|
4
|
+
|
|
5
|
+
## Why Mimir?
|
|
6
|
+
|
|
7
|
+
| Backend | Dependencies | Encryption | Hybrid Search | Local |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| **InMemoryMemoryService** | None | ❌ | ❌ | ✅ |
|
|
10
|
+
| **VertexAiMemoryBankService** | GCP + Gemini | ❌ | Gemini-driven | ❌ |
|
|
11
|
+
| **VertexAiRagMemoryService** | GCP + RAG | ❌ | GCP vector | ❌ |
|
|
12
|
+
| **MimirMemoryService** | **Single binary** | **✅ AES-256** | **✅ BM25+FTS5+Dense** | **✅** |
|
|
13
|
+
|
|
14
|
+
- **Zero cloud dependencies** — a single Rust binary, SQLite database, fully local
|
|
15
|
+
- **AES-256-GCM encryption** at rest — your memory data stays private
|
|
16
|
+
- **Hybrid search** — BM25 keyword + FTS5 + dense vector search
|
|
17
|
+
- **30+ MCP tools** — remember, recall, synthesize, benchmark, federate, and more
|
|
18
|
+
- **Ebbinghaus confidence decay** — memories fade naturally, important ones persist
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install adk-mimir-memory
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This package requires the `mimir` binary. Download it from:
|
|
27
|
+
https://github.com/Perseus-Computing-LLC/mimir/releases
|
|
28
|
+
|
|
29
|
+
Or build from source:
|
|
30
|
+
```bash
|
|
31
|
+
cargo install mimir
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from google.adk.agents import Agent
|
|
38
|
+
from adk_mimir_memory import MimirMemoryService
|
|
39
|
+
|
|
40
|
+
agent = Agent(
|
|
41
|
+
name="my_agent",
|
|
42
|
+
model="gemini-2.5-flash",
|
|
43
|
+
instruction="You are a helpful assistant with persistent memory.",
|
|
44
|
+
memory_service=MimirMemoryService(
|
|
45
|
+
db_path="~/.adk/mimir.db",
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it. Sessions, events, and explicit memories are now persisted across restarts.
|
|
51
|
+
|
|
52
|
+
### Configuration
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Custom database location
|
|
56
|
+
MimirMemoryService(db_path="/data/agent_memory.db")
|
|
57
|
+
|
|
58
|
+
# Custom mimir binary path (if not on $PATH)
|
|
59
|
+
MimirMemoryService(mimir_binary="/usr/local/bin/mimir")
|
|
60
|
+
|
|
61
|
+
# Both
|
|
62
|
+
MimirMemoryService(
|
|
63
|
+
db_path="/data/agent_memory.db",
|
|
64
|
+
mimir_binary="/usr/local/bin/mimir",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Perseus Live Context (Optional)
|
|
69
|
+
|
|
70
|
+
This package also includes a drop-in agent with live workspace awareness via [Perseus](https://github.com/Perseus-Computing-LLC/perseus):
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from adk_mimir_memory.perseus_context import perseus_context_agent
|
|
74
|
+
|
|
75
|
+
# The agent resolves @file, @search, @memory directives at inference time
|
|
76
|
+
runner.run_async(
|
|
77
|
+
user_id="user",
|
|
78
|
+
session_id="session",
|
|
79
|
+
new_message=types.Content(role="user", parts=[types.Part.from_text(
|
|
80
|
+
text="What does the README say about deployment?"
|
|
81
|
+
)]),
|
|
82
|
+
agent=perseus_context_agent,
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install adk-mimir-memory[perseus] # installs perseus-ctx
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Set directives via session state:
|
|
91
|
+
```python
|
|
92
|
+
session = await runner.session_service.create_session(
|
|
93
|
+
app_name="my_app",
|
|
94
|
+
user_id="user",
|
|
95
|
+
state={
|
|
96
|
+
"_perseus_directives": "@file AGENTS.md @file README.md @memory deployment",
|
|
97
|
+
"_perseus_workspace": "/path/to/project",
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## How It Works
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
┌─────────────┐ JSON-RPC (MCP stdio) ┌──────────┐
|
|
106
|
+
│ ADK Agent │ ──────────────────────────▶ │ Mimir │
|
|
107
|
+
│ (Python) │ ◀────────────────────────── │ (Rust) │
|
|
108
|
+
└─────────────┘ └────┬─────┘
|
|
109
|
+
│
|
|
110
|
+
SQLite + FTS5
|
|
111
|
+
(AES-256-GCM)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `MimirMemoryService` spawns a `mimir` subprocess and communicates via JSON-RPC over stdin/stdout (MCP stdio transport). Each `add_session_to_memory`, `add_memory`, and `search_memory` call translates to a Mimir MCP tool invocation.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT — see [Mimir](https://github.com/Perseus-Computing-LLC/mimir) and [Perseus](https://github.com/Perseus-Computing-LLC/perseus) for the backing services.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""ADK Mimir Memory Service — persistent, local, encrypted cross-session memory.
|
|
2
|
+
|
|
3
|
+
Mimir (github.com/Perseus-Computing-LLC/mimir) is an open-source (MIT)
|
|
4
|
+
persistent memory engine with 30+ MCP tools, FTS5 + dense hybrid search,
|
|
5
|
+
and optional AES-256-GCM encryption. This service talks to the Mimir
|
|
6
|
+
binary via JSON-RPC over stdin/stdout (MCP stdio transport).
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
A ``mimir`` binary must be on ``$PATH`` or passed explicitly via
|
|
10
|
+
``mimir_binary``. Download from:
|
|
11
|
+
https://github.com/Perseus-Computing-LLC/mimir/releases
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .mimir_memory_service import MimirMemoryService
|
|
15
|
+
|
|
16
|
+
__all__ = ["MimirMemoryService"]
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Mimir persistent memory service for ADK.
|
|
2
|
+
|
|
3
|
+
Mimir (github.com/Perseus-Computing-LLC/mimir) is an open-source (MIT)
|
|
4
|
+
persistent memory engine with 30+ MCP tools, FTS5 + dense hybrid search,
|
|
5
|
+
and optional AES-256-GCM encryption. This service talks to the Mimir
|
|
6
|
+
binary via JSON-RPC over stdin/stdout (MCP stdio transport).
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
A ``mimir`` binary must be on ``$PATH`` or passed explicitly via
|
|
10
|
+
``mimir_binary``. Build from source or download a pre-built binary from
|
|
11
|
+
the Mimir releases page.
|
|
12
|
+
|
|
13
|
+
Usage::
|
|
14
|
+
|
|
15
|
+
from adk_mimir_memory import MimirMemoryService
|
|
16
|
+
from google.adk.memory import InMemoryMemoryService
|
|
17
|
+
|
|
18
|
+
# Swap out the default in-memory service for persistent Mimir
|
|
19
|
+
agent = Agent(
|
|
20
|
+
name="my_agent",
|
|
21
|
+
memory_service=MimirMemoryService(
|
|
22
|
+
db_path="~/.adk/mimir.db",
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import atexit
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import shutil
|
|
34
|
+
import subprocess
|
|
35
|
+
import threading
|
|
36
|
+
from collections.abc import Mapping
|
|
37
|
+
from collections.abc import Sequence
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
|
+
|
|
41
|
+
from typing_extensions import override
|
|
42
|
+
|
|
43
|
+
from google.adk.memory.base_memory_service import BaseMemoryService
|
|
44
|
+
from google.adk.memory.base_memory_service import SearchMemoryResponse
|
|
45
|
+
from google.adk.memory.memory_entry import MemoryEntry
|
|
46
|
+
from google.genai import types
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from google.adk.events.event import Event
|
|
50
|
+
from google.adk.sessions.session import Session
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
_MIMIR_CATEGORY = "adk-memory"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _format_timestamp(timestamp: float) -> str:
|
|
58
|
+
"""Formats a unix timestamp as an ISO 8601 string."""
|
|
59
|
+
return datetime.fromtimestamp(timestamp).isoformat()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MimirMemoryService(BaseMemoryService):
|
|
63
|
+
"""Persistent memory service backed by Mimir.
|
|
64
|
+
|
|
65
|
+
Talks to a local ``mimir`` binary via JSON-RPC (MCP stdio). Stores
|
|
66
|
+
session events as structured entities and supports keyword (FTS5) search
|
|
67
|
+
across sessions.
|
|
68
|
+
|
|
69
|
+
This class is thread-safe.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
db_path: Filesystem path to the Mimir SQLite database.
|
|
73
|
+
mimir_binary: Path or name of the ``mimir`` executable.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
db_path: str = "~/.adk/mimir.db",
|
|
79
|
+
mimir_binary: str = "mimir",
|
|
80
|
+
):
|
|
81
|
+
"""Initializes the Mimir memory service.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
db_path: Path to the Mimir database file. Defaults to
|
|
85
|
+
``~/.adk/mimir.db``.
|
|
86
|
+
mimir_binary: Name or absolute path of the ``mimir`` executable.
|
|
87
|
+
Defaults to ``mimir`` (resolved from ``$PATH``).
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
RuntimeError: If the ``mimir`` binary cannot be found or the
|
|
91
|
+
subprocess fails to start.
|
|
92
|
+
"""
|
|
93
|
+
self.db_path = os.path.expanduser(db_path)
|
|
94
|
+
|
|
95
|
+
# Resolve the mimir binary.
|
|
96
|
+
if os.path.isabs(mimir_binary):
|
|
97
|
+
self._mimir_binary = mimir_binary
|
|
98
|
+
else:
|
|
99
|
+
resolved = shutil.which(mimir_binary)
|
|
100
|
+
if resolved is None:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f"mimir binary not found on $PATH (looked for '{mimir_binary}'). "
|
|
103
|
+
"Install Mimir from https://github.com/Perseus-Computing-LLC/mimir "
|
|
104
|
+
"or pass the absolute path via mimir_binary=."
|
|
105
|
+
)
|
|
106
|
+
self._mimir_binary = resolved
|
|
107
|
+
|
|
108
|
+
# Ensure the database directory exists.
|
|
109
|
+
os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
|
|
110
|
+
|
|
111
|
+
# Start the Mimir MCP stdio subprocess.
|
|
112
|
+
self._proc = subprocess.Popen(
|
|
113
|
+
[self._mimir_binary, "--db", self.db_path],
|
|
114
|
+
stdin=subprocess.PIPE,
|
|
115
|
+
stdout=subprocess.PIPE,
|
|
116
|
+
stderr=subprocess.PIPE,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
self._lock = threading.Lock()
|
|
120
|
+
self._request_id = 0
|
|
121
|
+
|
|
122
|
+
# Initialize the MCP session.
|
|
123
|
+
self._rpc(
|
|
124
|
+
"initialize",
|
|
125
|
+
{
|
|
126
|
+
"protocolVersion": "2024-11-05",
|
|
127
|
+
"capabilities": {},
|
|
128
|
+
"clientInfo": {"name": "adk-mimir-memory-service", "version": "1.0"},
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Clean up the subprocess on exit.
|
|
133
|
+
atexit.register(self._close)
|
|
134
|
+
|
|
135
|
+
def _close(self) -> None:
|
|
136
|
+
"""Terminates the Mimir subprocess."""
|
|
137
|
+
try:
|
|
138
|
+
self._proc.terminate()
|
|
139
|
+
self._proc.wait(timeout=5)
|
|
140
|
+
except Exception:
|
|
141
|
+
try:
|
|
142
|
+
self._proc.kill()
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def _next_id(self) -> int:
|
|
147
|
+
self._request_id += 1
|
|
148
|
+
return self._request_id
|
|
149
|
+
|
|
150
|
+
def _rpc(self, method: str, params: object) -> dict:
|
|
151
|
+
"""Sends a JSON-RPC request to Mimir and returns the result dict.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
method: The MCP method name (e.g. ``tools/call``).
|
|
155
|
+
params: The method parameters.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The ``result`` field of the JSON-RPC response.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
RuntimeError: If the RPC returns an error.
|
|
162
|
+
"""
|
|
163
|
+
req = {
|
|
164
|
+
"jsonrpc": "2.0",
|
|
165
|
+
"id": self._next_id(),
|
|
166
|
+
"method": method,
|
|
167
|
+
"params": params,
|
|
168
|
+
}
|
|
169
|
+
payload = json.dumps(req, default=str)
|
|
170
|
+
|
|
171
|
+
with self._lock:
|
|
172
|
+
try:
|
|
173
|
+
self._proc.stdin.write(payload + "\n")
|
|
174
|
+
self._proc.stdin.flush()
|
|
175
|
+
raw = self._proc.stdout.readline()
|
|
176
|
+
except (BrokenPipeError, OSError) as e:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"Mimir subprocess communication failed: {e}. "
|
|
179
|
+
"The mimir process may have crashed."
|
|
180
|
+
) from e
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
resp = json.loads(raw)
|
|
184
|
+
except json.JSONDecodeError as e:
|
|
185
|
+
raise RuntimeError(
|
|
186
|
+
f"Failed to parse Mimir response: {e}. Raw: {raw[:200]}"
|
|
187
|
+
) from e
|
|
188
|
+
|
|
189
|
+
if "error" in resp:
|
|
190
|
+
err = resp["error"]
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
f"Mimir RPC error [{err.get('code')}]: {err.get('message')}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return resp.get("result", {})
|
|
196
|
+
|
|
197
|
+
def _call_tool(self, name: str, arguments: dict) -> dict:
|
|
198
|
+
"""Calls a Mimir MCP tool and returns the ``structuredContent``."""
|
|
199
|
+
result = self._rpc(
|
|
200
|
+
"tools/call",
|
|
201
|
+
{"name": name, "arguments": arguments},
|
|
202
|
+
)
|
|
203
|
+
# MCP result is {content: [{type: "text", text: "..."}], structuredContent: {...}}
|
|
204
|
+
sc = result.get("structuredContent")
|
|
205
|
+
if sc is not None:
|
|
206
|
+
return sc
|
|
207
|
+
# Fallback: parse the text content
|
|
208
|
+
content = result.get("content", [])
|
|
209
|
+
if content:
|
|
210
|
+
try:
|
|
211
|
+
return json.loads(content[0].get("text", "{}"))
|
|
212
|
+
except (json.JSONDecodeError, IndexError, KeyError):
|
|
213
|
+
pass
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
@override
|
|
217
|
+
async def add_session_to_memory(self, session: Session) -> None:
|
|
218
|
+
"""Stores all events from a session in Mimir.
|
|
219
|
+
|
|
220
|
+
Each session is stored as a single Mimir entity keyed by session ID.
|
|
221
|
+
Subsequent calls for the same session will update the stored events.
|
|
222
|
+
"""
|
|
223
|
+
if not session.events:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
events_data = []
|
|
227
|
+
for event in session.events:
|
|
228
|
+
if not event.content or not event.content.parts:
|
|
229
|
+
continue
|
|
230
|
+
parts = []
|
|
231
|
+
for part in event.content.parts:
|
|
232
|
+
if part.text:
|
|
233
|
+
parts.append({"text": part.text})
|
|
234
|
+
elif hasattr(part, "function_call") and part.function_call:
|
|
235
|
+
parts.append({
|
|
236
|
+
"function_call": {
|
|
237
|
+
"name": part.function_call.name,
|
|
238
|
+
"args": part.function_call.args,
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
elif hasattr(part, "function_response") and part.function_response:
|
|
242
|
+
parts.append({
|
|
243
|
+
"function_response": {
|
|
244
|
+
"name": part.function_response.name,
|
|
245
|
+
"response": str(part.function_response.response)[:2000],
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
if parts:
|
|
249
|
+
events_data.append({
|
|
250
|
+
"author": event.author,
|
|
251
|
+
"timestamp": event.timestamp,
|
|
252
|
+
"parts": parts,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
if not events_data:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
self._call_tool(
|
|
259
|
+
"mimir_remember",
|
|
260
|
+
{
|
|
261
|
+
"category": _MIMIR_CATEGORY,
|
|
262
|
+
"key": f"session:{session.app_name}:{session.user_id}:{session.id}",
|
|
263
|
+
"body_json": json.dumps({
|
|
264
|
+
"session_id": session.id,
|
|
265
|
+
"app_name": session.app_name,
|
|
266
|
+
"user_id": session.user_id,
|
|
267
|
+
"events": events_data,
|
|
268
|
+
"event_count": len(events_data),
|
|
269
|
+
}),
|
|
270
|
+
"tags": ["adk", "session", session.app_name],
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@override
|
|
275
|
+
async def add_events_to_memory(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
app_name: str,
|
|
279
|
+
user_id: str,
|
|
280
|
+
events: Sequence[Event],
|
|
281
|
+
session_id: str | None = None,
|
|
282
|
+
custom_metadata: Mapping[str, object] | None = None,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Adds a delta of events to Mimir.
|
|
285
|
+
|
|
286
|
+
Events are appended to an existing session entity if one exists, or a
|
|
287
|
+
new entity is created. This is the recommended method for incremental
|
|
288
|
+
memory updates during long-running sessions.
|
|
289
|
+
"""
|
|
290
|
+
_ = custom_metadata
|
|
291
|
+
events_data = []
|
|
292
|
+
for event in events:
|
|
293
|
+
if not event.content or not event.content.parts:
|
|
294
|
+
continue
|
|
295
|
+
parts = []
|
|
296
|
+
for part in event.content.parts:
|
|
297
|
+
if part.text:
|
|
298
|
+
parts.append({"text": part.text})
|
|
299
|
+
elif hasattr(part, "function_call") and part.function_call:
|
|
300
|
+
parts.append({
|
|
301
|
+
"function_call": {
|
|
302
|
+
"name": part.function_call.name,
|
|
303
|
+
"args": part.function_call.args,
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
if parts:
|
|
307
|
+
events_data.append({
|
|
308
|
+
"author": event.author,
|
|
309
|
+
"timestamp": event.timestamp,
|
|
310
|
+
"parts": parts,
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
if not events_data:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
import time
|
|
317
|
+
|
|
318
|
+
sid = session_id or "__unknown__"
|
|
319
|
+
delta_key = f"delta:{app_name}:{user_id}:{sid}:{int(time.time() * 1000)}"
|
|
320
|
+
|
|
321
|
+
self._call_tool(
|
|
322
|
+
"mimir_remember",
|
|
323
|
+
{
|
|
324
|
+
"category": _MIMIR_CATEGORY,
|
|
325
|
+
"key": delta_key,
|
|
326
|
+
"body_json": json.dumps({
|
|
327
|
+
"session_id": sid,
|
|
328
|
+
"app_name": app_name,
|
|
329
|
+
"user_id": user_id,
|
|
330
|
+
"events": events_data,
|
|
331
|
+
"event_count": len(events_data),
|
|
332
|
+
}),
|
|
333
|
+
"tags": ["adk", "delta", app_name],
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
@override
|
|
338
|
+
async def add_memory(
|
|
339
|
+
self,
|
|
340
|
+
*,
|
|
341
|
+
app_name: str,
|
|
342
|
+
user_id: str,
|
|
343
|
+
memories: Sequence[MemoryEntry],
|
|
344
|
+
custom_metadata: Mapping[str, object] | None = None,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Adds explicit memory entries directly to Mimir.
|
|
347
|
+
|
|
348
|
+
Each MemoryEntry is stored as a separate entity tagged for the given
|
|
349
|
+
application and user.
|
|
350
|
+
"""
|
|
351
|
+
_ = custom_metadata
|
|
352
|
+
for i, entry in enumerate(memories):
|
|
353
|
+
content_text = ""
|
|
354
|
+
if entry.content and entry.content.parts:
|
|
355
|
+
content_text = " ".join(
|
|
356
|
+
p.text for p in entry.content.parts if p.text
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if not content_text:
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
self._call_tool(
|
|
363
|
+
"mimir_remember",
|
|
364
|
+
{
|
|
365
|
+
"category": _MIMIR_CATEGORY,
|
|
366
|
+
"key": f"memory:{app_name}:{user_id}:{entry.id or i}",
|
|
367
|
+
"body_json": json.dumps({
|
|
368
|
+
"content": content_text,
|
|
369
|
+
"author": entry.author,
|
|
370
|
+
"timestamp": entry.timestamp,
|
|
371
|
+
"metadata": entry.custom_metadata,
|
|
372
|
+
}),
|
|
373
|
+
"tags": ["adk", "explicit", app_name],
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
@override
|
|
378
|
+
async def search_memory(
|
|
379
|
+
self,
|
|
380
|
+
*,
|
|
381
|
+
app_name: str,
|
|
382
|
+
user_id: str,
|
|
383
|
+
query: str,
|
|
384
|
+
) -> SearchMemoryResponse:
|
|
385
|
+
"""Searches Mimir for memories matching the query.
|
|
386
|
+
|
|
387
|
+
Uses Mimir's FTS5 keyword search. Results are scoped to the given
|
|
388
|
+
application and user by filtering on the stored tags and body content.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
app_name: The application name for memory scope.
|
|
392
|
+
user_id: The user ID for memory scope.
|
|
393
|
+
query: The natural-language query to search for.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
A SearchMemoryResponse containing matching MemoryEntry objects.
|
|
397
|
+
"""
|
|
398
|
+
scoped_query = f"{query} {app_name} adk-memory {user_id}"
|
|
399
|
+
result = self._call_tool(
|
|
400
|
+
"mimir_recall",
|
|
401
|
+
{
|
|
402
|
+
"query": scoped_query,
|
|
403
|
+
"limit": 20,
|
|
404
|
+
"category": _MIMIR_CATEGORY,
|
|
405
|
+
},
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
response = SearchMemoryResponse()
|
|
409
|
+
items = result.get("items", [])
|
|
410
|
+
for item in items:
|
|
411
|
+
body = item.get("body_json", "{}")
|
|
412
|
+
try:
|
|
413
|
+
body_data = json.loads(body) if isinstance(body, str) else body
|
|
414
|
+
except json.JSONDecodeError:
|
|
415
|
+
body_data = {}
|
|
416
|
+
|
|
417
|
+
# Determine the best text content to surface.
|
|
418
|
+
content_text = body_data.get("content", "")
|
|
419
|
+
if not content_text:
|
|
420
|
+
events = body_data.get("events", [])
|
|
421
|
+
texts = []
|
|
422
|
+
for ev in events:
|
|
423
|
+
for part in ev.get("parts", []):
|
|
424
|
+
if part.get("text"):
|
|
425
|
+
texts.append(part["text"])
|
|
426
|
+
content_text = " | ".join(texts[:5]) if texts else ""
|
|
427
|
+
|
|
428
|
+
if not content_text:
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
response.memories.append(
|
|
432
|
+
MemoryEntry(
|
|
433
|
+
content=types.Content(
|
|
434
|
+
role="model",
|
|
435
|
+
parts=[types.Part.from_text(text=content_text)],
|
|
436
|
+
),
|
|
437
|
+
author=body_data.get("author") or "mimir",
|
|
438
|
+
timestamp=body_data.get("timestamp")
|
|
439
|
+
or _format_timestamp(
|
|
440
|
+
item.get("created_at_unix_ms", 0) / 1000.0
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return response
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Agent that uses Perseus for live workspace context resolution.
|
|
2
|
+
|
|
3
|
+
Perseus (github.com/Perseus-Computing-LLC/perseus) is an open-source (MIT)
|
|
4
|
+
live context engine that resolves workspace state at inference time. Instead
|
|
5
|
+
of baking static instructions into prompts, agents use Perseus directives
|
|
6
|
+
(``@file``, ``@search``, ``@memory``, etc.) to pull in exactly what they
|
|
7
|
+
need.
|
|
8
|
+
|
|
9
|
+
This module provides a drop-in agent demonstrating the pattern:
|
|
10
|
+
1. A ``before_agent_callback`` resolves Perseus context before each run.
|
|
11
|
+
2. The resolved context is injected into the agent's instruction template.
|
|
12
|
+
3. The agent knows about workspace state without it being hardcoded.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
from adk_mimir_memory.perseus_context import perseus_context_agent
|
|
17
|
+
|
|
18
|
+
# Use as a standalone agent
|
|
19
|
+
runner.run_async(
|
|
20
|
+
user_id="user",
|
|
21
|
+
session_id="session",
|
|
22
|
+
new_message=types.Content(...),
|
|
23
|
+
agent=perseus_context_agent,
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
|
|
32
|
+
from google.adk.agents import Agent
|
|
33
|
+
from google.adk.agents.callback_context import CallbackContext
|
|
34
|
+
|
|
35
|
+
_PERSEUS_BINARY = os.environ.get("PERSEUS_BINARY", "perseus")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_perseus_context(callback_context: CallbackContext) -> None:
|
|
39
|
+
"""Resolves Perseus directives and stores the result in agent state.
|
|
40
|
+
|
|
41
|
+
Called before each agent run. Reads directives from a state key
|
|
42
|
+
(defaulting to a sensible set) and resolves them via the Perseus CLI.
|
|
43
|
+
"""
|
|
44
|
+
directives = callback_context.state.get(
|
|
45
|
+
"_perseus_directives",
|
|
46
|
+
"@file AGENTS.md @file README.md",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
workspace = callback_context.state.get("_perseus_workspace", os.getcwd())
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[_PERSEUS_BINARY, "resolve", directives],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=15,
|
|
57
|
+
cwd=workspace,
|
|
58
|
+
)
|
|
59
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
60
|
+
callback_context.state["_perseus_context"] = result.stdout.strip()
|
|
61
|
+
else:
|
|
62
|
+
callback_context.state["_perseus_context"] = (
|
|
63
|
+
f"(Perseus: no context resolved for directives: {directives})"
|
|
64
|
+
)
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
callback_context.state["_perseus_context"] = (
|
|
67
|
+
"(Perseus CLI not installed. Install with: pip install perseus-ctx)"
|
|
68
|
+
)
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
callback_context.state["_perseus_context"] = (
|
|
71
|
+
"(Perseus resolution timed out)"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
perseus_context_agent = Agent(
|
|
76
|
+
name="perseus_context_agent",
|
|
77
|
+
description=(
|
|
78
|
+
"Agent with live workspace context via Perseus. Knows about "
|
|
79
|
+
"project files, git state, and workspace structure without "
|
|
80
|
+
"hardcoded instructions."
|
|
81
|
+
),
|
|
82
|
+
before_agent_callback=resolve_perseus_context,
|
|
83
|
+
instruction="""\
|
|
84
|
+
You are a helpful assistant with live context about the current workspace.
|
|
85
|
+
|
|
86
|
+
The following context was resolved by Perseus from the workspace files
|
|
87
|
+
and state. Use it to answer questions accurately.
|
|
88
|
+
|
|
89
|
+
--- BEGIN PERSEUS CONTEXT ---
|
|
90
|
+
{_perseus_context}
|
|
91
|
+
--- END PERSEUS CONTEXT ---
|
|
92
|
+
|
|
93
|
+
Use this context to give grounded, file-aware answers. If the context
|
|
94
|
+
is empty or unavailable, let the user know and fall back to general
|
|
95
|
+
knowledge.
|
|
96
|
+
""",
|
|
97
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adk-mimir-memory"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Thomas Connally", email = "51974392+tcconnally@users.noreply.github.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Persistent, local, encrypted cross-session memory for Google ADK agents — backed by Mimir."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"google-adk>=1.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
perseus = ["perseus-ctx"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Perseus-Computing-LLC/adk-mimir-memory"
|
|
35
|
+
Repository = "https://github.com/Perseus-Computing-LLC/adk-mimir-memory"
|
|
36
|
+
"Bug Tracker" = "https://github.com/Perseus-Computing-LLC/adk-mimir-memory/issues"
|