anatid 0.1.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.
- anatid-0.1.0/.gitignore +42 -0
- anatid-0.1.0/LICENSE +21 -0
- anatid-0.1.0/PKG-INFO +337 -0
- anatid-0.1.0/README.md +306 -0
- anatid-0.1.0/ext/README.md +77 -0
- anatid-0.1.0/ext/test/README.md +25 -0
- anatid-0.1.0/pyproject.toml +56 -0
- anatid-0.1.0/src/anatid/__init__.py +141 -0
- anatid-0.1.0/src/anatid/csr.py +351 -0
- anatid-0.1.0/src/anatid/database.py +810 -0
- anatid-0.1.0/src/anatid/errors.py +96 -0
- anatid-0.1.0/src/anatid/ids.py +95 -0
- anatid-0.1.0/src/anatid/integrations/__init__.py +11 -0
- anatid-0.1.0/src/anatid/integrations/mcp/__init__.py +48 -0
- anatid-0.1.0/src/anatid/integrations/mcp/server.py +812 -0
- anatid-0.1.0/src/anatid/integrations/mcp/sqlgate.py +462 -0
- anatid-0.1.0/src/anatid/integrations/openai_agents/__init__.py +70 -0
- anatid-0.1.0/src/anatid/integrations/openai_agents/approvals.py +353 -0
- anatid-0.1.0/src/anatid/integrations/openai_agents/session.py +686 -0
- anatid-0.1.0/src/anatid/integrations/openai_agents/tools.py +477 -0
- anatid-0.1.0/src/anatid/recall.py +613 -0
- anatid-0.1.0/src/anatid/schema.py +639 -0
- anatid-0.1.0/src/anatid/types.py +586 -0
- anatid-0.1.0/src/anatid/verbs.py +1049 -0
- anatid-0.1.0/tests/conftest.py +99 -0
- anatid-0.1.0/tests/test_core.py +1104 -0
- anatid-0.1.0/tests/test_extension.py +371 -0
- anatid-0.1.0/tests/test_mcp.py +567 -0
- anatid-0.1.0/tests/test_openai_agents.py +642 -0
anatid-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
|
|
10
|
+
# os
|
|
11
|
+
.DS_Store
|
|
12
|
+
|
|
13
|
+
# vendored duckdb checkouts (submodules; see ext/README.md)
|
|
14
|
+
ext/duckdb
|
|
15
|
+
ext/extension-ci-tools
|
|
16
|
+
ext/build/
|
|
17
|
+
spike/extension/duckdb
|
|
18
|
+
spike/extension/extension-ci-tools
|
|
19
|
+
spike/extension/build/
|
|
20
|
+
|
|
21
|
+
# benchmark artifacts: generated data and multi-GB engine files.
|
|
22
|
+
# The result JSON and REPORT are kept as evidence; the databases are not.
|
|
23
|
+
spike/data/
|
|
24
|
+
spike/results/*.duckdb
|
|
25
|
+
spike/results/*.duckdb.wal
|
|
26
|
+
spike/results/*.lbdb
|
|
27
|
+
spike/results/*.lbdb.wal
|
|
28
|
+
spike/results/*.grafeo
|
|
29
|
+
spike/results/*.log
|
|
30
|
+
spike/*.log
|
|
31
|
+
|
|
32
|
+
# the original attachment this project started from
|
|
33
|
+
Kimi_Agent_AI Graph DB Builder.zip
|
|
34
|
+
|
|
35
|
+
# the spike build tree is superseded by ext/ (and carries its own .git)
|
|
36
|
+
spike/extension
|
|
37
|
+
|
|
38
|
+
# secrets - never commit
|
|
39
|
+
.env
|
|
40
|
+
.env.*
|
|
41
|
+
!.env.example
|
|
42
|
+
*.pypirc
|
anatid-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anatid contributors
|
|
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.
|
anatid-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anatid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An open-source embedded graph database for AI agents, built on DuckDB.
|
|
5
|
+
Project-URL: Homepage, https://github.com/thedatasense/anatid
|
|
6
|
+
Project-URL: Source, https://github.com/thedatasense/anatid
|
|
7
|
+
Author: anatid contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,bitemporal,database,duckdb,embedded,graph,memory
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
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 :: Database :: Database Engines/Servers
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: duckdb>=1.5
|
|
22
|
+
Provides-Extra: agents
|
|
23
|
+
Requires-Dist: openai-agents>=0.22; extra == 'agents'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mcp>=2.1; extra == 'dev'
|
|
26
|
+
Requires-Dist: openai-agents>=0.22; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
28
|
+
Provides-Extra: mcp
|
|
29
|
+
Requires-Dist: mcp>=2.1; extra == 'mcp'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# anatid
|
|
33
|
+
|
|
34
|
+
**Embedded graph memory for AI agents, built on DuckDB. MIT licensed.**
|
|
35
|
+
|
|
36
|
+
One file on disk. No server, no daemon, no cluster. `pip install anatid` and your agent has a
|
|
37
|
+
memory that is a *graph* (entities and the edges between them), *bitemporal* (what was true, and
|
|
38
|
+
what you believed, at any past instant), and *searchable three ways at once* (vector similarity,
|
|
39
|
+
BM25 text, graph traversal — fused into one ranked list). Writes can be routed through the OpenAI
|
|
40
|
+
Agents SDK's human-in-the-loop approval flow, so an agent proposes a change to its memory and a
|
|
41
|
+
person decides whether it lands.
|
|
42
|
+
|
|
43
|
+
anatid exists because [Kuzu was archived on 2025-10-10](https://github.com/kuzudb/kuzu). Graphiti
|
|
44
|
+
deprecated its Kuzu driver, Mem0 removed open-source graph memory in v2.0.0, and Cognee is
|
|
45
|
+
migrating away. That left a lot of people with an embedded graph memory and nowhere to go. anatid
|
|
46
|
+
is somewhere to go, and it is built on an engine with a foundation behind it.
|
|
47
|
+
|
|
48
|
+
The engine choice was not a preference. It was a measurement: **2-hop recall at 1,000,000 memories
|
|
49
|
+
runs 2.6x faster on DuckDB than on a tuned LadybugDB** (the maintained MIT fork of Kuzu), returning
|
|
50
|
+
byte-identical result lists. [The numbers, the method, and the caveats are in
|
|
51
|
+
`docs/benchmarks.md`](docs/benchmarks.md).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install anatid # just duckdb
|
|
59
|
+
pip install "anatid[agents]" # + the OpenAI Agents SDK integration
|
|
60
|
+
pip install "anatid[mcp]" # + the MCP server
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The version in this repository is `0.1.0` and is not on PyPI yet. Until it is:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "git+https://github.com/thedatasense/anatid"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Python 3.10-3.13, one required dependency (`duckdb>=1.5`). CI runs the test suite — including
|
|
70
|
+
both integration suites, which is why `[dev]` installs `openai-agents` and `mcp` — on Linux and
|
|
71
|
+
macOS across all four Python versions. The tests that load the 100k-row spike dataset and the
|
|
72
|
+
ones that need the compiled C++ extension skip in CI, because neither is in the repository;
|
|
73
|
+
they are run locally before a release. Windows should work (DuckDB supports it) but is not tested.
|
|
74
|
+
|
|
75
|
+
## Quickstart
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from anatid import Anatid, utcnow
|
|
79
|
+
|
|
80
|
+
vec = [0.0] * 63 + [1.0] # your embedding model's output
|
|
81
|
+
|
|
82
|
+
with Anatid.open("agent.anatid", tenant=1, embedding_dim=64) as db:
|
|
83
|
+
db.relate("Ada", "Kestrel", rel_kind="leads") # an entity -> entity edge
|
|
84
|
+
m = db.remember("Ada prefers dark roast coffee", # a fact, filed under 2 entities
|
|
85
|
+
entities=["Ada", "coffee"], kind="preference",
|
|
86
|
+
embedding=vec, writer="agent-1",
|
|
87
|
+
episode="Standup 2026-03-01: Ada takes it dark roast.") # raw evidence first
|
|
88
|
+
t0 = utcnow()
|
|
89
|
+
db.rebuild_fts_index() # BM25 is not incremental: you say when
|
|
90
|
+
|
|
91
|
+
hits = db.recall("coffee", embedding=vec, seed_entity="Ada", k=3)
|
|
92
|
+
print(hits[0].content, hits[0].sources, "| bm25_stale:", hits.bm25_stale)
|
|
93
|
+
|
|
94
|
+
new = db.supersede(m.memory_id, "Ada switched to decaf") # closes the old row, keeps it
|
|
95
|
+
print("old still current?", db.get(m.memory_id).is_current) # False -- history is intact
|
|
96
|
+
print("at t0:", [x.content for x in db.as_of(t0).recall_2hop("Ada")])
|
|
97
|
+
print("evidence:", db.provenance(new.memory_id).source_text)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Ada prefers dark roast coffee ('vector', 'text', 'graph') | bm25_stale: False
|
|
102
|
+
old still current? False
|
|
103
|
+
at t0: ['Ada prefers dark roast coffee']
|
|
104
|
+
evidence: Standup 2026-03-01: Ada takes it dark roast.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
That is real output from running the block above. A longer, commented version covering
|
|
108
|
+
`recall_2hop`, `forget(hard=True)` and `stats()` is in
|
|
109
|
+
[`examples/quickstart.py`](examples/quickstart.py) — it needs no API key and runs in about a
|
|
110
|
+
second:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
$ python examples/quickstart.py
|
|
114
|
+
anatid schema v2 on duckdb 1.5.5, tenant 1, expand path: sql
|
|
115
|
+
before rebuild: bm25 stale=True, rows waiting=3
|
|
116
|
+
|
|
117
|
+
recall(query + embedding + seed): arms=('vector', 'text', 'graph') stale=False
|
|
118
|
+
[1] 0.0487 'Ada prefers dark roast coffee' via vector+text+graph about=['Ada', 'coffee']
|
|
119
|
+
[2] 0.0325 'The ingest service is maintained by Bo' via vector+graph about=['ingest service', 'Bo']
|
|
120
|
+
[3] 0.0320 'Ada leads Project Kestrel' via vector+graph about=['Ada', 'Kestrel']
|
|
121
|
+
|
|
122
|
+
recall_2hop('Ada'):
|
|
123
|
+
'The ingest service is maintained by Bo'
|
|
124
|
+
'Ada leads Project Kestrel'
|
|
125
|
+
'Ada prefers dark roast coffee'
|
|
126
|
+
|
|
127
|
+
supersede: old is_current=False valid_to=2026-03-31 09:00:00 -> new 'Ada switched to decaf'
|
|
128
|
+
|
|
129
|
+
as_of(day 1) : ['The ingest service is maintained by Bo', 'Ada leads Project Kestrel', 'Ada prefers dark roast coffee']
|
|
130
|
+
current : ['Ada switched to decaf', 'The ingest service is maintained by Bo', 'Ada leads Project Kestrel']
|
|
131
|
+
|
|
132
|
+
provenance(depth=1, writers=['agent-2', 'agent-1']):
|
|
133
|
+
current 'Ada switched to decaf' (by agent-2)
|
|
134
|
+
closed 'Ada prefers dark roast coffee' (by agent-1)
|
|
135
|
+
source: 'Standup 2026-03-01: Ada is leading Project Kestrel; she take'...
|
|
136
|
+
|
|
137
|
+
forget(hard=True): rows_removed=5 about_edges=2 supersedes_edges=1 audit_rows_deleted=1
|
|
138
|
+
|
|
139
|
+
stats: memories=3 current=2 entities=5 about=6 relates=2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Note the third line of that output. `Bo` is never mentioned in the query and is not connected to
|
|
143
|
+
`Ada` directly — the graph arm reached it in two hops (`Ada → Kestrel → ingest service`), which is
|
|
144
|
+
the thing a vector store cannot do for you.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## The verbs
|
|
149
|
+
|
|
150
|
+
| verb | what it does |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `remember(content, entities=[...])` | write a fact and the ABOUT edges that make it reachable |
|
|
153
|
+
| `recall(query, embedding=, seed_entity=)` | hybrid retrieval: cosine + BM25 + 2-hop graph, fused with RRF |
|
|
154
|
+
| `recall_2hop(seed)` / `context(entity)` | pure graph recall; `context` defaults to 0 hops |
|
|
155
|
+
| `supersede(old_id, content)` | replace a belief, keeping the old one closed and linked |
|
|
156
|
+
| `reinforce(id)` / `prune(...)` | strengthen what gets used, drop what does not |
|
|
157
|
+
| `forget(id, hard=False)` | stop believing (audit trail kept) or erase completely |
|
|
158
|
+
| `as_of(t)` | every read, as the database saw the world at `t` |
|
|
159
|
+
| `provenance(id)` | the supersession chain, the raw episodes, and every writer involved |
|
|
160
|
+
| `relate(a, b)` / `upsert_entity` / `episode` | the graph and evidence primitives underneath |
|
|
161
|
+
|
|
162
|
+
Each **write** verb is exactly one DuckDB transaction. Reads (`recall`, `recall_2hop`, `context`,
|
|
163
|
+
`get`, `provenance`, `stats`) run their statements outside an explicit transaction, so a
|
|
164
|
+
concurrent commit can land between a recall's arms and its hydration step — wrap the call in
|
|
165
|
+
`db.transaction()` yourself if you need one snapshot. `prune` is a query plus one transaction per
|
|
166
|
+
memory it forgets, so a failure part-way leaves the earlier deletions committed; take its
|
|
167
|
+
`dry_run` list first. Every verb takes `now=`/`as_of=` so tests are
|
|
168
|
+
deterministic. There are function forms too (`from anatid.verbs import remember`), and
|
|
169
|
+
`db.connection` hands you the raw DuckDB cursor whenever you want to write SQL — your memory is
|
|
170
|
+
just tables, joinable against your Parquet and CSV in place.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Why DuckDB, with numbers
|
|
175
|
+
|
|
176
|
+
Phase 0 was a benchmark, not a design document: 1,000,000 memories, 2.3M edges, 10 tenants, four
|
|
177
|
+
engines, the same operations with identical semantics, checked against a pure-Python oracle.
|
|
178
|
+
|
|
179
|
+
**2-hop recall (the query agent memory hits hardest), 1,000 queries, single thread:**
|
|
180
|
+
|
|
181
|
+
| engine | p50 | p95 | load | on disk | concurrent reads |
|
|
182
|
+
|---|---:|---:|---:|---:|---:|
|
|
183
|
+
| **DuckDB + C++ CSR extension** | **2.04 ms** | **3.07 ms** | 4.8 s | 481 MiB | 825 R1/s |
|
|
184
|
+
| **DuckDB, plain SQL** | **2.88 ms** | **3.50 ms** | 4.6 s | 434 MiB | 583 R1/s |
|
|
185
|
+
| LadybugDB 0.20.2 (tuned) | 7.35 ms | 28.73 ms | 16.2 s | 1,158 MiB | 147 R1/s |
|
|
186
|
+
|
|
187
|
+
- The kill criterion was "abandon DuckDB if it is more than **5x slower**". It came in at **0.39x**
|
|
188
|
+
(SQL) and **0.28x** (extension). At p95 it is 0.12x and 0.11x.
|
|
189
|
+
- **All three engines returned identical result id-lists** on 1,000 oracle-checked queries and on
|
|
190
|
+
200 post-write verify queries. Fast and wrong is not interesting.
|
|
191
|
+
- LadybugDB's number is its *best of six* Cypher formulations across two thread settings. The
|
|
192
|
+
naive formulation was 17x slower; reporting that one would have been dishonest.
|
|
193
|
+
- Where DuckDB loses: hybrid recall is ~20% slower (16.4 ms vs 20.0 ms p50 — no engine had an ANN
|
|
194
|
+
index, so this is a scan-speed comparison), and concurrent readers cost DuckDB writers real
|
|
195
|
+
throughput (397 W1/s with writers alone, 152-189 W1/s with 2 readers added). LadybugDB with
|
|
196
|
+
`enable_multi_writes=True` commits more writes per second than DuckDB does.
|
|
197
|
+
|
|
198
|
+
Full tables — every phase, p50/p95/p99, mixed workload, concurrency, correctness, and nine explicit
|
|
199
|
+
limitations of the benchmark itself — are in [`docs/benchmarks.md`](docs/benchmarks.md). The raw
|
|
200
|
+
JSON with per-operation latency arrays is in `spike/results/`.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## OpenAI Agents SDK: memory the agent proposes and a human approves
|
|
205
|
+
|
|
206
|
+
The [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) already has everything
|
|
207
|
+
needed for human-in-the-loop: `needs_approval=True` on a `function_tool`,
|
|
208
|
+
`RunResult.interruptions`, a serializable `RunState`, `state.approve()` / `state.reject()`. It also
|
|
209
|
+
has a `Session` protocol for conversation history, with SQLite, SQLAlchemy and Redis backends.
|
|
210
|
+
|
|
211
|
+
What it does not have is a **DuckDB session**, **graph memory**, or **approval-gated memory
|
|
212
|
+
writes**. As far as we can establish, no open-source project combines all four of the Agents SDK,
|
|
213
|
+
DuckDB, a graph store, and human approval on memory writes. anatid is the missing piece, and it
|
|
214
|
+
rebuilds none of the SDK's machinery:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from agents import Agent, Runner
|
|
218
|
+
from anatid import Anatid
|
|
219
|
+
from anatid.integrations.openai_agents import AnatidSession, create_memory_tools
|
|
220
|
+
|
|
221
|
+
db = Anatid.open("agent.anatid", tenant=1)
|
|
222
|
+
session = AnatidSession("conv-1", db) # conversation history, same file as the graph
|
|
223
|
+
tools = create_memory_tools(db, session=session) # 3 read tools + 3 write tools
|
|
224
|
+
|
|
225
|
+
agent = Agent(name="assistant", tools=tools)
|
|
226
|
+
result = await Runner.run(agent, "Ada switched to decaf, remember that", session=session)
|
|
227
|
+
|
|
228
|
+
while result.interruptions: # writes stop here; reads never do
|
|
229
|
+
state = result.to_state()
|
|
230
|
+
for item in result.interruptions:
|
|
231
|
+
print(item.tool_name, item.raw_item.arguments) # "anatid_remember" {"content": ...}
|
|
232
|
+
state.approve(item) # or state.reject(item)
|
|
233
|
+
result = await Runner.run(agent, state, session=session)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- **Reads are not gated; writes are.** `anatid_remember`, `anatid_supersede` and `anatid_forget`
|
|
237
|
+
carry `needs_approval`; `anatid_recall`, `anatid_context` and `anatid_provenance` do not. Nothing
|
|
238
|
+
touches the database until someone approves.
|
|
239
|
+
- **The policy is a callable.** `approve_low_risk()` auto-approves small, ordinary writes and still
|
|
240
|
+
stops for hard deletes. `anatid_forget(hard=True)` requires approval regardless unless you opt
|
|
241
|
+
out explicitly — a hard forget removes the row, its edges, its embedding and its provenance, and
|
|
242
|
+
that is not a decision to delegate to a model.
|
|
243
|
+
- **Approval can happen later, elsewhere.** `RunStateStore(db)` parks the SDK's serialized
|
|
244
|
+
`RunState` in the same anatid file, so an interrupted run can be reviewed and resumed minutes or
|
|
245
|
+
days later by another process — a review queue, not a blocking prompt.
|
|
246
|
+
- **History and knowledge are joinable**, because `AnatidSession` writes turns into a table in the
|
|
247
|
+
same DuckDB file as the memory graph. `await session.entities_mentioned()` is one SQL join
|
|
248
|
+
against `entities`, not two round-trips to two different stores; `memories_written_here()` tells
|
|
249
|
+
you what this conversation actually committed to memory.
|
|
250
|
+
|
|
251
|
+
## MCP server
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pip install "anatid[mcp]"
|
|
255
|
+
anatid-mcp --db memory.anatid # stdio; point Claude Desktop, Claude Code or Cursor at it
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The memory verbs over the Model Context Protocol, so any MCP client gets persistent, bitemporal,
|
|
259
|
+
graph-shaped memory: `remember`, `relate`, `supersede`, `reinforce`, `forget`, `prune` and
|
|
260
|
+
`rebuild_fts_index` on the write side, `recall`, `context`, `get`, `provenance` and `stats` on the
|
|
261
|
+
read side. (Those are the MCP tool names; the `anatid_`-prefixed names belong to the Agents SDK
|
|
262
|
+
integration above.) `--read-only` registers the read tools only.
|
|
263
|
+
|
|
264
|
+
There is also one deliberate escape hatch: a `sql` tool for the questions the verbs do not answer
|
|
265
|
+
("how many memories per kind?", "show me the audit trail"). It is read-only, and enforced in three
|
|
266
|
+
layers by DuckDB rather than by a regex over the query text: DuckDB's own statement classifier
|
|
267
|
+
(only SELECT/EXPLAIN, and *every* statement in the text must pass), a scan of DuckDB's parse tree
|
|
268
|
+
for file-reading functions and for base-table names that are not plain identifiers (DuckDB's
|
|
269
|
+
replacement scan makes `SELECT * FROM '/etc/passwd.csv'` an ordinary SELECT), and execution inside
|
|
270
|
+
`BEGIN TRANSACTION READ ONLY` on a private cursor that is always rolled back. DuckDB will not give
|
|
271
|
+
a second read-only *connection* to a file the process already holds, so the read-only transaction
|
|
272
|
+
is the mechanism. `PRAGMA create_fts_index(...)`, which expands into DDL at bind time, is rejected
|
|
273
|
+
on what it really is. Turn it off with `--no-sql-tool`.
|
|
274
|
+
|
|
275
|
+
`from anatid.integrations.mcp import build_server` if you want to embed the server in your own
|
|
276
|
+
process.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## What this is not, yet
|
|
281
|
+
|
|
282
|
+
Every item here is measured or documented in the source, not a guess. If you hit one of these, you
|
|
283
|
+
were warned; if you hit something that is *not* here, that is a bug and we want the report.
|
|
284
|
+
|
|
285
|
+
- **No ANN index.** The vector arm is a brute-force `array_cosine_similarity` scan, because DuckDB
|
|
286
|
+
ships no ANN index. The cost is linear in **one tenant's** row count: measured at 64 dims on the
|
|
287
|
+
spike hardware, 2.0 ms p50 with 10k memories in the tenant, 8.6 ms at 100k (an independent run
|
|
288
|
+
of the same measurement got 11.4 ms) and 23.3 ms at 1M. `BRUTE_FORCE_CEILING = 100_000` is
|
|
289
|
+
documented and *not enforced* — you are already paying ~9-11 ms per recall *at* that ceiling,
|
|
290
|
+
and past it this is the wrong tool. An owned ANN index is the headline item of v1.0.
|
|
291
|
+
- **The full-text index is not incremental.** DuckDB's `fts` index does not see rows inserted after
|
|
292
|
+
it was built. anatid does not paper over this: `rebuild_fts_index()` is explicit, `fts_status()`
|
|
293
|
+
tells you how stale you are, and every `recall()` result carries `.bm25_stale` and
|
|
294
|
+
`.pending_fts_rows` (with `on_stale_fts="error"` if you would rather raise). The staleness window
|
|
295
|
+
is the gap between your rebuilds, and it is yours to choose.
|
|
296
|
+
- **One writing process per file.** That is DuckDB's model, and it is enforced by the engine: a
|
|
297
|
+
second read-write process cannot even open the file (`IO Error: Could not set lock on file ...:
|
|
298
|
+
Conflicting lock is held`). Many threads *in that process* write concurrently and appends never
|
|
299
|
+
conflict (0 errors in a 30 s, 6-thread benchmark with no retry logic), but multi-process writes
|
|
300
|
+
are not something anatid provides.
|
|
301
|
+
- **Snapshot isolation, not serializable.** Two concurrent updates to the same row abort the
|
|
302
|
+
second with a retryable `ConflictError`. Retrying is your call, because only you know whether
|
|
303
|
+
re-reading first changes the write.
|
|
304
|
+
- **Tenant isolation is file-per-tenant.** DuckDB has no row-level or schema-level access control.
|
|
305
|
+
A `tenant_id` column is *scoping*; the real boundary is one file per tenant via `DatabasePool`,
|
|
306
|
+
enforced by the filesystem. Raw SQL through `db.connection` sees every tenant in the file, and
|
|
307
|
+
the docstrings say so.
|
|
308
|
+
- **Time travel is our filter, not the engine's.** DuckDB has no `AS OF SYSTEM TIME`. `as_of()`
|
|
309
|
+
is a `WHERE` clause over `valid_from`/`valid_to`/`tx_from`/`tx_to`. It reaches back exactly as
|
|
310
|
+
far as the rows still in the table — a hard purge is gone from every as-of view too, which is
|
|
311
|
+
the point of a hard purge.
|
|
312
|
+
- **The CSR extension has sharp edges.** It needs dense per-tenant entity ids (anatid's default
|
|
313
|
+
63-bit time-ordered ids are not dense), it is rebuilt in full rather than incrementally, and any
|
|
314
|
+
`relate()` marks it stale — at which point recall silently falls back to the SQL path, which
|
|
315
|
+
returns identical rows. It is an accelerator, off by default.
|
|
316
|
+
- **No Cypher yet.** v0.2. Today the API is the verbs above plus SQL.
|
|
317
|
+
- **v0.1.** The API may still move. Pin the version.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Documentation
|
|
322
|
+
|
|
323
|
+
- [`docs/architecture.md`](docs/architecture.md) — storage layout, the derived CSR and how it stays
|
|
324
|
+
MVCC-correct, the isolation contract, the temporal model, the recall pipeline.
|
|
325
|
+
- [`docs/benchmarks.md`](docs/benchmarks.md) — Phase 0 method, every result, and what the benchmark
|
|
326
|
+
does not tell you.
|
|
327
|
+
- [`docs/roadmap.md`](docs/roadmap.md) — v0.2 (Cypher subset, Graphiti/Cognee drivers, Node
|
|
328
|
+
bindings), v0.5 (production operation), v1.0 (ANN index, persistent CSR, duckdb-wasm).
|
|
329
|
+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to build it, what we care about in a change, and the
|
|
330
|
+
third-party notices.
|
|
331
|
+
- `spike/` — the Phase 0 evidence, kept read-only.
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MIT. Copyright (c) 2026 anatid contributors. Code adapted from DuckDB (MIT) or from Kuzu /
|
|
336
|
+
LadybugDB (MIT, Copyright 2022-2025 Kùzu Inc.) carries its original notice alongside ours; see
|
|
337
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md#third-party-notices).
|