memkernel 0.2.0__tar.gz → 0.3.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.
- {memkernel-0.2.0 → memkernel-0.3.0}/PKG-INFO +54 -9
- {memkernel-0.2.0 → memkernel-0.3.0}/README.md +51 -8
- {memkernel-0.2.0 → memkernel-0.3.0}/pyproject.toml +3 -1
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/__init__.py +1 -1
- memkernel-0.3.0/src/memkernel/adapters/__init__.py +3 -0
- memkernel-0.3.0/src/memkernel/adapters/rlmstudio.py +123 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/config.py +6 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/core/models.py +37 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/core/ports.py +36 -2
- memkernel-0.3.0/src/memkernel/core/services.py +797 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/factory.py +37 -1
- memkernel-0.3.0/src/memkernel/lifecycle.py +189 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/mcp/tools/memory.py +22 -7
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/retrieval/chunking.py +20 -0
- memkernel-0.3.0/src/memkernel/retrieval/reasoner.py +83 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/retrieval/search.py +31 -2
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/storage/chroma.py +10 -3
- memkernel-0.3.0/src/memkernel/storage/documents.py +185 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/wiki/sync.py +2 -1
- memkernel-0.2.0/src/memkernel/core/services.py +0 -485
- {memkernel-0.2.0 → memkernel-0.3.0}/LICENSE +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/NOTICE +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/api/app.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/api/routes/admin.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/mcp/server.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/mcp/tools/wiki.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/py.typed +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/wiki/__init__.py +0 -0
- {memkernel-0.2.0 → memkernel-0.3.0}/src/memkernel/wiki/pages.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memkernel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Code-aware memory server for AI coding copilots
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -22,6 +22,8 @@ Requires-Dist: rank-bm25 (>=0.2)
|
|
|
22
22
|
Requires-Dist: sentence-transformers (>=4.0,<6)
|
|
23
23
|
Requires-Dist: tree-sitter (>=0.22,<1)
|
|
24
24
|
Requires-Dist: tree-sitter-python (>=0.23)
|
|
25
|
+
Project-URL: Documentation, https://memkernel.dev
|
|
26
|
+
Project-URL: Homepage, https://memkernel.dev
|
|
25
27
|
Project-URL: Repository, https://github.com/gosha70/memkernel
|
|
26
28
|
Description-Content-Type: text/markdown
|
|
27
29
|
|
|
@@ -32,6 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
32
34
|
[](pyproject.toml)
|
|
33
35
|
[](LICENSE)
|
|
34
36
|
|
|
37
|
+
**Site:** [memkernel.dev](https://memkernel.dev) · **Package:** [pypi.org/project/memkernel](https://pypi.org/project/memkernel/)
|
|
38
|
+
|
|
35
39
|
Code-aware memory server for AI coding copilots.
|
|
36
40
|
|
|
37
41
|
MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory across sessions. When Claude compacts context or a session ends, project decisions, conventions, and code understanding are normally lost. MemKernel stores them in a local vector database and retrieves them on demand.
|
|
@@ -99,7 +103,7 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
99
103
|
| `pre-compact.py` | `PreCompact` | Retains a checkpoint episode, runs a consolidation pass |
|
|
100
104
|
| `post-compact.py` | `PostCompact` | Re-injects the checkpoint, long-term context and promotion candidates |
|
|
101
105
|
|
|
102
|
-
See the Prerequisites section inside each script for the Python environment requirement.
|
|
106
|
+
See the Prerequisites section inside each script for the Python environment requirement. Each script is a thin wrapper over [`MemoryLifecycle`](#embedding-memkernel-in-your-own-harness), so an in-process harness gets the same behaviour without the scripts.
|
|
103
107
|
|
|
104
108
|
---
|
|
105
109
|
|
|
@@ -112,8 +116,8 @@ See the Prerequisites section inside each script for the Python environment requ
|
|
|
112
116
|
| `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
|
|
113
117
|
| `forget(ref_id)` | Delete a memory from all indexes. |
|
|
114
118
|
| `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
|
|
115
|
-
| `consolidate(session_id?, limit?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). |
|
|
116
|
-
| `
|
|
119
|
+
| `consolidate(session_id?, limit?, synthesize?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`). |
|
|
120
|
+
| `ask(ref_id, question, mode?)` | Answer a question about one memory without loading it — for a `document` the full body is consulted. `mode`: `direct` \| `rag` \| `rlm` \| `auto`, passed to the reasoner unchanged; the built-in reasoner answers in `rag` mode with the best-matching chunks. Returns `answer`, `mode`, `citations`, `usage`. |
|
|
117
121
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
118
122
|
| `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
|
|
119
123
|
|
|
@@ -126,6 +130,7 @@ See the Prerequisites section inside each script for the Python environment requ
|
|
|
126
130
|
| `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
|
|
127
131
|
| `procedure` | long-term | How-to that worked: workflows, recipes, verified skills | 0.8 | Never |
|
|
128
132
|
| `code` | long-term | Function/class snapshots, API contracts | 0.6 | Never |
|
|
133
|
+
| `document` | long-term | Large artefacts (specs, transcripts, files): the body is stored and chunk-indexed, `get` returns a summary, `ask` answers from the indexed chunks (local reasoner) or the body (RLM Studio) | 0.6 | Never |
|
|
129
134
|
| `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
|
|
130
135
|
| `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
|
|
131
136
|
| `task` | working | The current goal, plan state, open questions | 0.5 | 24 hours |
|
|
@@ -138,7 +143,8 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
138
143
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
139
144
|
3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
|
|
140
145
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
141
|
-
5. **
|
|
146
|
+
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
147
|
+
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
142
148
|
|
|
143
149
|
### Quick examples
|
|
144
150
|
|
|
@@ -164,6 +170,10 @@ retain(content="Use arq, not Celery", type="decision", supersedes="<old decision
|
|
|
164
170
|
get(ref_id="<ref_id from recall>")
|
|
165
171
|
|
|
166
172
|
forget(ref_id="<ref_id>")
|
|
173
|
+
|
|
174
|
+
retain(content=<the whole spec>, type="document", source="specs/auth/spec.md")
|
|
175
|
+
|
|
176
|
+
ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?", mode="auto")
|
|
167
177
|
```
|
|
168
178
|
|
|
169
179
|
---
|
|
@@ -185,6 +195,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
|
|
|
185
195
|
| `MEMKERNEL_PREVIEW_LENGTH` | `200` | Max characters in the `preview` field of `ArtifactRef` |
|
|
186
196
|
| `MEMKERNEL_CHUNK_SIZE` | `1500` | Max characters per AST chunk (code memories) |
|
|
187
197
|
| `MEMKERNEL_CHUNK_OVERLAP` | `200` | Character overlap between token-based fallback chunks |
|
|
198
|
+
| `MEMKERNEL_DOCUMENT_STORE` | `local` | Where `document` bodies live (`local` = the project's own ChromaDB collection) |
|
|
199
|
+
| `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
|
|
200
|
+
| `MEMKERNEL_REASONER` | `local` | `ReasonerPort` behind `ask`: `local` (hybrid search over chunks, no LLM) or `rlmstudio` (falls back to `local` when `rlmstudio` is not importable) |
|
|
201
|
+
| `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
|
|
188
202
|
| `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
|
|
189
203
|
| `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
|
|
190
204
|
| `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
|
|
@@ -237,6 +251,35 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
237
251
|
|
|
238
252
|
`build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
|
|
239
253
|
|
|
254
|
+
### The session protocol as a component
|
|
255
|
+
|
|
256
|
+
`MemoryLifecycle` packages what the hook scripts do, so a harness calls four methods instead of re-implementing the protocol. Every method returns markdown ready for injection and does nothing but talk to the service:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from memkernel.factory import build_service
|
|
260
|
+
from memkernel.lifecycle import MemoryLifecycle
|
|
261
|
+
|
|
262
|
+
memory = MemoryLifecycle(build_service())
|
|
263
|
+
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
264
|
+
...
|
|
265
|
+
memory.on_pre_compact(summary) # checkpoint episode + consolidation, before the context is compacted
|
|
266
|
+
recovered = memory.on_post_compact() # what to re-inject after compaction
|
|
267
|
+
memory.on_session_end(summary) # final checkpoint + consolidation
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Documents and `ask`
|
|
271
|
+
|
|
272
|
+
`retain(type="document")` keeps large artefacts out of the context window: the body goes to the document store (built-in: the project's own ChromaDB collection), its chunks are indexed for hybrid search, and the memkernel record is a summary. `ask(ref_id, question, mode)` answers through a `ReasonerPort`:
|
|
273
|
+
|
|
274
|
+
- `MEMKERNEL_REASONER=local` (default) — no LLM: the question is matched against the document's chunks with dense + BM25 + RRF and the best chunks come back as the answer, `mode="rag"`.
|
|
275
|
+
- `MEMKERNEL_REASONER=rlmstudio` — delegates to [rlm-studio](https://github.com/gosha70/rlm-studio)'s `interact(content, question, mode=mode)`, so `direct`, `rag`, `rlm` and `auto` select how the document is read (ADR-009). `rlm-studio` is not a memkernel dependency and is not on PyPI yet; install it from git into the same environment:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
pip install "rlm-studio @ git+https://github.com/gosha70/rlm-studio"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
When it is not importable, `build_service()` logs a warning and uses the local reasoner. With an external reasoner, `consolidate(synthesize=True)` also drafts long-term statements from the session's short-term memories.
|
|
282
|
+
|
|
240
283
|
## LLM wiki bridge
|
|
241
284
|
|
|
242
285
|
Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern (`index.md`, `log.md`, typed pages with cited sources — the same schema as `code-copilot-team/knowledge/wiki`).
|
|
@@ -281,7 +324,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
|
|
|
281
324
|
|
|
282
325
|
1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
|
|
283
326
|
2. Merge to `main` with CI green.
|
|
284
|
-
3. Tag and push: `git tag -a v0.
|
|
327
|
+
3. Tag and push: `git tag -a v0.3.0 -m "memkernel 0.3.0" && git push origin v0.3.0`.
|
|
285
328
|
|
|
286
329
|
The workflow refuses a tag that does not match the `pyproject.toml` version, builds the sdist and wheel, smoke-installs the wheel, and uploads through the `pypi` environment.
|
|
287
330
|
|
|
@@ -293,12 +336,14 @@ The workflow refuses a tag that does not match the `pyproject.toml` version, bui
|
|
|
293
336
|
src/memkernel/
|
|
294
337
|
├── mcp/ # MCP server + tool definitions (memory tools, wiki tools)
|
|
295
338
|
├── core/ # Domain models (tiers, types), MemoryService, ports (interfaces)
|
|
296
|
-
├──
|
|
297
|
-
├──
|
|
339
|
+
├── adapters/ # Optional external systems (RLM Studio reasoner), imported lazily
|
|
340
|
+
├── storage/ # ChromaDB adapter, local document store
|
|
341
|
+
├── retrieval/ # AST/token chunking, embeddings, hybrid search (dense + BM25 + RRF + rescoring), local reasoner
|
|
298
342
|
├── memory/ # Lifecycle: TTL/decay policies, retention, deduplication
|
|
299
343
|
├── wiki/ # LLM-wiki bridge: page model, export, import
|
|
300
344
|
├── api/ # FastAPI admin endpoints
|
|
301
345
|
├── factory.py # build_service() — the supported embedding entry point
|
|
346
|
+
├── lifecycle.py # MemoryLifecycle — the session protocol as a component
|
|
302
347
|
└── config.py # Pydantic settings
|
|
303
348
|
```
|
|
304
349
|
|
|
@@ -315,7 +360,7 @@ MemKernel is the memory component of a small family of projects by the same auth
|
|
|
315
360
|
| Project | Role | Relationship |
|
|
316
361
|
|---|---|---|
|
|
317
362
|
| [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
|
|
318
|
-
| [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer
|
|
363
|
+
| [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
|
|
319
364
|
| [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
|
|
320
365
|
| [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
|
|
321
366
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](pyproject.toml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
**Site:** [memkernel.dev](https://memkernel.dev) · **Package:** [pypi.org/project/memkernel](https://pypi.org/project/memkernel/)
|
|
9
|
+
|
|
8
10
|
Code-aware memory server for AI coding copilots.
|
|
9
11
|
|
|
10
12
|
MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory across sessions. When Claude compacts context or a session ends, project decisions, conventions, and code understanding are normally lost. MemKernel stores them in a local vector database and retrieves them on demand.
|
|
@@ -72,7 +74,7 @@ Copy the hook scripts from [`templates/hooks/`](templates/hooks/) to `.claude/ho
|
|
|
72
74
|
| `pre-compact.py` | `PreCompact` | Retains a checkpoint episode, runs a consolidation pass |
|
|
73
75
|
| `post-compact.py` | `PostCompact` | Re-injects the checkpoint, long-term context and promotion candidates |
|
|
74
76
|
|
|
75
|
-
See the Prerequisites section inside each script for the Python environment requirement.
|
|
77
|
+
See the Prerequisites section inside each script for the Python environment requirement. Each script is a thin wrapper over [`MemoryLifecycle`](#embedding-memkernel-in-your-own-harness), so an in-process harness gets the same behaviour without the scripts.
|
|
76
78
|
|
|
77
79
|
---
|
|
78
80
|
|
|
@@ -85,8 +87,8 @@ See the Prerequisites section inside each script for the Python environment requ
|
|
|
85
87
|
| `get(ref_id)` | Fetch full content of a specific memory. Records an access. |
|
|
86
88
|
| `forget(ref_id)` | Delete a memory from all indexes. |
|
|
87
89
|
| `promote(ref_id, type, content?, importance?, tags?)` | Turn a short-term/working memory into a long-term one (you supply the distilled text). Keeps provenance via `promoted_from`. |
|
|
88
|
-
| `consolidate(session_id?, limit?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). |
|
|
89
|
-
| `
|
|
90
|
+
| `consolidate(session_id?, limit?, synthesize?)` | Purge expired short-term memories and list the ones most worth promoting (ranked by importance × how often they were recalled). `synthesize=true` also asks an external reasoner to draft long-term statements (`suggested`). |
|
|
91
|
+
| `ask(ref_id, question, mode?)` | Answer a question about one memory without loading it — for a `document` the full body is consulted. `mode`: `direct` \| `rag` \| `rlm` \| `auto`, passed to the reasoner unchanged; the built-in reasoner answers in `rag` mode with the best-matching chunks. Returns `answer`, `mode`, `citations`, `usage`. |
|
|
90
92
|
| `wiki_export(wiki_dir?)` | Compile long-term memories without a wiki page into draft markdown pages (`index.md`, `log.md` updated). |
|
|
91
93
|
| `wiki_import(wiki_dir?)` | Index curated wiki pages as long-term memories (idempotent; changed pages supersede). |
|
|
92
94
|
|
|
@@ -99,6 +101,7 @@ See the Prerequisites section inside each script for the Python environment requ
|
|
|
99
101
|
| `fact` | long-term | Durable project facts: build commands, environments, contracts | 0.7 | Never |
|
|
100
102
|
| `procedure` | long-term | How-to that worked: workflows, recipes, verified skills | 0.8 | Never |
|
|
101
103
|
| `code` | long-term | Function/class snapshots, API contracts | 0.6 | Never |
|
|
104
|
+
| `document` | long-term | Large artefacts (specs, transcripts, files): the body is stored and chunk-indexed, `get` returns a summary, `ask` answers from the indexed chunks (local reasoner) or the body (RLM Studio) | 0.6 | Never |
|
|
102
105
|
| `episode` | short-term | Session events, compaction checkpoints | 0.4 | 30 days, extended by every recall |
|
|
103
106
|
| `observation` | short-term | Findings, tool outputs, things noticed | 0.3 | 7 days, extended by every recall |
|
|
104
107
|
| `task` | working | The current goal, plan state, open questions | 0.5 | 24 hours |
|
|
@@ -111,7 +114,8 @@ Retention: `ttl = base_ttl × (1 + access_boost × access_count)`, measured from
|
|
|
111
114
|
2. **While working** — `retain(type="task")` when the plan changes; `retain(type="observation")` for findings; durable knowledge as `decision` / `convention` / `fact` / `procedure`.
|
|
112
115
|
3. **Before compaction** — `retain(type="episode", checkpoint=true)` (the `pre-compact.py` hook) and `consolidate()`.
|
|
113
116
|
4. **After compaction / end of phase** — `promote(ref_id, type, content)` for the candidates worth keeping.
|
|
114
|
-
5. **
|
|
117
|
+
5. **Large artefacts** — `retain(type="document", content=<body>, source=<name>)` then `ask(ref_id, question)`; never `get` a document to read it.
|
|
118
|
+
6. **Curate** — `wiki_export` → review the drafts → `wiki_import`.
|
|
115
119
|
|
|
116
120
|
### Quick examples
|
|
117
121
|
|
|
@@ -137,6 +141,10 @@ retain(content="Use arq, not Celery", type="decision", supersedes="<old decision
|
|
|
137
141
|
get(ref_id="<ref_id from recall>")
|
|
138
142
|
|
|
139
143
|
forget(ref_id="<ref_id>")
|
|
144
|
+
|
|
145
|
+
retain(content=<the whole spec>, type="document", source="specs/auth/spec.md")
|
|
146
|
+
|
|
147
|
+
ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?", mode="auto")
|
|
140
148
|
```
|
|
141
149
|
|
|
142
150
|
---
|
|
@@ -158,6 +166,10 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
|
|
|
158
166
|
| `MEMKERNEL_PREVIEW_LENGTH` | `200` | Max characters in the `preview` field of `ArtifactRef` |
|
|
159
167
|
| `MEMKERNEL_CHUNK_SIZE` | `1500` | Max characters per AST chunk (code memories) |
|
|
160
168
|
| `MEMKERNEL_CHUNK_OVERLAP` | `200` | Character overlap between token-based fallback chunks |
|
|
169
|
+
| `MEMKERNEL_DOCUMENT_STORE` | `local` | Where `document` bodies live (`local` = the project's own ChromaDB collection) |
|
|
170
|
+
| `MEMKERNEL_DOCUMENT_SUMMARY_CHARS` | `1000` | Characters of the body kept in a `document` memory's summary record |
|
|
171
|
+
| `MEMKERNEL_REASONER` | `local` | `ReasonerPort` behind `ask`: `local` (hybrid search over chunks, no LLM) or `rlmstudio` (falls back to `local` when `rlmstudio` is not importable) |
|
|
172
|
+
| `MEMKERNEL_ASK_TOP_K` | `5` | Chunks the local reasoner returns per `ask` |
|
|
161
173
|
| `MEMKERNEL_EPISODE_TTL_DAYS` | `30` | Base TTL for `episode` memories (short-term) |
|
|
162
174
|
| `MEMKERNEL_OBSERVATION_TTL_DAYS` | `7` | Base TTL for `observation` memories (short-term) |
|
|
163
175
|
| `MEMKERNEL_TASK_TTL_HOURS` | `24` | Base TTL for `task` memories (working) |
|
|
@@ -210,6 +222,35 @@ report = svc.consolidate() # {"purged": n, "candidates": [...]}
|
|
|
210
222
|
|
|
211
223
|
`build_service()` is the supported entry point; the MCP server, the admin API and the hook templates all use it. Integration notes for `code-copilot-team` and `haa` are in [`docs/integration/harness-integration.md`](docs/integration/harness-integration.md).
|
|
212
224
|
|
|
225
|
+
### The session protocol as a component
|
|
226
|
+
|
|
227
|
+
`MemoryLifecycle` packages what the hook scripts do, so a harness calls four methods instead of re-implementing the protocol. Every method returns markdown ready for injection and does nothing but talk to the service:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from memkernel.factory import build_service
|
|
231
|
+
from memkernel.lifecycle import MemoryLifecycle
|
|
232
|
+
|
|
233
|
+
memory = MemoryLifecycle(build_service())
|
|
234
|
+
system_context = memory.on_session_start() # checkpoint, long-term context, promotion candidates, wiki pointer
|
|
235
|
+
...
|
|
236
|
+
memory.on_pre_compact(summary) # checkpoint episode + consolidation, before the context is compacted
|
|
237
|
+
recovered = memory.on_post_compact() # what to re-inject after compaction
|
|
238
|
+
memory.on_session_end(summary) # final checkpoint + consolidation
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Documents and `ask`
|
|
242
|
+
|
|
243
|
+
`retain(type="document")` keeps large artefacts out of the context window: the body goes to the document store (built-in: the project's own ChromaDB collection), its chunks are indexed for hybrid search, and the memkernel record is a summary. `ask(ref_id, question, mode)` answers through a `ReasonerPort`:
|
|
244
|
+
|
|
245
|
+
- `MEMKERNEL_REASONER=local` (default) — no LLM: the question is matched against the document's chunks with dense + BM25 + RRF and the best chunks come back as the answer, `mode="rag"`.
|
|
246
|
+
- `MEMKERNEL_REASONER=rlmstudio` — delegates to [rlm-studio](https://github.com/gosha70/rlm-studio)'s `interact(content, question, mode=mode)`, so `direct`, `rag`, `rlm` and `auto` select how the document is read (ADR-009). `rlm-studio` is not a memkernel dependency and is not on PyPI yet; install it from git into the same environment:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
pip install "rlm-studio @ git+https://github.com/gosha70/rlm-studio"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
When it is not importable, `build_service()` logs a warning and uses the local reasoner. With an external reasoner, `consolidate(synthesize=True)` also drafts long-term statements from the session's short-term memories.
|
|
253
|
+
|
|
213
254
|
## LLM wiki bridge
|
|
214
255
|
|
|
215
256
|
Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern (`index.md`, `log.md`, typed pages with cited sources — the same schema as `code-copilot-team/knowledge/wiki`).
|
|
@@ -254,7 +295,7 @@ Releases are published to PyPI by [`.github/workflows/release.yml`](.github/work
|
|
|
254
295
|
|
|
255
296
|
1. Set `version` in `pyproject.toml` and move the `[Unreleased]` entries in [`CHANGELOG.md`](CHANGELOG.md) under the new version.
|
|
256
297
|
2. Merge to `main` with CI green.
|
|
257
|
-
3. Tag and push: `git tag -a v0.
|
|
298
|
+
3. Tag and push: `git tag -a v0.3.0 -m "memkernel 0.3.0" && git push origin v0.3.0`.
|
|
258
299
|
|
|
259
300
|
The workflow refuses a tag that does not match the `pyproject.toml` version, builds the sdist and wheel, smoke-installs the wheel, and uploads through the `pypi` environment.
|
|
260
301
|
|
|
@@ -266,12 +307,14 @@ The workflow refuses a tag that does not match the `pyproject.toml` version, bui
|
|
|
266
307
|
src/memkernel/
|
|
267
308
|
├── mcp/ # MCP server + tool definitions (memory tools, wiki tools)
|
|
268
309
|
├── core/ # Domain models (tiers, types), MemoryService, ports (interfaces)
|
|
269
|
-
├──
|
|
270
|
-
├──
|
|
310
|
+
├── adapters/ # Optional external systems (RLM Studio reasoner), imported lazily
|
|
311
|
+
├── storage/ # ChromaDB adapter, local document store
|
|
312
|
+
├── retrieval/ # AST/token chunking, embeddings, hybrid search (dense + BM25 + RRF + rescoring), local reasoner
|
|
271
313
|
├── memory/ # Lifecycle: TTL/decay policies, retention, deduplication
|
|
272
314
|
├── wiki/ # LLM-wiki bridge: page model, export, import
|
|
273
315
|
├── api/ # FastAPI admin endpoints
|
|
274
316
|
├── factory.py # build_service() — the supported embedding entry point
|
|
317
|
+
├── lifecycle.py # MemoryLifecycle — the session protocol as a component
|
|
275
318
|
└── config.py # Pydantic settings
|
|
276
319
|
```
|
|
277
320
|
|
|
@@ -288,7 +331,7 @@ MemKernel is the memory component of a small family of projects by the same auth
|
|
|
288
331
|
| Project | Role | Relationship |
|
|
289
332
|
|---|---|---|
|
|
290
333
|
| [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
|
|
291
|
-
| [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer
|
|
334
|
+
| [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
|
|
292
335
|
| [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
|
|
293
336
|
| [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
|
|
294
337
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "memkernel"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Code-aware memory server for AI coding copilots"
|
|
5
5
|
authors = ["EGOGE"]
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
repository = "https://github.com/gosha70/memkernel"
|
|
9
|
+
homepage = "https://memkernel.dev"
|
|
10
|
+
documentation = "https://memkernel.dev"
|
|
9
11
|
classifiers = [
|
|
10
12
|
"Programming Language :: Python :: 3",
|
|
11
13
|
"Programming Language :: Python :: 3.11",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Copyright (c) 2026 EGOGE
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""RLM Studio adapter implementing ReasonerPort (ADR-009).
|
|
4
|
+
|
|
5
|
+
``rlmstudio`` is imported inside :class:`RLMStudioReasoner.__init__`, never at
|
|
6
|
+
package import time, because it pulls LiteLLM in. It is not a dependency of
|
|
7
|
+
memkernel (FR-016); install it from git::
|
|
8
|
+
|
|
9
|
+
pip install "rlm-studio @ git+https://github.com/gosha70/rlm-studio"
|
|
10
|
+
|
|
11
|
+
and set ``MEMKERNEL_REASONER=rlmstudio``. When the package is missing the
|
|
12
|
+
adapter raises :class:`ReasonerUnavailable` and ``build_service()`` falls
|
|
13
|
+
back to the LLM-free :class:`~memkernel.retrieval.reasoner.LocalReasoner`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from memkernel.core.models import Answer, ReasonerError
|
|
22
|
+
from memkernel.retrieval.reasoner import ReasonerUnavailable
|
|
23
|
+
|
|
24
|
+
INSTALL_HINT = 'pip install "rlm-studio @ git+https://github.com/gosha70/rlm-studio"'
|
|
25
|
+
|
|
26
|
+
# Field names tried, in order, when mapping an ``interact`` result onto Answer.
|
|
27
|
+
# ``InteractResult`` (rlmstudio.api) carries ``answer``, ``mode_used``,
|
|
28
|
+
# ``metrics`` and ``raw_result`` (a RunResultDTO with ``success`` / ``error``);
|
|
29
|
+
# the other names cover dict results and older shapes.
|
|
30
|
+
_ANSWER_FIELDS = ("answer", "response", "text", "content", "output")
|
|
31
|
+
_MODE_FIELDS = ("mode_used", "mode", "strategy")
|
|
32
|
+
_CITATION_FIELDS = ("citations", "sources", "references")
|
|
33
|
+
_USAGE_FIELDS = ("metrics", "usage", "cost", "budget")
|
|
34
|
+
_RAW_RESULT_FIELDS = ("raw_result",)
|
|
35
|
+
_SUCCESS_FIELDS = ("success",)
|
|
36
|
+
_ERROR_FIELDS = ("error",)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _field(result: Any, names: tuple[str, ...]) -> Any:
|
|
40
|
+
"""Read the first present field from a mapping or an object; None when absent."""
|
|
41
|
+
for name in names:
|
|
42
|
+
if isinstance(result, Mapping):
|
|
43
|
+
if name in result:
|
|
44
|
+
return result[name]
|
|
45
|
+
elif hasattr(result, name):
|
|
46
|
+
return getattr(result, name)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _usage(raw: Any) -> dict[str, float | int]:
|
|
51
|
+
"""Keep the numeric entries of a usage mapping (tokens, cost, steps, ...)."""
|
|
52
|
+
usage: dict[str, float | int] = {}
|
|
53
|
+
items = raw.items() if isinstance(raw, Mapping) else vars(raw).items() if hasattr(raw, "__dict__") else ()
|
|
54
|
+
for key, value in items:
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
continue
|
|
57
|
+
if isinstance(value, int | float):
|
|
58
|
+
usage[str(key)] = value
|
|
59
|
+
return usage
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _failure(result: Any) -> str | None:
|
|
63
|
+
"""Return the error text when the result (or its raw result) reports ``success=False``."""
|
|
64
|
+
for candidate in (result, _field(result, _RAW_RESULT_FIELDS)):
|
|
65
|
+
if candidate is None:
|
|
66
|
+
continue
|
|
67
|
+
if _field(candidate, _SUCCESS_FIELDS) is False:
|
|
68
|
+
error = _field(candidate, _ERROR_FIELDS)
|
|
69
|
+
return str(error) if error else "execution reported success=False"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_answer(result: Any, requested_mode: str) -> Answer:
|
|
74
|
+
"""Map whatever ``rlmstudio.interact`` returned onto :class:`Answer`.
|
|
75
|
+
|
|
76
|
+
A result whose ``success`` (or ``raw_result.success``) is False raises
|
|
77
|
+
:class:`ReasonerError`: RLM Studio puts formatted error text in ``answer``
|
|
78
|
+
on failure, and that text must never be taken for an answer. The execution
|
|
79
|
+
``trace`` is not provenance and is never turned into a citation.
|
|
80
|
+
"""
|
|
81
|
+
if result is None:
|
|
82
|
+
return Answer(answer="", mode=requested_mode)
|
|
83
|
+
if isinstance(result, str):
|
|
84
|
+
return Answer(answer=result, mode=requested_mode)
|
|
85
|
+
failure = _failure(result)
|
|
86
|
+
if failure is not None:
|
|
87
|
+
raise ReasonerError(f"rlmstudio {requested_mode} run failed: {failure}")
|
|
88
|
+
raw_answer = _field(result, _ANSWER_FIELDS)
|
|
89
|
+
answer = str(raw_answer) if raw_answer is not None else str(result)
|
|
90
|
+
raw_mode = _field(result, _MODE_FIELDS)
|
|
91
|
+
mode = str(raw_mode) if raw_mode else requested_mode
|
|
92
|
+
raw_citations = _field(result, _CITATION_FIELDS)
|
|
93
|
+
citations = [str(c) for c in raw_citations] if isinstance(raw_citations, list | tuple) else []
|
|
94
|
+
raw_usage = _field(result, _USAGE_FIELDS)
|
|
95
|
+
usage = _usage(raw_usage) if raw_usage is not None else {}
|
|
96
|
+
return Answer(answer=answer, mode=mode, citations=citations, usage=usage)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RLMStudioReasoner:
|
|
100
|
+
"""``ReasonerPort`` delegating to ``rlmstudio.interact(content, question, mode=mode)``."""
|
|
101
|
+
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
try:
|
|
104
|
+
import rlmstudio # type: ignore[import-not-found] # optional, never a dependency (FR-016)
|
|
105
|
+
except ImportError as exc:
|
|
106
|
+
raise ReasonerUnavailable(f"rlmstudio is not importable ({exc}); {INSTALL_HINT}") from exc
|
|
107
|
+
self._rlmstudio: Any = rlmstudio
|
|
108
|
+
|
|
109
|
+
def ask(self, content: str, question: str, mode: str) -> Answer:
|
|
110
|
+
"""Pass ``mode`` through unchanged (direct | rag | rlm | auto); report the mode actually used.
|
|
111
|
+
|
|
112
|
+
Raises :class:`ReasonerError` when RLM Studio reports a failed run or
|
|
113
|
+
when the call itself raises (provider down, timeout, budget exceeded):
|
|
114
|
+
callers degrade on that one exception type instead of on whatever the
|
|
115
|
+
provider stack throws.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
result = self._rlmstudio.interact(content, question, mode=mode)
|
|
119
|
+
except ReasonerError:
|
|
120
|
+
raise
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
raise ReasonerError(f"rlmstudio {mode} run raised {type(exc).__name__}: {exc}") from exc
|
|
123
|
+
return to_answer(result, mode)
|
|
@@ -34,6 +34,12 @@ class MemKernelSettings(BaseSettings):
|
|
|
34
34
|
chunk_size: int = 1500 # max chars per chunk
|
|
35
35
|
chunk_overlap: int = 200 # char overlap between token chunks (fallback only)
|
|
36
36
|
|
|
37
|
+
# Documents and ask() (ADR-009)
|
|
38
|
+
document_store: Literal["local"] = "local" # where `document` bodies live
|
|
39
|
+
document_summary_chars: int = 1000 # characters of the body kept in the document's summary record
|
|
40
|
+
reasoner: Literal["local", "rlmstudio"] = "local" # ReasonerPort behind ask(); falls back to local
|
|
41
|
+
ask_top_k: int = 5 # chunks the local reasoner returns per ask()
|
|
42
|
+
|
|
37
43
|
# Lifecycle / decay (see docs/architecture/memory-tiers.md)
|
|
38
44
|
episode_ttl_days: int = 30 # base TTL for short-term `episode` memories
|
|
39
45
|
observation_ttl_days: int = 7 # base TTL for short-term `observation` memories
|
|
@@ -42,6 +42,8 @@ class MemoryType(StrEnum):
|
|
|
42
42
|
CODE = "code"
|
|
43
43
|
# Long-term, procedural
|
|
44
44
|
PROCEDURE = "procedure"
|
|
45
|
+
# Long-term, a large artefact whose body lives in the document store (ADR-009)
|
|
46
|
+
DOCUMENT = "document"
|
|
45
47
|
# Short-term, episodic
|
|
46
48
|
EPISODE = "episode"
|
|
47
49
|
OBSERVATION = "observation"
|
|
@@ -55,6 +57,7 @@ TYPE_TIER: dict[MemoryType, MemoryTier] = {
|
|
|
55
57
|
MemoryType.FACT: MemoryTier.LONG_TERM,
|
|
56
58
|
MemoryType.CODE: MemoryTier.LONG_TERM,
|
|
57
59
|
MemoryType.PROCEDURE: MemoryTier.LONG_TERM,
|
|
60
|
+
MemoryType.DOCUMENT: MemoryTier.LONG_TERM,
|
|
58
61
|
MemoryType.EPISODE: MemoryTier.SHORT_TERM,
|
|
59
62
|
MemoryType.OBSERVATION: MemoryTier.SHORT_TERM,
|
|
60
63
|
MemoryType.TASK: MemoryTier.WORKING,
|
|
@@ -69,12 +72,30 @@ DEFAULT_IMPORTANCE: dict[MemoryType, float] = {
|
|
|
69
72
|
MemoryType.PROCEDURE: 0.8,
|
|
70
73
|
MemoryType.FACT: 0.7,
|
|
71
74
|
MemoryType.CODE: 0.6,
|
|
75
|
+
MemoryType.DOCUMENT: 0.6,
|
|
72
76
|
MemoryType.TASK: 0.5,
|
|
73
77
|
MemoryType.EPISODE: 0.4,
|
|
74
78
|
MemoryType.OBSERVATION: 0.3,
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
|
|
82
|
+
# Tags that mark the records a document store keeps beside the memkernel summary
|
|
83
|
+
# record: the canonical body and its indexed chunks. Both are hidden from
|
|
84
|
+
# top-level recall / list results; a chunk hit resolves to the document summary.
|
|
85
|
+
DOCUMENT_BODY_TAG = "document_body"
|
|
86
|
+
DOCUMENT_CHUNK_TAG = "document_chunk"
|
|
87
|
+
# Boolean metadata key set on body/chunk records so search can exempt them
|
|
88
|
+
# from owner-specific filters (session, type, tier, checkpoint): one body may
|
|
89
|
+
# be owned by several summaries in different sessions, so the filters are
|
|
90
|
+
# applied to the owning summary a hit resolves to, not to the shared part.
|
|
91
|
+
DOCUMENT_PART_KEY = "document_part"
|
|
92
|
+
|
|
93
|
+
# Reasoning modes accepted by ask(): RLM Studio's Direct / RAG / RLM / auto
|
|
94
|
+
# selection, passed through to the ReasonerPort unchanged (ADR-009).
|
|
95
|
+
ASK_MODES: tuple[str, ...] = ("direct", "rag", "rlm", "auto")
|
|
96
|
+
DEFAULT_ASK_MODE = "auto"
|
|
97
|
+
|
|
98
|
+
|
|
78
99
|
def tier_of(memory_type: MemoryType) -> MemoryTier:
|
|
79
100
|
"""Return the tier a memory type belongs to."""
|
|
80
101
|
return TYPE_TIER[memory_type]
|
|
@@ -103,6 +124,22 @@ class Memory(BaseModel):
|
|
|
103
124
|
tags: list[str] = []
|
|
104
125
|
supersedes: str | None = None
|
|
105
126
|
promoted_from: str | None = None
|
|
127
|
+
# Set on a ``document`` summary record and on the body/chunk records the
|
|
128
|
+
# document store keeps for it (ADR-009).
|
|
129
|
+
document_id: str | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ReasonerError(RuntimeError):
|
|
133
|
+
"""A ReasonerPort implementation ran but did not produce an answer (provider error, timeout, budget)."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Answer(BaseModel):
|
|
137
|
+
"""Result of ``ask``: the answer text, the reasoning mode actually used, and provenance."""
|
|
138
|
+
|
|
139
|
+
answer: str
|
|
140
|
+
mode: str
|
|
141
|
+
citations: list[str] = []
|
|
142
|
+
usage: dict[str, float | int] = {}
|
|
106
143
|
|
|
107
144
|
|
|
108
145
|
class ArtifactRef(BaseModel):
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from memkernel.core.models import Memory, SearchResult
|
|
14
|
+
from memkernel.core.models import Answer, Memory, SearchResult
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@runtime_checkable
|
|
@@ -47,7 +47,9 @@ class VectorStorePort(Protocol):
|
|
|
47
47
|
"""Fetch-and-merge: only overwrites caller-specified keys."""
|
|
48
48
|
...
|
|
49
49
|
|
|
50
|
-
def count(self) -> int:
|
|
50
|
+
def count(self, where: dict[str, Any] | None = None) -> int:
|
|
51
|
+
"""Number of records, optionally only those matching a metadata filter (ids-only fetch)."""
|
|
52
|
+
...
|
|
51
53
|
|
|
52
54
|
def list_collections(self) -> list[dict[str, Any]]:
|
|
53
55
|
"""List all collections visible to this adapter with their record counts."""
|
|
@@ -93,3 +95,35 @@ class ChunkerPort(Protocol):
|
|
|
93
95
|
"""Code/text chunking strategy (stub for future US3)."""
|
|
94
96
|
|
|
95
97
|
def chunk(self, content: str, metadata: dict[str, Any]) -> list[str]: ...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class DocumentStorePort(Protocol):
|
|
102
|
+
"""Document bodies: stored whole, chunk-indexed, searchable per document (ADR-009)."""
|
|
103
|
+
|
|
104
|
+
def put(self, content: str, name: str) -> str:
|
|
105
|
+
"""Store a document body and return its doc_id (idempotent for identical content)."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
def get(self, doc_id: str) -> str | None:
|
|
109
|
+
"""Return the stored body, or None if ``doc_id`` is unknown."""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def index(self, doc_id: str, chunks: list[str]) -> int:
|
|
113
|
+
"""Index the chunks of a stored document; returns the number of chunks indexed."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
def indexed(self, doc_id: str) -> int:
|
|
117
|
+
"""Number of chunks currently indexed for ``doc_id`` (0 for an unknown or un-indexed body); ids-only."""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
def search(self, doc_id: str, query: str, top_k: int) -> list[str]:
|
|
121
|
+
"""Return the top_k chunk texts of one document ranked against ``query``."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@runtime_checkable
|
|
126
|
+
class ReasonerPort(Protocol):
|
|
127
|
+
"""Answers a question about a body of text in a given mode (direct | rag | rlm | auto)."""
|
|
128
|
+
|
|
129
|
+
def ask(self, content: str, question: str, mode: str) -> Answer: ...
|