icyloom 1.0.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 (64) hide show
  1. icyloom-1.0.0/LICENSE +21 -0
  2. icyloom-1.0.0/PKG-INFO +580 -0
  3. icyloom-1.0.0/README.md +535 -0
  4. icyloom-1.0.0/pyproject.toml +69 -0
  5. icyloom-1.0.0/setup.cfg +4 -0
  6. icyloom-1.0.0/src/icyloom/__init__.py +10 -0
  7. icyloom-1.0.0/src/icyloom/__main__.py +8 -0
  8. icyloom-1.0.0/src/icyloom/admin.py +225 -0
  9. icyloom-1.0.0/src/icyloom/breaker.py +157 -0
  10. icyloom-1.0.0/src/icyloom/cache.py +237 -0
  11. icyloom-1.0.0/src/icyloom/cli.py +399 -0
  12. icyloom-1.0.0/src/icyloom/clientcheck.py +225 -0
  13. icyloom-1.0.0/src/icyloom/config.py +233 -0
  14. icyloom-1.0.0/src/icyloom/context/__init__.py +12 -0
  15. icyloom-1.0.0/src/icyloom/context/anchors.py +149 -0
  16. icyloom-1.0.0/src/icyloom/context/audit.py +92 -0
  17. icyloom-1.0.0/src/icyloom/context/compress.py +119 -0
  18. icyloom-1.0.0/src/icyloom/context/drawer.py +300 -0
  19. icyloom-1.0.0/src/icyloom/context/ratelimit.py +81 -0
  20. icyloom-1.0.0/src/icyloom/context/redact.py +82 -0
  21. icyloom-1.0.0/src/icyloom/context/sandbox.py +184 -0
  22. icyloom-1.0.0/src/icyloom/context/sessions.py +166 -0
  23. icyloom-1.0.0/src/icyloom/context/store.py +341 -0
  24. icyloom-1.0.0/src/icyloom/context/verify.py +56 -0
  25. icyloom-1.0.0/src/icyloom/dashboard.py +710 -0
  26. icyloom-1.0.0/src/icyloom/data/normalization_rules.json +5832 -0
  27. icyloom-1.0.0/src/icyloom/data/tools_registry.json +10 -0
  28. icyloom-1.0.0/src/icyloom/db.py +416 -0
  29. icyloom-1.0.0/src/icyloom/gateway.py +796 -0
  30. icyloom-1.0.0/src/icyloom/ledger.py +166 -0
  31. icyloom-1.0.0/src/icyloom/logsetup.py +48 -0
  32. icyloom-1.0.0/src/icyloom/mcpserver.py +462 -0
  33. icyloom-1.0.0/src/icyloom/migrate.py +131 -0
  34. icyloom-1.0.0/src/icyloom/normalize.py +278 -0
  35. icyloom-1.0.0/src/icyloom/paths.py +200 -0
  36. icyloom-1.0.0/src/icyloom/pool.py +103 -0
  37. icyloom-1.0.0/src/icyloom/py.typed +0 -0
  38. icyloom-1.0.0/src/icyloom/runner.py +76 -0
  39. icyloom-1.0.0/src/icyloom/static/dashboard.html +2462 -0
  40. icyloom-1.0.0/src/icyloom/static/logo.svg +68 -0
  41. icyloom-1.0.0/src/icyloom/tokenizer.py +75 -0
  42. icyloom-1.0.0/src/icyloom/toolproxy.py +788 -0
  43. icyloom-1.0.0/src/icyloom/upstream.py +141 -0
  44. icyloom-1.0.0/src/icyloom.egg-info/PKG-INFO +580 -0
  45. icyloom-1.0.0/src/icyloom.egg-info/SOURCES.txt +62 -0
  46. icyloom-1.0.0/src/icyloom.egg-info/dependency_links.txt +1 -0
  47. icyloom-1.0.0/src/icyloom.egg-info/entry_points.txt +2 -0
  48. icyloom-1.0.0/src/icyloom.egg-info/requires.txt +21 -0
  49. icyloom-1.0.0/src/icyloom.egg-info/top_level.txt +1 -0
  50. icyloom-1.0.0/tests/test_cache_correctness.py +234 -0
  51. icyloom-1.0.0/tests/test_client.py +92 -0
  52. icyloom-1.0.0/tests/test_context.py +194 -0
  53. icyloom-1.0.0/tests/test_dashboard.py +151 -0
  54. icyloom-1.0.0/tests/test_db_schema.py +17 -0
  55. icyloom-1.0.0/tests/test_gateway.py +256 -0
  56. icyloom-1.0.0/tests/test_mcp_transports.py +342 -0
  57. icyloom-1.0.0/tests/test_normalize.py +166 -0
  58. icyloom-1.0.0/tests/test_proofs.py +170 -0
  59. icyloom-1.0.0/tests/test_providers.py +220 -0
  60. icyloom-1.0.0/tests/test_registry.py +226 -0
  61. icyloom-1.0.0/tests/test_toolcalling_all.py +173 -0
  62. icyloom-1.0.0/tests/test_toolcalling_gemini.py +256 -0
  63. icyloom-1.0.0/tests/test_toolproxy.py +50 -0
  64. icyloom-1.0.0/tests/test_toolproxy_canonical.py +119 -0
icyloom-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Malik Belkacemi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
icyloom-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,580 @@
1
+ Metadata-Version: 2.4
2
+ Name: icyloom
3
+ Version: 1.0.0
4
+ Summary: Unified local gateway: LLM cache + compressed context drawer, in a single MCP server
5
+ Author: Malik Belkacemi
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/MlkBlk-IT/icy-loom
8
+ Project-URL: Repository, https://github.com/MlkBlk-IT/icy-loom
9
+ Project-URL: Issues, https://github.com/MlkBlk-IT/icy-loom/issues
10
+ Project-URL: Changelog, https://github.com/MlkBlk-IT/icy-loom/blob/main/CHANGELOG.md
11
+ Keywords: mcp,mcp-server,cache,llm,proxy,context,local-first
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS :: MacOS X
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Internet :: Proxy Servers
23
+ Requires-Python: <3.14,>=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: mcp<2,>=1.2
27
+ Requires-Dist: fastapi<1,>=0.115
28
+ Requires-Dist: uvicorn>=0.30
29
+ Requires-Dist: httpx<1,>=0.27
30
+ Requires-Dist: sse-starlette>=2.1
31
+ Requires-Dist: websockets>=12.0
32
+ Requires-Dist: pydantic>=2.9
33
+ Requires-Dist: pyyaml>=6.0
34
+ Requires-Dist: pyahocorasick>=2.1
35
+ Requires-Dist: tiktoken>=0.7
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=8; extra == "dev"
38
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
39
+ Provides-Extra: qdrant
40
+ Requires-Dist: qdrant-client>=1.9; extra == "qdrant"
41
+ Provides-Extra: vector
42
+ Requires-Dist: numpy>=1.24; extra == "vector"
43
+ Requires-Dist: faiss-cpu>=1.7; extra == "vector"
44
+ Dynamic: license-file
45
+
46
+ <div align="center">
47
+
48
+ <p align="center"><img src="https://raw.githubusercontent.com/MlkBlk-IT/icy-loom/main/docs/mlkblk-banner.svg" width="100%" alt="icy-loom — Malik Belkacemi · MLKBLK-IT" /></p>
49
+
50
+ # IcyLoom
51
+
52
+ **One MCP server. One SQLite file. One ledger that tells you what it actually saved.**
53
+
54
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-46231A?style=flat-square&labelColor=FDF3E3&logo=python&logoColor=FFC93E)](https://www.python.org/)
55
+ [![License](https://img.shields.io/badge/license-MIT-FF5A36?style=flat-square&labelColor=FDF3E3&logo=open-source-init&logoColor=white)](LICENSE)
56
+ [![MCP](https://img.shields.io/badge/MCP-13%20tools-0FB5A6?style=flat-square&labelColor=FDF3E3)](src/icyloom/mcpserver.py)
57
+ [![Tests](https://img.shields.io/badge/tests-131%20passing-22B784?style=flat-square&labelColor=FDF3E3&logo=pytest&logoColor=white)](tests/)
58
+ [![Bind](https://img.shields.io/badge/bind-127.0.0.1%20only-FFC93E?style=flat-square&labelColor=FDF3E3&logo=lock&logoColor=46231A)](icyloom.yaml)
59
+
60
+ </div>
61
+
62
+ ---
63
+
64
+ A local gateway that puts an **LLM cache**, a **compressed context drawer** and an **MCP tool relay** behind **one MCP server**, **one configuration file** and **one SQLite database**.
65
+
66
+ IcyLoom is a **standalone** project: no imports, links or paths to any other project. The code is written from scratch; the only files taken from elsewhere are **data** (normalisation rules, a tool registry), because the cache hash depends on them.
67
+
68
+ | | |
69
+ |---|---|
70
+ | **Cache** | Every prompt normalised, hashed and stored. A hit is a **zero-token** answer. |
71
+ | **Context drawer** | Sources filed, chunked, anchored, invariants checked — compressed on demand, opt-in. |
72
+ | **Tool relay** | Downstream MCP servers, cached by canonical signature, **deny by default**. |
73
+ | **Registry** | `v_token_ledger`: tokens **paid upstream** and tokens **avoided**, in one unit, one view. |
74
+ | **Console** | A hand-drawn web console on `:8153` — side panel, live views, working buttons. |
75
+ | **Privacy** | Binds `127.0.0.1`. Nothing leaves the machine. |
76
+
77
+ **The numbers are real.** A cache hit is counted as *avoided*, a miss is counted as *paid* with the upstream's own usage figures — never estimated. The 35 % threshold is read straight from the ledger.
78
+
79
+ <img src="docs/screenshots/overview.png" alt="IcyLoom console — overview" width="860" />
80
+
81
+ <sub>Overview: the unified registry, five live KPIs, calls per bucket, latency, outcomes, models, upstreams. Captured on a demo database seeded by `tools/seed_demo.py` (synthetic traffic) — not production data.</sub>
82
+
83
+ ## Quick start
84
+
85
+ Five minutes, on a clean machine. No GPU, no paid API, no LM Studio, no Node,
86
+ no `uvx`.
87
+
88
+ ### Linux / macOS
89
+
90
+ ```bash
91
+ python3 -m venv venv
92
+ source venv/bin/activate
93
+ python -m pip install --upgrade pip
94
+ python -m pip install https://github.com/MlkBlk-IT/icy-loom/releases/download/v1.0.0/icyloom-1.0.0-py3-none-any.whl
95
+
96
+ icyloom init
97
+ icyloom doctor
98
+
99
+ # in a second terminal
100
+ icyloom serve
101
+ icyloom check --transport all
102
+ ```
103
+
104
+ ### Windows (PowerShell)
105
+
106
+ ```powershell
107
+ py -3.12 -m venv venv
108
+ .\venv\Scripts\Activate.ps1
109
+ python -m pip install --upgrade pip
110
+ python -m pip install https://github.com/MlkBlk-IT/icy-loom/releases/download/v1.0.0/icyloom-1.0.0-py3-none-any.whl
111
+
112
+ icyloom init
113
+ icyloom doctor
114
+
115
+ # in a second terminal
116
+ icyloom serve
117
+ icyloom check --transport all
118
+ ```
119
+
120
+ Without the wheel (git required):
121
+
122
+ ```bash
123
+ python -m pip install "icyloom @ git+https://github.com/MlkBlk-IT/icy-loom.git@v1.0.0"
124
+ ```
125
+
126
+ Expected output of `icyloom check`: `21/21 vérifications réussies`.
127
+
128
+ `icyloom check` plays the role of the client: it starts the server, connects,
129
+ lists the 13 tools and calls them (`ctx_store`, `ctx_search`, `ctx_verify`,
130
+ `cache`, `ledger`, `proxy_info`). It tests the **three** transports (stdio,
131
+ streamable-HTTP, SSE) and prints `21/21 vérifications réussies` when all is
132
+ well. If HTTP or SSE fails, the stack is not running (`icyloom serve`); the
133
+ **stdio** mode, on the other hand, is self-contained — it is the one Claude
134
+ Code and Cursor use.
135
+
136
+ Then plug IcyLoom into your MCP client:
137
+
138
+ ```bash
139
+ icyloom client --target cursor --install # writes the config on your machine
140
+ icyloom client --target claude # prints the block to paste
141
+ ```
142
+
143
+ Targets: `cursor`, `claude` (Desktop), `claude-code`, `windsurf`, `codex`,
144
+ `json` (raw block).
145
+
146
+ ```
147
+ icyloom.yaml single configuration (Pydantic)
148
+ src/icyloom/data/ externalised data, shipped inside the package (JSON)
149
+ var/icyloom.db single SQLite database (WAL)
150
+
151
+ HTTP gateway :8150 MCP server (one build function)
152
+ /v1/chat/completions stdio dedicated process
153
+ /v1/messages streamable-HTTP :8151
154
+ /v1/models, /v1/embeddings SSE :8152
155
+ /{upstream}/v1/... 13 tools, one surface
156
+ cache -> routing -> upstream
157
+
158
+ admin API + dashboard :8153
159
+ gateway, MCP and admin all write to one database and one ledger
160
+ ```
161
+
162
+ Outside a repository checkout, nothing is written inside `site-packages`: the
163
+ configuration, the database and the logs go to the user data directory —
164
+ `%LOCALAPPDATA%\icyloom` on Windows, `$XDG_DATA_HOME/icyloom` (or
165
+ `~/.local/share/icyloom`) on Linux and macOS. Override it with
166
+ `ICYLOOM_DATA_DIR`.
167
+
168
+ ## Status & known limitations
169
+
170
+ This section is the honest summary. Raw measurements live in
171
+ [`docs/archive/PROGRESS.md`](docs/archive/PROGRESS.md).
172
+
173
+ | Area | Status |
174
+ |---|---|
175
+ | Installation from a wheel on a clean machine | **Verified.** `init`, `doctor` (exit 0), `serve`, `check` 21/21, from a venv outside the repository |
176
+ | Test suite | **137** tests pass (131 + 6 new proofs and regressions) |
177
+ | Cache round trip (miss then hit) | **Verified by an automated test**: the first request calls the upstream, the identical second request does not, the served response is identical, and the ledger records both |
178
+ | Tool relay | **Verified by an automated test** against a real MCP stdio server: the tool is reached, it answers, and the second identical call is served from the cache |
179
+ | Tool-result cache correctness (stale hits, replayed writes, cached errors, loop detection) | **Verified.** Synthetic harness: 0 stale hits out of 140 (before the fix: 425 out of 436). Real `fetch` server: 8 of 8 checks pass |
180
+ | Gateway latency | p50 13.5 ms (MISS) and 10.7 ms (HIT); p95 about 16 ms against a 200 ms budget |
181
+ | Latency gain on a slow tool | One real `fetch` call: 1527 ms first, 24 ms when repeated (one static page) |
182
+ | **Token savings on tool calls** | **Not delivered and not measured.** A tool-cache hit is ledgered with 0 tokens saved |
183
+ | Context compression | **Opt-in, off by default.** `ctx_verify` proves the invariants survive a round trip, but **answer quality is not proven** (see Known limitations) |
184
+ | Python versions | 3.11, 3.12, 3.13 supported. Python 3.14 is **not** in the supported range yet |
185
+
186
+ The "50 percent tokens saved" printed by `tests/proof_e2e.py` is true by construction: that test sends every request twice. It is not a traffic figure and must not be quoted as one.
187
+
188
+ ## Getting started
189
+
190
+ ```bash
191
+ icyloom doctor # diagnostics
192
+ icyloom serve # gateway + MCP + admin
193
+ icyloom mcp --transport stdio # for an MCP client
194
+ icyloom gateway # gateway only (8150)
195
+ icyloom admin # admin API only (8153)
196
+ icyloom paths # every resolved path
197
+ ```
198
+
199
+ An MCP client connects with a single entry, for example:
200
+
201
+ ```json
202
+ { "mcpServers": { "icyloom": { "command": "/path/to/venv/bin/python",
203
+ "args": ["-m", "icyloom", "mcp", "--transport", "stdio"] } } }
204
+ ```
205
+
206
+ An LLM client points its `base_url` at `http://127.0.0.1:8150/v1`.
207
+
208
+ `icyloom doctor` also probes each enabled upstream (`/models`) and checks that the tool-server executables are on the PATH. A missing upstream or a missing tool executable is a **warning**, not a failure: a default installation has neither, and still works. When `server.remote_access_token` is set, every admin and gateway route except `/health` requires it, and a non-loopback `server.host` is refused without one.
209
+
210
+ ## The tool cache
211
+
212
+ IcyLoom relays calls to downstream MCP servers and can cache their results. The rules:
213
+
214
+ 1. **Deny by default.** Only tools set to `true` in a server's `cache_tools` are ever replayed from the cache. There is no guessing from the tool name. A server with `cache_enabled: true` and no `cache_tools` caches nothing.
215
+ 2. **State epoch per server.** Any call that is not cacheable (a possible write) advances that server's epoch. The epoch is part of the cache key, so a read cached before a write is never served after it. The epoch also carries a boot token: the cache does not survive a relay restart, because the server may have changed while the relay was down.
216
+ 3. **Errors are never cached.** A result flagged `is_error` is not stored.
217
+ 4. **Schema-guided canonicalisation.** A near-identical call is recognised only according to the tool's `inputSchema`: integer, number and boolean parameters, and path-like string parameters. Without a schema, only an exactly identical call matches. Never: case changes, whitespace folding inside strings, reordering of lists. Schemas are loaded when `list_tools` is called.
218
+ 5. **The loop guard counts cache hits.** A loop answered from the cache is still a loop: the 5th identical call within the window is blocked.
219
+ 6. **A batch stops at the first error.** The remaining calls are reported as `skipped_after_error`.
220
+
221
+ ```yaml
222
+ tool_proxy:
223
+ servers:
224
+ - name: fetch
225
+ command: uvx
226
+ args: ["mcp-server-fetch@2026.10.10"]
227
+ cache_enabled: true
228
+ ttl_seconds: 3600 # protects against changes made outside the relay
229
+ cache_tools:
230
+ fetch: true # explicit allowlist: nothing else is cached
231
+ ```
232
+
233
+ The TTL is the only protection against a downstream server whose state changes outside the relay. Choose it per server.
234
+
235
+ ### Loading tools fast
236
+
237
+ Two things make the relay quick to use, both verified by `tools\proof_tools.py`:
238
+
239
+ 1. **One live session per downstream server.** The server process stays alive between calls instead of being respawned for every call. The session lives in its own thread and its own event loop, because the MCP SDK opens a stdio session inside an anyio task group that must be entered and left in the same task. Measured on a local server: the first call costs ~840 ms (process start), the next ones ~21 ms — **40× faster**.
240
+ 2. **The tool catalogue is cached and searchable.** `proxy_info(what="tools", server=…)` lists the tools of a server and keeps the result (with the `inputSchema`) in memory; `proxy_info(what="search", query=…)` then finds a tool by name or description — "git" finds `git_log`, "file" finds `read_file`. The catalogue is also loaded automatically on the first call to an unknown tool, because the schema is what makes the near-identical recognition possible.
241
+
242
+ ```powershell
243
+ .\.venv\Scripts\python.exe tools\proof_tools.py # 14 checks: near-hit, no false hit, reuse, search
244
+ ```
245
+
246
+ The schema-guided merge is deliberately conservative: `limit: 10` and `limit: "10"` are one and the same call (the schema declares an integer), but `" 8150 "` and `"8150"` are **not** merged (a string is a value — the downstream tool answered "unknown port" for the padded one).
247
+
248
+ ## The console (dashboard)
249
+
250
+ A web console, served by the admin API, shows **all** metrics with matching charts:
251
+
252
+ ```
253
+ http://127.0.0.1:8153/dashboard (alias: http://127.0.0.1:8153/)
254
+ ```
255
+
256
+ ```powershell
257
+ .\.venv\Scripts\python.exe -m icyloom dashboard # URL + current state
258
+ .\.venv\Scripts\python.exe -m icyloom dashboard --serve # serve the console
259
+ ```
260
+
261
+ ![IcyLoom console](docs/images/dashboard.png)
262
+
263
+ *The console on a demo database seeded by `tools/seed_demo.py` (synthetic traffic) — not production data.*
264
+
265
+ ### One console, two consoles fused
266
+
267
+ The console is the **merge of the IcyCache console and the Loom dash**, rebuilt on the
268
+ unified registry `v_token_ledger` and drawn in the rough-sketch style (warm paper, brown
269
+ ink, hand-drawn strokes, hatched fills, stickers, doodles).
270
+
271
+ It is an **application shell**, not a long scrollable page: a **side panel on the left
272
+ switches the view**, and the content area only ever renders the view you are on.
273
+
274
+ | View | Fused from | Content |
275
+ |---------|-----------|---------|
276
+ | Overview | IcyCache + Loom | 5 KPIs with sparklines, the unified registry card (tokens paid / avoided / baseline, latency avoided, 35 % threshold), calls per bucket, latency p50/p95, hit/miss donut, per-model breakdown, upstreams & clients |
277
+ | Activity | IcyCache | **3D traffic mountain** (day × hour, drag to rotate), topographic map with isolines, hourly traffic table, latest requests |
278
+ | Economy | IcyCache + Loom | 14-day curve (paid vs avoided, hatched area, dashed average, circled record), **gain gauge against the 35 % line**, split of the baseline per domain, economy per day |
279
+ | Cache | IcyCache | most reused entries (hatched pseudo-3D bars), searchable table of every stored prompt (hash, model, provider, hits) |
280
+ | Context | Loom | sessions, sources, chunks, invariants, anchors, redactions, stored sources table, sources per session |
281
+ | Tools | Loom | downstream MCP servers, calls per tool, tool cache (entries, bytes, latency avoided), per-tool breakdown |
282
+ | Audit | Loom | live LLM journal and tool journal, both searchable |
283
+ | Guide | IcyCache Help | request-path diagram, how to read the metrics, the 13 MCP tools, shortcuts & tips, live system state |
284
+
285
+ The **side panel** carries the eight views (each with its own live counter), the system
286
+ actions — **Pause / Refresh / Export / Compact**, which really act — and a live status card
287
+ (calls, hit rate, tokens avoided, gain, ledger rows, database size). A **toolbar** above the
288
+ content shows the current view, the `LIVE` state, the measured request rate and the event
289
+ count; the **control bar** holds the **analysis window** (1 h / 24 h / 7 d / 14 d — it really
290
+ changes the query), a Reconnect button and the live database pills.
291
+
292
+ Every button is wired to a real action: Pause freezes the figures (the polling stops, the
293
+ pill turns `PAUSED`), Refresh and Reconnect re-query the admin API, Export downloads the
294
+ exact JSON the page is drawing, Compact shrinks the layout for a laptop screen. Keyboard
295
+ shortcuts: `1`…`8` switch view, `R` refreshes, `P` pauses, `E` exports, `C` compacts.
296
+
297
+ <div align="center">
298
+ <img src="docs/screenshots/activity.png" alt="Activity view" width="430" />
299
+ <img src="docs/screenshots/economy.png" alt="Economy view" width="430" />
300
+ <br/>
301
+ <img src="docs/screenshots/cache.png" alt="Cache view" width="430" />
302
+ <img src="docs/screenshots/guide.png" alt="Guide view" width="430" />
303
+ </div>
304
+
305
+ <sub>The eight views: <b>Activity</b> (3D mountain + topo map), <b>Economy</b> (curve, gauge, split),
306
+ <b>Cache</b> (entries, ranking), <b>Guide</b> (path of a request). Context, Tools and Audit complete
307
+ the set — see `docs/screenshots/`.</sub>
308
+
309
+ **Everything is computed in real time**; no value is frozen in the page:
310
+
311
+ * `/api/dashboard` is polled every 2 s; the server keeps the snapshot fresh in a background
312
+ thread, so the answer is instant (≈ 10 ms) even on a busy base;
313
+ * requests/min is computed between two real samples and smoothed;
314
+ * each value is interpolated toward its new target, so counters scroll instead of jumping;
315
+ * the window and the pause button apply immediately;
316
+ * every timestamp is **UTC** — one single clock, from the SQLite defaults to the charts.
317
+
318
+ The data comes from a single read-only route:
319
+
320
+ ```
321
+ GET /api/dashboard -> { kpi, config, registry, surface, series, breakdown, tables, context, recent, db }
322
+ ```
323
+
324
+ `registry` is the unified `v_token_ledger` (cache + compression + tool tax, all in tokens),
325
+ `surface` is the day × hour traffic grid behind the 3D mountain and the map, `series` holds
326
+ the time series (per hour over 24 h, per day over 14 d). `GET /api/gateway` relays the
327
+ gateway's live state (circuits, upstreams) from port 8150 so the page stays on a single origin.
328
+
329
+ The console is **self-contained**: one HTML file, hand-written SVG charts and a hand-written
330
+ sketch engine (no rough.js, no framework), no external dependency, no CDN, no network call
331
+ except `/api/dashboard`. The tokens shown are the **real** tokens reported by the upstream.
332
+
333
+ Reference screenshots (1440 × 960) live in [`docs/screenshots/`](docs/screenshots):
334
+ `overview`, `activity`, `economy`, `cache`, `context`, `tools`, `audit`, `guide`.
335
+ The logo is [`src/icyloom/static/logo.svg`](src/icyloom/static/logo.svg) — the same file is
336
+ served by the console at `http://127.0.0.1:8153/assets/logo.svg`.
337
+
338
+ Demo data set (fake data, isolated stacks, ports 8150-8154):
339
+
340
+ ```powershell
341
+ .\.venv\Scripts\python.exe tools\seed_demo.py # continuous traffic, about 4 req/s
342
+ .\.venv\Scripts\python.exe tools\check_js.py # JavaScript syntax check of the console
343
+ ```
344
+
345
+ ## The functional test campaign
346
+
347
+ Re-run on 2026-10-28 with the current code: **88/88 checks green** (report in `var\campaign-report.json`).
348
+
349
+ A hermetic campaign that exercises **every** layer on an isolated stack (ports 8160-8164, temporary database), with entirely different inputs: 24 distinct prompts, 6 distinct documents, 3 distinct markers, 3 distinct tool calls:
350
+
351
+ ```powershell
352
+ .\.venv\Scripts\python.exe tools\campaign.py # report + var\campaign-report.json
353
+ ```
354
+ It covers the cache (variants, anti-collision, key scope), the ledger (real tokens, gain), the context drawer (byte-exact, invariants, discriminating search, isolated sessions), the tool relay, the gateway formats (OpenAI, Anthropic, SSE streaming), MCP HTTP/SSE parity, the safeguards (confirmation, concurrency) and the consistency of the console with the database. Output: one verdict per check and a non-zero exit code at the first failure.
355
+
356
+ Two blocks play the role of an **agent** doing real work:
357
+
358
+ - **N, agentic work:** the agent looks up its spec in the drawer, files a real document, asks **8 really different requests** (plan, risks, test, talk, threat, rollback, measure, strategy), iterates one request ("v1" then "v2"), then calls **6 distinct downstream tools** and closes with an invariants check.
359
+ - **O, anti-mixing:** interleaved requests and tools, purge, replay.
360
+
361
+ To prove that a hit returns the **right** answer, the campaign uses `tools/echo_upstream.py`: the upstream returns the fingerprint of the request it received (`echo:<sha256>`). An answer served from the cache therefore carries the fingerprint of its own request, never another one's.
362
+
363
+ ## The 13 tools
364
+
365
+ | Tool | Role | Destructive action |
366
+ |------|------|--------------------|
367
+ | `cache(action)` | `stats` · `search` · `normalize` · `clear` | `clear` needs `confirm=true` |
368
+ | `tool_proxy(calls[])` | relay to downstream MCP servers, in a batch (stops at the first error) | none |
369
+ | `proxy_info(what)` | `servers` · `tools` · `stats` · `loop` | none |
370
+ | `tool_cache_clear(server)` | purge the tool cache | yes, needs `confirm=true` |
371
+ | `ctx_store(source)` | `text` · `file` · `cmd`: stores the bulk of the context | none |
372
+ | `ctx_get(chunk_id)` | re-reads the original bytes (byte-exact) | none |
373
+ | `ctx_search(query)` | search the drawer | none |
374
+ | `ctx_verify(session)` | proof that no invariant was lost | none |
375
+ | `ctx_check(session)` | loss and fidelity check | none |
376
+ | `ctx_profile(mode)` | `strict` (default) · `balanced` · `aggressive` | none |
377
+ | `ctx_session(action)` | `list` · `purge` · `transfer` | `purge` needs `confirm=true` |
378
+ | `ctx_admin(action)` | `stats` · `health` · `vacuum` · `purge_expired` · `rebuild_index` | `vacuum` / `purge_expired` / `rebuild_index` need `confirm=true` |
379
+ | `ledger(view)` | `summary` · `daily`: measured cost and gain | none |
380
+
381
+ Every tool carries `ToolAnnotations` (read-only, destructive, idempotent, open-world). A destructive action without `confirm=true` is refused **with no side effect**.
382
+
383
+ ## Capability map: tools and routes
384
+
385
+ *(Capabilities from spec section 5. `✔` = covered and tested · `◐` = partly covered · `✘` = not implemented, announced)*
386
+
387
+ ### Cache and network
388
+
389
+ | # | Capability | Where |
390
+ |---|------------|-------|
391
+ | A1 | OpenAI `/v1/chat/completions` | `gateway.py` ✔ (tested) |
392
+ | A2 | Anthropic `/v1/messages` | `gateway.py` ✔ |
393
+ | A3 | `/v1/embeddings`, `/v1/models` | `gateway.py` ✔ |
394
+ | A4 | routing `/{upstream}/v1/...` | `gateway.py` ✔ |
395
+ | A5 | priority: URL prefix > header > body > default, plus `sticky` | `upstream.py` ✔ |
396
+ | A6 | canonical normalisation + hash | `normalize.py` ◐ (1122 rules; 70 % agreement with the old cache, see below) |
397
+ | A7 | LLM cache (TTL, LRU, cap) | `cache.py` ✔ |
398
+ | A8 | semantic cache (Qdrant) | ✘ opt-in, not implemented (config and cache key ready) |
399
+ | A9 | **real** tokens (never estimated on the upstream side) | `gateway.py`, `ledger.py` ✔ |
400
+ | A10 | prefix cache | ◐ `prefix_events` table present, proxy not implemented |
401
+ | A11 | tool relay (single + batch) | `toolproxy.py` ✔ (batch stops at the first error) |
402
+ | A12 | tool cache: per-server TTL, **deny-by-default allowlist**, state epoch, no cached errors, schema-guided canonicalisation | `toolproxy.py` ✔ (synthetic harness + real `fetch` server, 2026-10-27) |
403
+ | A13 | loop guard (**counts cache hits**) | `toolproxy.py` ✔ |
404
+ | A14 | resilient pool: drain, recycle, retry | `pool.py` ✔ |
405
+ | A15 | circuit breaker **with real re-arming** | `breaker.py` ✔ |
406
+ | A16 | WebSocket `/ws/events` | ✘ not implemented |
407
+ | A17 | REST API (health, status, metrics, cache-clear, tools, ledger...) | `gateway.py`, `admin.py` ✔ |
408
+ | A18 | HTML dashboard | `dashboard.py` + `static\` ✔ (covered by `tests/test_dashboard.py`) |
409
+ | A19 | stdio / SSE / streamable-HTTP transports | `mcpserver.py` ✔ (all 3 tested) |
410
+ | A20 | tool registry, assertion `total == 100` | `cli.py doctor` ✔ |
411
+ | A21 | IcyCache's 11 tool-relay tools | merged into the 13 (`cache`, `tool_proxy`, `proxy_info`, `tool_cache_clear`) ✔ |
412
+ | A22 | CLI `doctor`/`paths`/`version`/`serve`/`mcp`/`gateway`/`admin`/`migrate`/`init`/`config get` | `cli.py` ✔ |
413
+ | A23 | cache warm-up (`warmup`) | ✘ not implemented |
414
+ | A24 | stdout = pure JSON-RPC, logs on stderr, UTF-8 forced | `logsetup.py`, `mcpserver.py` ✔ (tested) |
415
+
416
+ ### Context
417
+
418
+ | # | Capability | Where |
419
+ |---|------------|-------|
420
+ | B1 | 17 context tools | merged into the 13 (`ctx_*`) ✔ |
421
+ | B2 | 3 profiles, `strict` lossless by default (for invariants; see Known limitations) | `context/compress.py` ✔ |
422
+ | B3 | invariants (numbers, paths, URLs, versions, fingerprints, **ports**, dates, constraints) | `context/anchors.py` ✔ |
423
+ | B4 | `ctx_verify`: proven coverage | `context/verify.py` ✔ |
424
+ | B5 | FTS5 drawer + search | `context/drawer.py` ✔ (vector search ◐ absent) |
425
+ | B6 | UUIDv7 sessions, TTL, isolation, purge | `context/sessions.py` ✔ |
426
+ | B7 | cross-session transfer | `context/store.py` ✔ |
427
+ | B8 | database / column encryption | ✘ not implemented |
428
+ | B9 | secret redaction before storage | `context/redact.py` ✔ |
429
+ | B10 | fail-closed sandbox | `context/sandbox.py` ✔ |
430
+ | B11 | rate limiting | `context/ratelimit.py` ✔ |
431
+ | B12 | JSONL audit + rotation | `context/audit.py` ✔ |
432
+ | B13 | auto-store of a tool output | ✘ not implemented (threshold exposed in the config) |
433
+ | B14 | FAISS vector index | ✘ opt-in, not implemented |
434
+ | B15 | admin CLI (health, stats, sessions, vacuum, purge) | `admin.py`, `cli.py` ✔ |
435
+ | B16 | dashboard | `dashboard.py` + `static\` ✔ |
436
+
437
+ ## The ledger
438
+
439
+ A single `ledger_entries` table records everything that costs and everything that saves: LLM calls (real upstream tokens), cache hits, context compression, tool calls. The **35 %** threshold is readable there continuously:
440
+
441
+ ```
442
+ tokens saved / (tokens paid + tokens saved)
443
+ ```
444
+
445
+ The tokens of an **LLM cache hit** are the **real** tokens measured on the original call: they are never estimated. Compression tokens come from the local counter (tiktoken if available, otherwise an estimator), and the counter used is recorded in every entry.
446
+
447
+ **Tool path:** a tool-cache hit is ledgered with `saved_latency_ms` only; `tool_tokens_saved` is 0. The tool cache saves latency, not tokens.
448
+
449
+ ## Known limitations
450
+
451
+ - **No measured token savings on tool calls.** See the Status section.
452
+ - **Compression quality is not proven.** In a prototype run on a real model (carried over from earlier notes, details in `PROGRESS.md`), estimated prompt tokens dropped by about 84 to 92 percent, but the model answered incorrectly: the anchor keeps only a header, the invariants and the first 800 characters, so a key line in the middle of a log was lost. Compression therefore stays **opt-in**; "strict" guarantees that the invariants are kept, not that the text the model sees is unchanged.
453
+ - The gateway cache uses one slot per key shared by JSON and streaming responses, so a streamed request can miss a hit created by the JSON one.
454
+ - An upstream `ReadTimeout` during streaming is not caught in `gateway.py` (around line 578) and can end a client run.
455
+ - The tool-cache epoch is verified on a synthetic server; a real server with both reads and writes has not been tested yet.
456
+ - Downstream state that changes outside the relay is only covered by the TTL.
457
+
458
+ ## Design decisions
459
+
460
+ 1. **One MCP server, two HTTP processes.** `mcp>=1.2,<2` exposes one transport per instance: HTTP and SSE are therefore two instances built by the **same** function (`build_server`), which guarantees an identical tool surface. stdio stays a dedicated process.
461
+ 2. **Own environment variables.** `ICYLOOM_CONFIG`, `ICYLOOM_HOME`, `ICYLOOM_STATE_DIR`. The historical names from other projects are not reused: the project is independent.
462
+ 3. **Ports 8150-8153** strictly reserved, bound to `127.0.0.1`; tests run on 8154-8159.
463
+ 4. **One transaction per request.** Cache, LLM journal and ledger are written in **one** transaction: three successive transactions cost about 20 ms per request, more than the gain from the cache.
464
+ 5. **Circuit breaker really re-armed:** after `reset_seconds` the state becomes half-open and one probe is allowed.
465
+ 6. **Upstream failover:** a failing upstream no longer permanently excludes the others; the priority is never mutated during a call.
466
+ 7. **Compression off by default**, `strict` profile, never lossy implicitly.
467
+ 8. **Fail-closed sandbox:** without an allowed directory, no command is run.
468
+ 9. **Tool cache: deny by default**, with a per-server state epoch (see The tool cache).
469
+ 10. **No git repository for now:** the licence is still undecided, so the project is not published. The safety net is a folder copy.
470
+
471
+ ## Cache hash compatibility
472
+
473
+ The normalisation engine follows the reference semantics (NFKD, lowercase, Aho-Corasick rules, number-words, diacritics, sentence punctuation). **Operators are kept** (`12+2` is not `1+22`), and volatile fields (`stream`, `user`, `metadata`) are excluded from the key.
474
+
475
+ Measured on the historical cache (read-only): **70 %** of normalised texts coincide. The remaining 30 % were written by an earlier version of the engine that removed `:`, `/`, `+`, `*` and did not convert numbers to words. Those entries are **already** incompatible with the reference engine; honouring them would break compatibility with the current engine. `tests/test_normalize.py` measures this agreement and checks the equivalence between the fast engine and an independent naive implementation.
476
+
477
+ ## Tests
478
+
479
+ ```powershell
480
+ .\.venv\Scripts\python.exe -m pytest tests\ -q --ignore=tests\proof_e2e.py # 102 tests
481
+ .\.venv\Scripts\python.exe tests\proof_e2e.py # end-to-end proof
482
+ .\.venv\Scripts\python.exe tests\test_cache_correctness.py # stale-hit measurement (prints a report)
483
+ .\.venv\Scripts\python.exe tests\real_mcp_fetch.py # real MCP server; needs uvx and Internet
484
+ ```
485
+
486
+ - **`proof_e2e.py`** starts a fake upstream on 8154, launches the stack on 8150-8153, checks the 4 services, verifies the tool list over HTTP and SSE, then measures latency over 200 requests (direct / MISS / HIT) and the ledger gain. Its "50 %" gain is by construction (every request is sent twice).
487
+ - **`test_cache_correctness.py`** runs a stateful fake tool server under interleaved reads and writes and compares every cached answer with the server's true state. Run it as a script to get the numbers, or through pytest for strict thresholds (zero stale hits).
488
+ - **`real_mcp_fetch.py`** drives the real `mcp-server-fetch` through the relay: schema capture, MISS then HIT, integer canonicalisation, no merge across different URLs, a real error never replayed.
489
+ - Tests use a temporary database and never touch `var\icyloom.db`. Set `ICYLOOM_TEST_PORT` to run the suite while a stack occupies 8154.
490
+
491
+ ## Layout
492
+
493
+ ```
494
+ D:\ICY-LOOM\
495
+ ├── icyloom.yaml single configuration
496
+ ├── pyproject.toml package (requires-python >=3.11,<3.14)
497
+ ├── LICENSE MIT
498
+ ├── README.md
499
+ ├── PROGRESS.md what is proven, what is not, what is next
500
+ ├── MERGE_PLAN.md the merge plan (IcyCache + Loom -> IcyLoom)
501
+ ├── RESEARCH.md the research notes behind the choices
502
+ ├── ICYLOOM_PROMPT.md the original brief
503
+ ├── docs\
504
+ │ ├── logo.png the mark (source: src\icyloom\static\logo.svg)
505
+ │ └── screenshots\ the eight console views
506
+ ├── data\
507
+ │ ├── normalization_rules.json 1122 rules (data)
508
+ │ └── tools_registry.json 100 anchors (data)
509
+ ├── src\icyloom\
510
+ │ ├── cli.py doctor / paths / version / config / serve / ...
511
+ │ ├── config.py Pydantic models of the single configuration
512
+ │ ├── paths.py path resolution (no hard-coded path)
513
+ │ ├── logsetup.py logs on stderr, UTF-8 forced
514
+ │ ├── db.py single database: schema, WAL, additive migrations
515
+ │ ├── normalize.py normalisation + cache key
516
+ │ ├── cache.py LLM cache (TTL, LRU)
517
+ │ ├── upstream.py multi-upstream routing
518
+ │ ├── pool.py HTTP pool: drain -> recycle -> retry
519
+ │ ├── breaker.py circuit breaker with real re-arming
520
+ │ ├── gateway.py OpenAI/Anthropic HTTP gateway
521
+ │ ├── admin.py admin API + metrics + console + /assets/logo.svg
522
+ │ ├── dashboard.py metrics aggregate (read-only snapshot)
523
+ │ ├── static\ web console (HTML + SVG, self-contained) + logo.svg
524
+ │ ├── mcpserver.py single FastMCP instance + 13 tools
525
+ │ ├── toolproxy.py tool relay + cache (deny-by-default, epoch) + loop guard
526
+ │ ├── ledger.py single economic journal
527
+ │ ├── tokenizer.py local token counter (tiktoken or estimator)
528
+ │ ├── migrate.py additive migration (sources read-only)
529
+ │ ├── runner.py combined launcher
530
+ │ └── context\ drawer, anchors, invariants, verification, sandbox...
531
+ ├── tools\
532
+ │ ├── seed_demo.py demo data set (real traffic, isolated stack)
533
+ │ ├── spread_demo.py deterministic time-spreading of the demo events
534
+ │ ├── campaign.py hermetic functional campaign (88 checks)
535
+ │ ├── check_js.py JavaScript syntax check of the console
536
+ │ ├── fake_mcp_server.py fake downstream MCP server (relay target)
537
+ │ ├── run_fake_upstream.py fake OpenAI/Anthropic upstream
538
+ │ └── proof_tools.py, report_live.py, report_tools.py, diag_norm.py, ...
539
+ └── tests\ test suite, end-to-end proof, correctness harness, real-server test
540
+ ```
541
+
542
+ ## Project map
543
+
544
+ Everything points at everything else:
545
+
546
+ | Document | What it answers |
547
+ |---|---|
548
+ | [`README.md`](README.md) | What IcyLoom is, how to run it, what it measures |
549
+ | [`PROGRESS.md`](PROGRESS.md) | What is **proven**, what is **not**, what is next |
550
+ | [`MERGE_PLAN.md`](MERGE_PLAN.md) | How IcyCache and Loom were merged into one project |
551
+ | [`RESEARCH.md`](RESEARCH.md) | The research behind the technical choices |
552
+ | [`ICYLOOM_PROMPT.md`](ICYLOOM_PROMPT.md) | The original brief this project answers |
553
+ | [`LICENSE`](LICENSE) | MIT |
554
+ | [`docs/logo.png`](docs/logo.png) | The mark (source: `src/icyloom/static/logo.svg`) |
555
+ | [`docs/screenshots/`](docs/screenshots) | The eight console views, 1440 × 960 |
556
+
557
+ The console is the living version of this README: it reads the same
558
+ `v_token_ledger` the ledger section describes, through the same
559
+ `GET /api/dashboard` route.
560
+
561
+ ## License
562
+
563
+ MIT — see [`LICENSE`](LICENSE).
564
+
565
+ Copyright (c) 2026 Malik Belkacemi.
566
+
567
+
568
+
569
+
570
+ ## Ownership and license
571
+
572
+ [Project origin and evidence](PROJECT-ORIGIN.md) · [License](LICENSE)
573
+
574
+ ---
575
+
576
+ <div align="center">
577
+
578
+ <sub>Original project and documentation by <strong>Malik Belkacemi · MLKBLK-IT</strong> · © 2026</sub>
579
+
580
+ </div>