palimpsests 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.
Files changed (48) hide show
  1. palimpsests-0.2.0/.github/workflows/ci.yml +61 -0
  2. palimpsests-0.2.0/.github/workflows/release.yml +65 -0
  3. palimpsests-0.2.0/.gitignore +26 -0
  4. palimpsests-0.2.0/ARCHITECTURE.md +443 -0
  5. palimpsests-0.2.0/CHANGELOG.md +77 -0
  6. palimpsests-0.2.0/CONTRIBUTING.md +49 -0
  7. palimpsests-0.2.0/LICENSE +201 -0
  8. palimpsests-0.2.0/PKG-INFO +238 -0
  9. palimpsests-0.2.0/README.md +202 -0
  10. palimpsests-0.2.0/docs/USAGE.md +258 -0
  11. palimpsests-0.2.0/pyproject.toml +117 -0
  12. palimpsests-0.2.0/src/palimpsests/__init__.py +4 -0
  13. palimpsests-0.2.0/src/palimpsests/audit/__init__.py +33 -0
  14. palimpsests-0.2.0/src/palimpsests/audit/key_manager.py +77 -0
  15. palimpsests-0.2.0/src/palimpsests/audit/log.py +280 -0
  16. palimpsests-0.2.0/src/palimpsests/cli.py +142 -0
  17. palimpsests-0.2.0/src/palimpsests/context/__init__.py +40 -0
  18. palimpsests-0.2.0/src/palimpsests/context/block_memory.py +186 -0
  19. palimpsests-0.2.0/src/palimpsests/context/embeddings.py +48 -0
  20. palimpsests-0.2.0/src/palimpsests/context/tokens.py +47 -0
  21. palimpsests-0.2.0/src/palimpsests/context/window_manager.py +174 -0
  22. palimpsests-0.2.0/src/palimpsests/core.py +327 -0
  23. palimpsests-0.2.0/src/palimpsests/engine/__init__.py +40 -0
  24. palimpsests-0.2.0/src/palimpsests/engine/base.py +159 -0
  25. palimpsests-0.2.0/src/palimpsests/engine/capabilities.py +78 -0
  26. palimpsests-0.2.0/src/palimpsests/engine/messages.py +74 -0
  27. palimpsests-0.2.0/src/palimpsests/engine/session.py +55 -0
  28. palimpsests-0.2.0/src/palimpsests/providers/__init__.py +31 -0
  29. palimpsests-0.2.0/src/palimpsests/providers/errors.py +60 -0
  30. palimpsests-0.2.0/src/palimpsests/providers/llamacpp.py +297 -0
  31. palimpsests-0.2.0/src/palimpsests/providers/native.py +107 -0
  32. palimpsests-0.2.0/src/palimpsests/providers/ollama.py +261 -0
  33. palimpsests-0.2.0/src/palimpsests/providers/process.py +168 -0
  34. palimpsests-0.2.0/src/palimpsests/py.typed +3 -0
  35. palimpsests-0.2.0/src/palimpsests/registry.py +177 -0
  36. palimpsests-0.2.0/tests/conftest.py +65 -0
  37. palimpsests-0.2.0/tests/test_audit.py +186 -0
  38. palimpsests-0.2.0/tests/test_block_memory.py +196 -0
  39. palimpsests-0.2.0/tests/test_cli.py +104 -0
  40. palimpsests-0.2.0/tests/test_context_window.py +224 -0
  41. palimpsests-0.2.0/tests/test_core.py +281 -0
  42. palimpsests-0.2.0/tests/test_engine_contract.py +175 -0
  43. palimpsests-0.2.0/tests/test_llama_process.py +180 -0
  44. palimpsests-0.2.0/tests/test_llamacpp.py +251 -0
  45. palimpsests-0.2.0/tests/test_native.py +89 -0
  46. palimpsests-0.2.0/tests/test_ollama.py +284 -0
  47. palimpsests-0.2.0/tests/test_registry.py +131 -0
  48. palimpsests-0.2.0/tests/test_smoke.py +13 -0
@@ -0,0 +1,61 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: lint
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ # Lint WITHOUT an editable install of the package. This is the fix
22
+ # for a subtle isort divergence: when `palimpsests` is pip-installed
23
+ # (via -e .), ruff's isort sees it as an installed third-party
24
+ # package and regroups first-party imports differently than it does
25
+ # from the src-layout heuristic alone — which passed locally but
26
+ # failed in CI. Linting a plain checkout keeps `palimpsests`
27
+ # first-party by the src heuristic, deterministically.
28
+ - name: Install ruff only
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ pip install ruff==0.15.20
32
+
33
+ - name: Lint (ruff)
34
+ run: |
35
+ ruff --version
36
+ ruff check --output-format=full .
37
+
38
+ test:
39
+ name: test (${{ matrix.os }}, py${{ matrix.python-version }})
40
+ runs-on: ${{ matrix.os }}
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ os: [ubuntu-latest, macos-latest, windows-latest]
45
+ python-version: ["3.11", "3.12"]
46
+
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - name: Set up Python ${{ matrix.python-version }}
51
+ uses: actions/setup-python@v5
52
+ with:
53
+ python-version: ${{ matrix.python-version }}
54
+
55
+ - name: Install (base + dev)
56
+ run: |
57
+ python -m pip install --upgrade pip
58
+ pip install -e ".[dev]"
59
+
60
+ - name: Test (pytest)
61
+ run: pytest -q
@@ -0,0 +1,65 @@
1
+ name: release
2
+
3
+ # Publish to PyPI on a version tag. Build and publish are split so the
4
+ # OIDC-scoped publish job has the narrowest possible permissions, and so
5
+ # the built artifacts are visible even if publishing is skipped.
6
+ #
7
+ # Publishing uses PyPI Trusted Publishing (OIDC) — no API token is stored
8
+ # in the repo. The trusted publisher must be configured once on PyPI for
9
+ # project "palimpsests", workflow "release.yml", environment "pypi"
10
+ # (a pending publisher works before the first release, so the first tag
11
+ # creates the project). Until that is configured, the publish step fails
12
+ # harmlessly; the build job still runs.
13
+
14
+ on:
15
+ push:
16
+ tags:
17
+ - "v*"
18
+
19
+ jobs:
20
+ build:
21
+ name: build distributions
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Build sdist and wheel
31
+ run: |
32
+ python -m pip install --upgrade build
33
+ python -m build
34
+
35
+ - name: Check metadata
36
+ run: |
37
+ python -m pip install --upgrade twine
38
+ python -m twine check dist/*
39
+
40
+ - uses: actions/upload-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ publish:
46
+ name: publish to PyPI
47
+ needs: build
48
+ runs-on: ubuntu-latest
49
+ # The environment gates the release and scopes the OIDC token. Create
50
+ # an environment named "pypi" in the repo settings; optionally add a
51
+ # required reviewer there for a manual approval gate on every publish.
52
+ environment:
53
+ name: pypi
54
+ url: https://pypi.org/p/palimpsests
55
+ permissions:
56
+ # id-token: write is what lets PyPI verify this workflow via OIDC.
57
+ id-token: write
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+
64
+ - name: Publish
65
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Test / tooling
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # Palimpsests runtime state (never commit user workspaces or models)
18
+ .palimpsests/
19
+ *.gguf
20
+ workspace/
21
+ .context-memory/
22
+
23
+ # Editors / OS
24
+ .vscode/
25
+ .idea/
26
+ .DS_Store
@@ -0,0 +1,443 @@
1
+ # Palimpsests — Architecture
2
+
3
+ **Status:** design document
4
+ **Scope:** the inference engine and the layers built above it
5
+
6
+ ---
7
+
8
+ ## 0. Thesis
9
+
10
+ Ollama and llama.cpp are optimized for one question: *how fast can I answer a
11
+ single request?* But a growing class of workloads has a different shape —
12
+ **agentic workloads**: a process that makes hundreds of calls in a loop, shares
13
+ one system prompt across calls, retries, branches, and invokes tools. That is a
14
+ different profile than single-request throughput.
15
+
16
+ Palimpsests builds **three levels of control** over local inference behind a
17
+ single abstraction, plus a **context-memory layer** that works the same on all
18
+ three levels. Levels 1–2 are "how do I cheaply run someone else's model." Level 3
19
+ is an own inference service designed for agentic, multi-session workloads on
20
+ local/edge hardware, where datacenter solutions (vLLM/SGLang) do not fit because
21
+ of their VRAM requirements.
22
+
23
+ The name reflects the central mechanism: a *palimpsest* is a parchment scraped
24
+ clean and rewritten, where the old text still shows through. That is exactly what
25
+ the context-memory layer does — it evicts the middle of the context (scrapes),
26
+ writes new content into the window, but the evicted text bleeds back through
27
+ retrieval; at level 3 the same image applies to KV state.
28
+
29
+ The governing principle of this whole document: **we do not modify the attention
30
+ kernel.** Everything we do lives either as engine launch parameters (levels 1–2),
31
+ orchestration above the engine (context-memory), or an own service with KV-state
32
+ management (level 3). Research gives us *principles*; we implement them where we
33
+ have control.
34
+
35
+ ---
36
+
37
+ ## 1. Three levels of control
38
+
39
+ ```
40
+ Level 1: Ollama / LM Studio (external daemon)
41
+ -> we don't control inference, only an HTTP client to someone else's server
42
+ -> maximum compatibility, zero control over loading/quant/batching
43
+
44
+ Level 2: Embedded engine (llama.cpp via subprocess)
45
+ -> we control loading, quant, sampling, KV cache, offload
46
+ -> self-contained, no external daemon
47
+ -> we depend on the upstream API
48
+
49
+ Level 3: Own wire protocol (own inference service)
50
+ -> full control: continuous batching, shared prefix KV,
51
+ server-side tool loop, KV persistence
52
+ -> the most work, but this is a platform, not a wrapper
53
+ ```
54
+
55
+ All three hide behind a single `InferenceEngine` abstraction. Callers never know
56
+ which level is active — they ask `engine.capabilities`, never `isinstance`.
57
+
58
+ | Level | engine_id | What it adds over the previous |
59
+ |---|---|---|
60
+ | 1 | `ollama` | Basic local inference, zero configuration |
61
+ | 2 | `llamacpp` | Fine-grained control of memory mechanisms (§3) |
62
+ | 3 | `pal-native` | Agentic-first: batching, shared prefix, tool-loop, KV-memory |
63
+
64
+ ---
65
+
66
+ ## 2. The abstraction and the stateless / stateful split
67
+
68
+ Two worlds, deliberately separated so the abstraction does not break when we
69
+ reach level 3:
70
+
71
+ - **stateless** (`chat()` / `chat_stream()`) — "fire and forget." Levels 1–2 live
72
+ here entirely.
73
+ - **stateful** (`InferenceSession`) — a live session with persistent KV state.
74
+ Exists only at level 3.
75
+
76
+ ### 2.1 Data shared across all levels
77
+
78
+ - `ChatChunk` — a stream increment (`delta`, `done`, metadata in the final chunk).
79
+ Has a `tool_call` slot for the server-side tool loop (level 3; always `None` on
80
+ 1–2).
81
+ - `ChatResponse` — the accumulated stream (non-streaming is derived for free).
82
+ - `ModelInfo` — `name`, `size_bytes`, `engine_id`, `quant`, `loaded`.
83
+ - `EngineMemoryConfig` — see §3.
84
+ - `EngineCapabilities` — the adapter's capability declaration (drives orchestration).
85
+
86
+ ### 2.2 `EngineCapabilities` — behavior via data, not types
87
+
88
+ The orchestrator asks `engine.capabilities.server_side_tools`, **not**
89
+ `if isinstance(engine, ...)`. Adding level 3 means raising flags in one place;
90
+ call sites do not change.
91
+
92
+ ```
93
+ control_level: int # 1 | 2 | 3
94
+ streaming: bool
95
+ stateful_sessions: bool # holds KV between calls (level 3)
96
+ shared_prefix: bool # shared prefix KV across sessions (level 3)
97
+ server_side_tools: bool # tool-loop without re-prefill (level 3)
98
+ continuous_batching: bool # N sessions in one forward (level 3)
99
+ kv_persistence: bool # KV to/from disk (level 3)
100
+ ```
101
+
102
+ ### 2.3 The contract
103
+
104
+ - `InferenceEngine.chat_stream(...)` — levels 1–2 implement only this.
105
+ - `InferenceEngine.open_session(...)` — level 3; on 1–2 the base class raises
106
+ `CapabilityUnsupported` (**loud refusal, not a silent fallback**).
107
+ - `InferenceSession` — a live session: `send()`, `append_tool_result()`,
108
+ `save_state() -> bytes`, `load_state(bytes)`, `close()`.
109
+ - `BaseInferenceEngine` — shared logic: `chat()` = accumulation of `chat_stream()`;
110
+ the default `open_session()` raises `CapabilityUnsupported`.
111
+
112
+ **Consequence:** level 3 does not rewrite the architecture — it **fills already
113
+ existing slots**. `NativeEngine` will be the first to return a real
114
+ `InferenceSession` instead of an error; no call site above it changes.
115
+
116
+ ---
117
+
118
+ ## 3. `EngineMemoryConfig` — memory-reduction mechanisms (levels 1–2)
119
+
120
+ Every real memory mechanism is an **engine launch parameter**, not our logic. We
121
+ do not implement them — we **expose** them. Each adapter declares what it can do;
122
+ the UI shows only what is supported.
123
+
124
+ ```
125
+ kv_cache_quant: str | None # None | "q8_0" | "q4_0" | "turbo3"
126
+ flash_attention: bool # PREREQUISITE for kv_cache_quant
127
+ gpu_layers: int | None # -ngl; None = CPU-only
128
+ use_mmap: bool # a trade-off, not a clear win
129
+ context_size: int | None # -c; the main driver of KV size
130
+ draft_model: str | None # speculative decoding
131
+ ```
132
+
133
+ ### 3.1 Production-ready in engines (we expose, not implement)
134
+
135
+ - **KV cache quantization** (`--cache-type-k q8_0/q4_0`) — the biggest practical
136
+ win. F16 KV at 128K can OOM; q8/q4 KV lets it fit. **Requires Flash Attention**,
137
+ otherwise KV is dequantized every step and becomes slower than no quantization
138
+ at all.
139
+ - **Flash Attention** (`--flash-attn`) — constant attention memory regardless of
140
+ length (tiled). Prerequisite for KV-quant.
141
+ - **Partial GPU offload** (`-ngl N`) — N layers on GPU, the rest on CPU. Runs
142
+ models larger than VRAM. RAM does not drop automatically (mmap).
143
+ - **mmap** (default on) — weights are mapped from disk on demand; `--no-mmap`
144
+ reduces RAM when the model is smaller than RAM but blocks loading a model larger
145
+ than RAM.
146
+ - **Speculative decoding** — a draft model proposes, the large one verifies in
147
+ batches. Speed, not memory.
148
+
149
+ ### 3.2 Fresh, working, edge (know it, don't base on it)
150
+
151
+ - **TurboQuant for GGML** (`--cache-type-k turbo3`) — ~4.57x KV compression at
152
+ 3.5 bpw. For 13B+ on very long context, near-lossless; degrades on very small
153
+ models. An option, not a default.
154
+
155
+ ### 3.3 Research — do NOT base on this (papers, not engines)
156
+
157
+ KIVI, KVQuant, PyramidKV, XQuant rematerialization, low-rank KV,
158
+ retrieval-augmented attention (InfLLM, RetrievalAttention) at the kernel level —
159
+ mostly not in stable engine releases. A source of ideas, not a base.
160
+
161
+ ### 3.4 Validation in code
162
+
163
+ `kv_cache_quant != None` requires `flash_attention == True`. This is an adapter
164
+ check with a **clear error**, not a silent performance regression.
165
+
166
+ ### 3.5 Sensible defaults (consumer hardware: 16–32GB RAM, maybe a consumer GPU)
167
+
168
+ - **KV quant `q8_0` + Flash Attention** — near-lossless, meaningful savings on
169
+ context.
170
+ - **`turbo3` KV** — for 13B+ at 128K+, with a small-model warning.
171
+ - **`context_size`** — the main lever (a direct multiplier of KV size).
172
+ - **mmap on** by default; `--no-mmap` when the model fits in RAM.
173
+ - **GPU offload** — auto-detect how many layers fit, with a manual override.
174
+
175
+ ---
176
+
177
+ ## 4. Context-memory — an orchestration layer above all levels
178
+
179
+ The most important layer. It works **the same** on levels 1/2/3 because it lives
180
+ at the text/message level entering the model, not in the attention kernel. This
181
+ is what makes local inference with long memory practical on consumer hardware.
182
+
183
+ ```
184
+ context/
185
+ window_manager.py # Ideas 1+3: sink + window + evict-middle, prefix stability
186
+ block_memory.py # Idea 2: evicted -> embed -> retrieval (on the fs layer)
187
+ ```
188
+
189
+ ### 4.1 Idea 1 — the StreamingLLM principle for long context
190
+
191
+ **Research:** the first ~4 tokens ("attention sinks") absorb 45–55% of attention
192
+ mass; evicting them under a sliding window blows up perplexity ~100x. Fix: always
193
+ keep the first K tokens + a sliding window of the last W. This is already a
194
+ standard eviction technique, not our invention — we apply it at the
195
+ message-history level.
196
+
197
+ **Our implementation** (not KV, but the message-history level):
198
+
199
+ ```
200
+ [SINK: system prompt + first N messages] <- NEVER cut
201
+ [EVICTED MIDDLE: old messages] <- eviction candidates
202
+ [WINDOW: last W messages] <- always kept
203
+ ```
204
+
205
+ The difference from naive truncation: we keep the "sink" — the system prompt and
206
+ first exchanges that set the task structure. Implementation is a
207
+ `ContextWindowManager` above the `InferenceEngine`: a tokenizer counts fit
208
+ (`context_size`); on overflow it keeps sink+window and evicts the middle (not
209
+ lost — see §4.2).
210
+
211
+ ### 4.2 Idea 2 — the InfLLM principle as block retrieval over files
212
+
213
+ **Research:** InfLLM splits context into blocks, keeps most in CPU memory, pulls
214
+ the top-k relevant ones for the current query.
215
+
216
+ **Our implementation:** retrieval not of KV blocks (kernel level) but of **text
217
+ blocks of evicted context** — the same principle one level up:
218
+
219
+ - The evicted "middle" is chunked into blocks.
220
+ - Each block is embedded by a local embedder (`nomic-embed-text` via the active
221
+ engine).
222
+ - Blocks live in `workspace/.context-memory/` (on the fs layer); the vector index
223
+ is a SQLite database alongside.
224
+ - Before a call: embed the query, retrieve the top-k relevant blocks, insert them
225
+ back between sink and window.
226
+
227
+ This is effectively a RAG pattern over evicted context — a known approach applied
228
+ to our backing store. The result: memory bounded by disk, not RAM.
229
+
230
+ ### 4.3 Idea 3 — prefix-cache-aware structuring (a free win)
231
+
232
+ **Research:** the prompt/prefix caching win only materializes if the prefix is
233
+ byte-stable.
234
+
235
+ **Our approach:** the sink part (system + first messages) is stable by definition
236
+ -> the prefix is always identical -> the engine reuses the prefix KV
237
+ **automatically**. So Idea 1 turns this win on for free, as long as we
238
+ deliberately keep the sink stable (no timestamps/dynamics at the start of the
239
+ prompt).
240
+
241
+ ### 4.4 Layer summary
242
+
243
+ A 7B model with a real 8K context + block-memory retrieval behaves as if it had a
244
+ much longer context, without OOM, because the evicted content is on disk. The
245
+ backing store (`workspace/.context-memory/`) is deliberately shared with the
246
+ future level-3 KV persistence (§5.5) — it just stores something different
247
+ (text vs KV).
248
+
249
+ ---
250
+
251
+ ## 5. Level 3 — own inference service (agentic-first)
252
+
253
+ Not "run someone else's model faster" (that is 1–2), but an inference service for
254
+ agentic, multi-session workloads. All ideas below share one architectural base: a
255
+ **persistent model server with KV-state management that understands requests are
256
+ related** (a shared system prompt, related sessions). Implemented sequentially.
257
+
258
+ > **Important (see §6 Prior art):** none of the level-3 ideas are our invention.
259
+ > Continuous batching and shared prefix are standard in datacenter serving engines
260
+ > (vLLM, SGLang). SSD KV persistence already ships in a product (oMLX) and appears
261
+ > in research (Persistent Q4 KV Cache, arXiv 2603.04428). Our bet is not the
262
+ > novelty of a mechanism, but bringing these mechanisms to the local desktop under
263
+ > a single abstraction together with levels 1–2 and context-memory.
264
+
265
+ ### 5.1 Continuous batching for multi-session
266
+
267
+ **Problem:** several parallel agentic sessions. On 1/2 each is a separate request
268
+ (queue, or a separate copy of the model = multiplied RAM). Ollama/LM Studio have
269
+ no continuous batching (serial dispatch, FCFS).
270
+
271
+ **Level 3:** one loaded model; the decode step processes tokens from N sessions in
272
+ one forward pass. Not full vLLM — the right scheduler for **2–8 local sessions**,
273
+ not 1000 users. Follows directly from the multi-session profile.
274
+
275
+ **Slot:** the scheduler multiplexes several `InferenceSession`s onto one model.
276
+
277
+ ### 5.2 Shared prefix KV across sessions
278
+
279
+ **Problem:** sessions share a large system prompt (rules, tool definitions). On
280
+ 1/2 each call recomputes the prefix KV from scratch.
281
+
282
+ **Level 3:** compute the system prefix KV **once**, keep it in memory; each session
283
+ starts from the ready prefix KV (radix-tree prefix sharing like SGLang, but at the
284
+ scale of "a few sessions share one system prompt").
285
+
286
+ **Slot:** `open_session(system_prompt=X)` — sessions with the same X share the
287
+ prefix KV.
288
+
289
+ ### 5.3 Server-side tool-calling loop
290
+
291
+ **Problem:** the agentic loop is host -> engine (up to tool_call) -> parse ->
292
+ execute tool -> **new call** with the result -> engine **re-reads the whole
293
+ context**. Each tool-use iteration is a full re-prefill.
294
+
295
+ **Level 3:** the loop lives **inside** the service. The model generates a tool_call
296
+ -> decode pauses with **KV preserved** -> tool_call goes out -> the result comes
297
+ back -> decode **resumes from the same KV**, appending only the result. The context
298
+ is not re-read. For a local model where prefill is slow, this is the difference
299
+ between "usable" and "painful" on a 10-step tool loop.
300
+
301
+ **Slot:** `send()` yields a chunk with `tool_call`; `append_tool_result()`
302
+ continues from the same KV.
303
+
304
+ ### 5.4 Speculative decoding with a role-specific draft model
305
+
306
+ **Research:** a draft model proposes in batches, the large one verifies; ~90 ->
307
+ 180+ tokens/sec on high-context long-output.
308
+
309
+ **Level 3:** we integrate draft+target into our loop. The draft model can be chosen
310
+ per session type — a fast classifier gets an aggressive draft, long reasoning gets
311
+ a more accurate one.
312
+
313
+ **Slot:** `config.draft_model` at the `open_session` level.
314
+
315
+ ### 5.5 KV states as persistent memory (catches up with oMLX, cross-platform)
316
+
317
+ At level 3 we have KV access -> instead of retrieving text and **recomputing** its
318
+ KV (as block-memory §4.2 does), we store the **KV itself** for evicted blocks to
319
+ disk and load it back ready.
320
+
321
+ **State of the world (see §6):** this already exists — oMLX does paged SSD KV
322
+ caching (safetensors, two-tier RAM/SSD), cutting TTFT from 30–90s to <5s; arXiv
323
+ 2603.04428 does the same with Q4 KV on disk. **Our difference** is not discovery
324
+ but: (a) cross-platform (oMLX is Apple/MLX only), (b) integration into a single
325
+ abstraction together with levels 1–2 and context-memory, (c) a backing store
326
+ shared with the text memory of §4.2.
327
+
328
+ **Slot:** `save_state() -> bytes` / `load_state(bytes)`. The bytes go into the same
329
+ `workspace/.context-memory/` as the text blocks of §4.2 — text-memory ->
330
+ KV-memory becomes a natural transition, not a separate project.
331
+
332
+ An honest complexity assessment: this is a late-horizon feature, not a "next PR."
333
+
334
+ ### 5.6 Level-3 implementation ranking
335
+
336
+ | Idea | Value | Complexity | Order |
337
+ |---|---|---|---|
338
+ | Continuous batching | Very high | Medium | 1 |
339
+ | Shared prefix KV | Very high | Medium | 1 (one architecture with batching) |
340
+ | Server-side tool loop | High | Medium-high | 2 |
341
+ | Speculative per-role | Medium | Medium | 3 (optional) |
342
+ | KV-as-memory | High | Very high | Horizon |
343
+
344
+ Ideas 1–3 share one base (persistent server + KV-state management), so once it
345
+ exists, tool-loop and KV-memory are additions on top of the same base, not
346
+ separate projects.
347
+
348
+ ---
349
+
350
+ ## 6. Prior art & positioning (honest analysis of analogs)
351
+
352
+ A landscape survey (as of mid-2026) showed: **no project assembles this whole
353
+ stack, but every individual piece already exists somewhere.** We invent no new
354
+ mechanism. This must stay in view so strategy is not built on a false sense of
355
+ novelty.
356
+
357
+ ### 6.1 What already exists
358
+
359
+ | Stack component | State of the world | Examples |
360
+ |---|---|---|
361
+ | Provider abstraction (L1–2) | commodity | LM Studio, Jan, ServiceStack AI Server (OSS gateway) |
362
+ | Sink/window context (§4.1) | known technique | StreamingLLM; practical guides |
363
+ | Block retrieval of evicted context (§4.2) | RAG pattern | numerous memory projects |
364
+ | Continuous batching on edge (§5.1) | actively worked on | Clairvoyant (SJF sidecar-proxy for Ollama/llama.cpp); vLLM/SGLang for datacenter |
365
+ | Shared prefix KV (§5.2) | serving standard | vLLM, SGLang |
366
+ | KV persistence as memory (§5.5) | **already shipped** | **oMLX** (macOS, paged SSD KV, safetensors); arXiv 2603.04428 (Q4 KV on disk) |
367
+
368
+ ### 6.2 Key findings
369
+
370
+ - **oMLX** — a macOS-native inference server on mlx-lm with paged SSD KV caching:
371
+ cache blocks persist to disk (safetensors, two-tier RAM/SSD), survive context
372
+ shifts and restarts without recomputation; TTFT on long context drops from
373
+ 30–90s to <5s. This is our §5.5, already in production — but Apple/MLX only, KV
374
+ persistence alone, without the three-level abstraction or context-memory.
375
+ - **Clairvoyant** — a drop-in sidecar proxy for serial OpenAI-compatible backends,
376
+ solving head-of-line blocking via SJF scheduling; directly attacks our §5.1
377
+ problem (batching/queueing on consumer hardware without datacenter VRAM).
378
+ - **ServiceStack AI Server** — an OSS self-hosted gateway with multi-provider
379
+ orchestration; our provider-abstraction level, but server-side, without
380
+ context-memory and without levels 2–3.
381
+
382
+ ### 6.3 Where the value actually is
383
+
384
+ Not technology, but **integration + positioning**:
385
+
386
+ 1. **The full stack as one product.** No one has assembled "three levels of
387
+ control under one abstraction + context-memory on all three + local-first."
388
+ oMLX is Apple + KV-persistence only. Clairvoyant is a batching proxy only. AI
389
+ Server is a gateway only. Palimpsests is the only one offering the full stack.
390
+ 2. **Cross-platform.** The main concrete advantage over oMLX (closest by
391
+ substance): not tied to Apple Silicon.
392
+ 3. **One abstraction from wrapper to native service.** The user moves through
393
+ levels 1->2->3 without changing code above the engine.
394
+
395
+ ### 6.4 Strategic conclusion
396
+
397
+ The problem is **maturing and consolidating now**: continuous batching on edge is
398
+ being attacked, KV persistence already ships in a product, provider abstraction is
399
+ a commodity. The biggest risk is not "someone did the same thing" but "someone
400
+ assembles the integration faster." So the priority is to **assemble a working full
401
+ stack end-to-end faster than the market consolidates**, rather than perfecting each
402
+ level in isolation.
403
+
404
+ ---
405
+
406
+ ## 7. PR sequence
407
+
408
+ The order is deliberate: **context-memory (I3, I5) before llama.cpp (I6)** — pure
409
+ Python delivers value even at level 1, without the native-wheel CI pain, and
410
+ differentiates faster.
411
+
412
+ | PR | Name | Substance | Depends on |
413
+ |---|---|---|---|
414
+ | I1 | Inference Protocol + contracts | `InferenceEngine`, `InferenceSession`, `ChatChunk/Response`, `ModelInfo`, `EngineMemoryConfig`, `EngineCapabilities`, `BaseInferenceEngine`; registry `local_inference` + `active_inference_engine`; audit `engine_id` (nullable). Inert. | — |
415
+ | I2 | Ollama -> level-1 adapter | Port the HTTP client -> `OllamaEngine`; `chat_stream` via Ollama `stream:true`; thin re-export for compatibility. Zero behavioral diff. | I1 |
416
+ | I3 | `ContextWindowManager` | Ideas 1+3: sink/window/evict + prefix stability. Pure Python. | I1 |
417
+ | I4 | `local_chat` API / CLI | `chat`/`models`/`engine` commands; routes through `ContextWindowManager`. | I2, I3 |
418
+ | I5 | `BlockMemory` | Idea 2: evicted -> embed -> retrieval; backing `workspace/.context-memory/` + SQLite index. | I3, fs layer |
419
+ | I6 | llama.cpp level-2 adapter | `LlamaCppEngine` spawns and owns a `llama-server` subprocess (OpenAI-compatible HTTP, not the `llama-cpp-python` binding); full `EngineMemoryConfig` applied as launch flags; FA<->KV-quant validation inherited; spawn + attach modes; opt-in via `PALIMPSESTS_LLAMACPP_MODEL`; `[llamacpp]` extra is an empty marker (binary out-of-band); wire-level CI (no real llama.cpp). | I1 |
420
+ | I7 | Native wire slot | `NativeEngine` subclasses `BaseInferenceEngine`; `control_level=3` with every feature flag `False`; every operation raises `CapabilityUnsupported`; `is_available()` `False`. Honest placeholder — `ProcessManager` extracted later, on the first real native lifecycle. | I1 |
421
+
422
+ After that: sequential implementation of the level-3 features (§5) on top of the
423
+ I7 slot, in the order of §5.6.
424
+
425
+ ---
426
+
427
+ ## 8. Invariant principles (guardrails)
428
+
429
+ - **We do not touch the attention kernel.** Everything is engine parameters,
430
+ orchestration, or an own service with KV management.
431
+ - **Behavior via `capabilities`, not `isinstance`.** Adding a level = flags in one
432
+ place.
433
+ - **Loud refusal, not a silent fallback** (`CapabilityUnsupported`).
434
+ - **One active engine globally** (radio). Per-call routing comes later.
435
+ - **Subprocess-ready from day one.** Even in-process llama.cpp does not assume
436
+ in-process in the contract.
437
+ - **A shared backing store** for text-memory (§4.2) and KV-memory (§5.5) —
438
+ `workspace/.context-memory/`.
439
+ - **We do not claim mechanism novelty** (§6). Our bet is integration,
440
+ cross-platform reach, and the speed of assembling the full stack.
441
+ - **Research gives principles; we implement them where we have control.**
442
+ Kernel-level papers are a source of ideas for the application/service level, not
443
+ a base.
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to Palimpsests are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
+ aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
6
+ once it reaches v1.0. Before v1.0, minor versions may include breaking
7
+ API changes.
8
+
9
+ ## [0.2.0] — 2026-07-06
10
+
11
+ The three-level architecture is now structurally complete: all three
12
+ control levels exist behind one `InferenceEngine` contract.
13
+
14
+ ### Added
15
+
16
+ - **Level 2 — llama.cpp adapter (`LlamaCppEngine`).** The first *control*
17
+ level: Palimpsests spawns and owns a `llama-server` subprocess, so the
18
+ full `EngineMemoryConfig` (context size, GPU offload, flash attention,
19
+ KV-cache quantization, mmap, draft model) is applied as real launch
20
+ flags rather than ignored. Two modes: spawn (own the server from a
21
+ model path) and attach (talk to a user-run server by URL). Opt-in via
22
+ `PALIMPSESTS_LLAMACPP_MODEL`.
23
+ - **Managed subprocess lifecycle (`LlamaServerProcess`).** Free-port
24
+ allocation, spawn, readiness by health poll, early-death detection, and
25
+ idempotent shutdown — scoped to `llama-server` for now.
26
+ - **Level 3 slot — `NativeEngine`.** A registered, honest placeholder:
27
+ `control_level=3` with every feature flag `False`, every operation
28
+ refusing with `CapabilityUnsupported`, and `is_available()` `False`.
29
+ The serving service (continuous batching, shared-prefix KV, server-side
30
+ tool loop, KV persistence) is not implemented yet.
31
+ - **Block-memory retrieval (`BlockMemory`).** Evicted context is embedded
32
+ and stored in SQLite; the most relevant blocks are retrieved back on
33
+ demand (numpy cosine, no vector DB). Injectable embedder, defaulting
34
+ through the active engine's `/api/embeddings`. Backing store shared with
35
+ future KV persistence under `<workspace>/.context-memory/`.
36
+ - **Block memory wired into the chat flow.** `chat` now stores evicted
37
+ messages and, lazily (only when eviction happened), retrieves relevant
38
+ blocks back as a single prepended system message. Graceful: without an
39
+ embed-capable engine or numpy, chat behaves exactly as before.
40
+ - **Ollama embeddings.** `OllamaEngine.embed()` exposes `/api/embeddings`,
41
+ the default source for block-memory vectors.
42
+ - **`docs/USAGE.md`** — a run + settings guide for the current state.
43
+
44
+ ### Changed
45
+
46
+ - `AppContext.engines` widened from `OllamaEngine` to the `InferenceEngine`
47
+ protocol now that multiple adapters coexist. Callers read capabilities,
48
+ never the concrete type, so nothing downstream changed.
49
+ - The `[llamacpp]` extra is now empty and documented: the server-subprocess
50
+ approach needs the `llama-server` binary out-of-band, not a Python
51
+ package. `numpy` moved to its own `[embeddings]` extra (and `[dev]`).
52
+ - Development status classifier is Beta; README, roadmap, and install
53
+ instructions updated to reflect levels 1–2 shipped and the level-3 slot.
54
+
55
+ ## [0.1.0] — 2026-07-06
56
+
57
+ Initial release.
58
+
59
+ ### Added
60
+
61
+ - **Level 1 — Ollama adapter (`OllamaEngine`).** Thin HTTP client to an
62
+ external Ollama daemon: streaming chat, model listing, availability
63
+ probe, and the subset of `EngineMemoryConfig` Ollama honors.
64
+ - **The engine contract.** `InferenceEngine` protocol, `BaseInferenceEngine`
65
+ (derives `chat` from `chat_stream`, refuses sessions by default),
66
+ `EngineCapabilities`, `EngineMemoryConfig` (with the flash-attention
67
+ prerequisite for KV-quant enforced), and the level-3 `InferenceSession`
68
+ protocol.
69
+ - **Context-window manager.** Sink/window/evict fitting to a token budget,
70
+ reporting what it evicted.
71
+ - **Registry** — one active engine globally (radio, not checkbox).
72
+ - **Audit log** — append-only, encrypted at rest (SQLCipher) with a key
73
+ from the OS keychain, falling back to an ephemeral key headless.
74
+ - **CLI** — `chat`, `models`, `engine list` / `engine use`.
75
+
76
+ [0.2.0]: https://github.com/Assault-Consulting/Palimpsests/releases/tag/v0.2.0
77
+ [0.1.0]: https://github.com/Assault-Consulting/Palimpsests/releases/tag/v0.1.0