aegisdb-langgraph 0.8.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.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: aegisdb-langgraph
3
+ Version: 0.8.0
4
+ Summary: Use AegisDB as a LangGraph long-term-memory store
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/d4n-larsson/aegisdb
7
+ Project-URL: Repository, https://github.com/d4n-larsson/aegisdb
8
+ Project-URL: Issues, https://github.com/d4n-larsson/aegisdb/issues
9
+ Keywords: aegisdb,langgraph,langchain,agent-memory,store
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: aegisdb
13
+ Requires-Dist: langgraph-checkpoint<5,>=2
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7; extra == "dev"
16
+ Requires-Dist: langgraph>=0.2; extra == "dev"
17
+
18
+ # aegisdb-langgraph — AegisDB as a LangGraph store
19
+
20
+ Use [AegisDB](https://github.com/d4n-larsson/aegisdb) as the long-term memory
21
+ behind a LangGraph agent.
22
+
23
+ ```bash
24
+ pip install aegisdb-langgraph
25
+ ```
26
+
27
+ ```python
28
+ from aegisdb_langgraph import AegisStore
29
+
30
+ store = AegisStore(host="127.0.0.1", port=9470, namespace="my-agent")
31
+
32
+ store.put(("users", "42"), "prefs", {"theme": "dark"})
33
+ store.get(("users", "42"), "prefs").value # {"theme": "dark"}
34
+ store.search(("users",), query="dark") # BM25 over stored values
35
+ store.list_namespaces() # [("users", "42")]
36
+ ```
37
+
38
+ …and inside a graph, the way LangGraph injects it:
39
+
40
+ ```python
41
+ graph = builder.compile(store=AegisStore(namespace="my-agent"))
42
+
43
+ def remember(state, *, store):
44
+ store.put(("users", state["user"]), "last", {"seen": "hello"})
45
+ ```
46
+
47
+ `BaseStore` funnels `get` / `put` / `search` / `delete` / `list_namespaces`
48
+ through one abstract `batch(ops)`, so that is all this implements — the
49
+ concrete methods come from the base class and cannot drift from it. `aput`,
50
+ `aget` and friends work too, on a worker thread.
51
+
52
+ ## The mapping
53
+
54
+ LangGraph addresses items by a hierarchical `namespace: tuple[str, ...]` plus a
55
+ `key`, and searches by namespace *prefix*. AegisDB has a flat `agent_id` that is
56
+ an isolation boundary rather than a path, so the hierarchy is carried in tags:
57
+
58
+ | | |
59
+ |---|---|
60
+ | `agent_id` | the store's own `namespace=` — one tenant, isolated by the server |
61
+ | tags | a marker, a hash of `(namespace, key)`, and a hash of **every prefix** of the namespace |
62
+ | `data` | JSON: `{"ns": [...], "key": ..., "value": {...}}` |
63
+
64
+ A tag per prefix is what makes a prefix search an index lookup rather than a
65
+ scan. Tags are hashed because AegisDB caps one at 64 bytes (and 32 per record)
66
+ while a namespace or key can be longer; the readable copy lives in `data`,
67
+ which is also what makes `list_namespaces` possible.
68
+
69
+ The digest is length-prefixed per segment, so `("a", "bc")` and `("ab", "c")`
70
+ cannot collide — joining on a separator would make them the same string, and
71
+ two namespaces sharing a tag is a cross-namespace read.
72
+
73
+ **Namespace depth is capped at 29.** Marker + key + one tag per prefix has to
74
+ fit in AegisDB's 32-tag ceiling. Deeper raises `ValueError` naming the limit,
75
+ rather than an `INVALID_REQUEST` that says nothing about namespaces.
76
+
77
+ ## Three things it does not do
78
+
79
+ **TTL is refused, not ignored.** AegisDB expires only working memory, which is
80
+ a per-session ring buffer rather than a store, so a TTL here would never fire.
81
+ Silently never expiring something a caller asked to expire is a retention
82
+ surprise. Use the server's `forget` op, which ages records out by importance
83
+ and recency — and which the LangGraph API has no way to express.
84
+
85
+ **No vector indexing.** `query=` runs the server's BM25 index: no embedding
86
+ provider needed, and exact tokens (an error string, a flag, an identifier)
87
+ match well, with the server's relevance score carried through to
88
+ `SearchItem.score`. `index=` is accepted and ignored, because honouring it
89
+ would mean this package owning an embeddings function. A server started with
90
+ `--no-lexical-index` raises a `RuntimeError` naming that flag when a query was
91
+ actually passed — a queryless search keeps working, and any other `NOT_READY`
92
+ is re-raised untouched rather than blamed on an index it never used.
93
+
94
+ **`filter=` is applied client-side.** AegisDB cannot filter on arbitrary JSON
95
+ fields, so candidates are pulled back and matched here — using *LangGraph's
96
+ own* `_compare_values`, so the semantics are identical to `InMemoryStore`
97
+ rather than a second implementation that drifts. It inherits that function's
98
+ limits too: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte` are supported and `$in`
99
+ is not, in both stores alike.
100
+
101
+ Because filtering happens here, paging does too — asking the server for an
102
+ `offset` would skip rows before they were filtered, so page 2 would silently
103
+ omit matches. An **unfiltered** search does page on the server, so a deep page
104
+ stays a small read.
105
+
106
+ `search_scan_limit` (default 1000) bounds how many candidates a filtered search
107
+ pulls back; `list_namespaces` is bounded the same way, since AegisDB indexes
108
+ tags but cannot enumerate them. It is **clamped to 1000**, because the server
109
+ clamps `top_k` there and says nothing — a larger value would read as "scans
110
+ more" while changing nothing.
111
+
112
+ **Past that bound the answer is short and cannot say so.** `search` and
113
+ `list_namespaces` return plain lists, with nowhere to put a flag. A store
114
+ holding more than `search_scan_limit` items can therefore be missing matches
115
+ from a filtered search, or namespaces from a listing. Stated here because it
116
+ cannot be signalled there.
117
+
118
+ ## Isolation
119
+
120
+ `namespace=` is the AegisDB namespace, enforced by the server. Two stores with
121
+ different ones cannot see each other's items even though they share a server —
122
+ so one AegisDB instance can back many agents, and the LangGraph hierarchy lives
123
+ inside each.
124
+
125
+ Passing your own `client=` makes *its* `agent_id` authoritative, since that is
126
+ what the server enforces; giving both and disagreeing raises, rather than
127
+ reporting one namespace while writing into another.
128
+
129
+ ## Concurrency
130
+
131
+ **One store is one connection, and `batch` holds a lock.** The client owns a
132
+ socket and is not thread-safe, while `abatch` runs on a worker thread and
133
+ LangGraph's sync runner executes a superstep's tasks on a pool — so without
134
+ serialising, two nodes touching the store interleave on the same socket and one
135
+ reads the other's response. Concurrent nodes therefore queue; for real
136
+ parallelism, build a store per worker.
137
+
138
+ Across *processes* nothing here can help: `put` is a read-modify-write and
139
+ AegisDB has no upsert, so two racing writers can leave two records for one key.
140
+ Reads pick deterministically (lowest id) so the store keeps answering
141
+ consistently if that happens.
142
+
143
+ ## Tests
144
+
145
+ ```bash
146
+ make langgraph-test # from the repo root
147
+ ```
148
+
149
+ Every behaviour meant to match the reference implementation is asserted by
150
+ running the same case against `InMemoryStore` and comparing — an assertion
151
+ written from a reading of the contract would only encode that reading. The
152
+ suite also compiles a real graph with `store=` and lets LangGraph inject it,
153
+ which is the part that would break if the class satisfied the ABC but not the
154
+ runtime's expectations of it.
@@ -0,0 +1,137 @@
1
+ # aegisdb-langgraph — AegisDB as a LangGraph store
2
+
3
+ Use [AegisDB](https://github.com/d4n-larsson/aegisdb) as the long-term memory
4
+ behind a LangGraph agent.
5
+
6
+ ```bash
7
+ pip install aegisdb-langgraph
8
+ ```
9
+
10
+ ```python
11
+ from aegisdb_langgraph import AegisStore
12
+
13
+ store = AegisStore(host="127.0.0.1", port=9470, namespace="my-agent")
14
+
15
+ store.put(("users", "42"), "prefs", {"theme": "dark"})
16
+ store.get(("users", "42"), "prefs").value # {"theme": "dark"}
17
+ store.search(("users",), query="dark") # BM25 over stored values
18
+ store.list_namespaces() # [("users", "42")]
19
+ ```
20
+
21
+ …and inside a graph, the way LangGraph injects it:
22
+
23
+ ```python
24
+ graph = builder.compile(store=AegisStore(namespace="my-agent"))
25
+
26
+ def remember(state, *, store):
27
+ store.put(("users", state["user"]), "last", {"seen": "hello"})
28
+ ```
29
+
30
+ `BaseStore` funnels `get` / `put` / `search` / `delete` / `list_namespaces`
31
+ through one abstract `batch(ops)`, so that is all this implements — the
32
+ concrete methods come from the base class and cannot drift from it. `aput`,
33
+ `aget` and friends work too, on a worker thread.
34
+
35
+ ## The mapping
36
+
37
+ LangGraph addresses items by a hierarchical `namespace: tuple[str, ...]` plus a
38
+ `key`, and searches by namespace *prefix*. AegisDB has a flat `agent_id` that is
39
+ an isolation boundary rather than a path, so the hierarchy is carried in tags:
40
+
41
+ | | |
42
+ |---|---|
43
+ | `agent_id` | the store's own `namespace=` — one tenant, isolated by the server |
44
+ | tags | a marker, a hash of `(namespace, key)`, and a hash of **every prefix** of the namespace |
45
+ | `data` | JSON: `{"ns": [...], "key": ..., "value": {...}}` |
46
+
47
+ A tag per prefix is what makes a prefix search an index lookup rather than a
48
+ scan. Tags are hashed because AegisDB caps one at 64 bytes (and 32 per record)
49
+ while a namespace or key can be longer; the readable copy lives in `data`,
50
+ which is also what makes `list_namespaces` possible.
51
+
52
+ The digest is length-prefixed per segment, so `("a", "bc")` and `("ab", "c")`
53
+ cannot collide — joining on a separator would make them the same string, and
54
+ two namespaces sharing a tag is a cross-namespace read.
55
+
56
+ **Namespace depth is capped at 29.** Marker + key + one tag per prefix has to
57
+ fit in AegisDB's 32-tag ceiling. Deeper raises `ValueError` naming the limit,
58
+ rather than an `INVALID_REQUEST` that says nothing about namespaces.
59
+
60
+ ## Three things it does not do
61
+
62
+ **TTL is refused, not ignored.** AegisDB expires only working memory, which is
63
+ a per-session ring buffer rather than a store, so a TTL here would never fire.
64
+ Silently never expiring something a caller asked to expire is a retention
65
+ surprise. Use the server's `forget` op, which ages records out by importance
66
+ and recency — and which the LangGraph API has no way to express.
67
+
68
+ **No vector indexing.** `query=` runs the server's BM25 index: no embedding
69
+ provider needed, and exact tokens (an error string, a flag, an identifier)
70
+ match well, with the server's relevance score carried through to
71
+ `SearchItem.score`. `index=` is accepted and ignored, because honouring it
72
+ would mean this package owning an embeddings function. A server started with
73
+ `--no-lexical-index` raises a `RuntimeError` naming that flag when a query was
74
+ actually passed — a queryless search keeps working, and any other `NOT_READY`
75
+ is re-raised untouched rather than blamed on an index it never used.
76
+
77
+ **`filter=` is applied client-side.** AegisDB cannot filter on arbitrary JSON
78
+ fields, so candidates are pulled back and matched here — using *LangGraph's
79
+ own* `_compare_values`, so the semantics are identical to `InMemoryStore`
80
+ rather than a second implementation that drifts. It inherits that function's
81
+ limits too: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte` are supported and `$in`
82
+ is not, in both stores alike.
83
+
84
+ Because filtering happens here, paging does too — asking the server for an
85
+ `offset` would skip rows before they were filtered, so page 2 would silently
86
+ omit matches. An **unfiltered** search does page on the server, so a deep page
87
+ stays a small read.
88
+
89
+ `search_scan_limit` (default 1000) bounds how many candidates a filtered search
90
+ pulls back; `list_namespaces` is bounded the same way, since AegisDB indexes
91
+ tags but cannot enumerate them. It is **clamped to 1000**, because the server
92
+ clamps `top_k` there and says nothing — a larger value would read as "scans
93
+ more" while changing nothing.
94
+
95
+ **Past that bound the answer is short and cannot say so.** `search` and
96
+ `list_namespaces` return plain lists, with nowhere to put a flag. A store
97
+ holding more than `search_scan_limit` items can therefore be missing matches
98
+ from a filtered search, or namespaces from a listing. Stated here because it
99
+ cannot be signalled there.
100
+
101
+ ## Isolation
102
+
103
+ `namespace=` is the AegisDB namespace, enforced by the server. Two stores with
104
+ different ones cannot see each other's items even though they share a server —
105
+ so one AegisDB instance can back many agents, and the LangGraph hierarchy lives
106
+ inside each.
107
+
108
+ Passing your own `client=` makes *its* `agent_id` authoritative, since that is
109
+ what the server enforces; giving both and disagreeing raises, rather than
110
+ reporting one namespace while writing into another.
111
+
112
+ ## Concurrency
113
+
114
+ **One store is one connection, and `batch` holds a lock.** The client owns a
115
+ socket and is not thread-safe, while `abatch` runs on a worker thread and
116
+ LangGraph's sync runner executes a superstep's tasks on a pool — so without
117
+ serialising, two nodes touching the store interleave on the same socket and one
118
+ reads the other's response. Concurrent nodes therefore queue; for real
119
+ parallelism, build a store per worker.
120
+
121
+ Across *processes* nothing here can help: `put` is a read-modify-write and
122
+ AegisDB has no upsert, so two racing writers can leave two records for one key.
123
+ Reads pick deterministically (lowest id) so the store keeps answering
124
+ consistently if that happens.
125
+
126
+ ## Tests
127
+
128
+ ```bash
129
+ make langgraph-test # from the repo root
130
+ ```
131
+
132
+ Every behaviour meant to match the reference implementation is asserted by
133
+ running the same case against `InMemoryStore` and comparing — an assertion
134
+ written from a reading of the contract would only encode that reading. The
135
+ suite also compiles a real graph with `store=` and lets LangGraph inject it,
136
+ which is the part that would break if the class satisfied the ABC but not the
137
+ runtime's expectations of it.
@@ -0,0 +1,13 @@
1
+ """AegisDB as a LangGraph long-term-memory store.
2
+
3
+ from aegisdb_langgraph import AegisStore
4
+
5
+ store = AegisStore(host="127.0.0.1", port=9470, namespace="my-agent")
6
+ store.put(("users", "42"), "prefs", {"theme": "dark"})
7
+
8
+ See `store.py` for the mapping onto AegisDB and the three things it
9
+ deliberately does not do (TTL, vector indexing, server-side filtering).
10
+ """
11
+ from .store import MAX_NAMESPACE_DEPTH, AegisStore
12
+
13
+ __all__ = ["AegisStore", "MAX_NAMESPACE_DEPTH"]