From f5e93cabb38070a707ac42df1abfef405ebde869 Mon Sep 17 00:00:00 2001 From: Jaixii Date: Thu, 30 Jul 2026 22:50:05 -0400 Subject: [PATCH 1/2] feat: harden memory recall and evaluation --- .claude-plugin/skill-assets.sha256 | 4 +- .env.example | 4 + AGENTS.md | 2 +- BENCHMARKS.md | 154 ++++- CHANGELOG.md | 37 ++ MANIFEST.in | 1 + README.md | 95 ++- docker-compose.yml | 6 +- docs/ARCHITECTURE_V3.md | 2 +- docs/KILO_CODE_INTEGRATION.md | 9 +- docs/dashboard-button-qa.md | 9 +- docs/images/context-efficiency.png | Bin 62027 -> 187813 bytes docs/images/context-efficiency.svg | 125 +++- engraphis/backends/extractor.py | 170 ++++- engraphis/backends/sync_folder.py | 16 +- engraphis/classic_assets/dashboard.js | 4 +- engraphis/config.py | 5 +- engraphis/core/consolidate.py | 76 ++- engraphis/core/context.py | 11 + engraphis/core/engine.py | 10 +- engraphis/core/grounded.py | 12 + engraphis/core/interfaces.py | 7 + engraphis/core/recall.py | 32 +- engraphis/core/retrieval_policy.py | 39 ++ engraphis/core/store.py | 131 +++- engraphis/core/sync.py | 36 +- engraphis/dashboard_assets/engraphis-graph.js | 122 +++- engraphis/dashboard_assets/ledger.js | 2 +- engraphis/inspector/app.py | 4 + engraphis/mcp_server.py | 36 +- engraphis/read_only_api.py | 8 + engraphis/routes/v2_api.py | 17 +- engraphis/service.py | 68 +- engraphis/static/dashboard.js | 4 +- eval/EVIDENCE.md | 30 + eval/agent_benchmarks.py | 592 ++++++++++++++++++ eval/benchmark.py | 333 +++++++++- eval/chunking_eval.py | 40 +- eval/code_agent_ab.py | 238 +++++++ eval/context_economy.py | 398 ++++++++++++ eval/longmemeval_v2_evidence.py | 252 ++++++++ eval/performance.py | 118 +++- pyproject.toml | 2 +- scripts/externalize_dashboard_assets.py | 15 +- scripts/install_shortcuts.py | 58 +- scripts/sync.py | 12 +- skills/engraphis-memory/SKILL.md | 5 +- skills/engraphis-memory/references/TOOLS.md | 24 +- tests/e2e/graph-engine.spec.js | 54 ++ tests/test_benchmark_evidence.py | 149 ++++- tests/test_chunking_eval.py | 27 + tests/test_chunking_extractor.py | 125 +++- tests/test_code_agent_ab.py | 142 +++++ tests/test_consolidate.py | 111 ++++ tests/test_context_economy.py | 233 +++++++ tests/test_context_packing.py | 35 ++ tests/test_dashboard_v2.py | 19 +- tests/test_eval_agent_benchmarks.py | 280 +++++++++ tests/test_eval_harness.py | 4 + tests/test_eval_performance.py | 54 +- tests/test_externalize_dashboard_assets.py | 15 + tests/test_graph_engine_asset.py | 19 +- tests/test_install_shortcuts.py | 60 ++ tests/test_longmemeval_v2_evidence.py | 135 ++++ tests/test_mcp_server.py | 15 +- tests/test_packaging.py | 6 +- tests/test_read_only_api.py | 35 +- tests/test_receipts.py | 88 +++ tests/test_release_infrastructure.py | 13 +- tests/test_retrieval_policy.py | 56 ++ tests/test_sync.py | 181 +++++- tests/test_sync_cli.py | 17 + 72 files changed, 4979 insertions(+), 269 deletions(-) create mode 100644 eval/EVIDENCE.md create mode 100644 eval/agent_benchmarks.py create mode 100644 eval/code_agent_ab.py create mode 100644 eval/context_economy.py create mode 100644 eval/longmemeval_v2_evidence.py create mode 100644 tests/test_code_agent_ab.py create mode 100644 tests/test_context_economy.py create mode 100644 tests/test_eval_agent_benchmarks.py create mode 100644 tests/test_install_shortcuts.py create mode 100644 tests/test_longmemeval_v2_evidence.py diff --git a/.claude-plugin/skill-assets.sha256 b/.claude-plugin/skill-assets.sha256 index 8498d23e..a9bdd327 100644 --- a/.claude-plugin/skill-assets.sha256 +++ b/.claude-plugin/skill-assets.sha256 @@ -1,6 +1,6 @@ b20f1f0f7263b044f880341ddfde3b346b517581321044196c851d7384042563 .claude-plugin/marketplace.json ef872abd9089782b25c8c42a0d5c1582ca1a8930dedb564d1609951f2d263ce3 .claude-plugin/plugin.json -696fe737e83a8d073c8dac77704ada7261332ede2527b4c4d426b1e657e034da skills/engraphis-memory/SKILL.md +018364f63e2181d83ba8d9532c50a41bd30e65eaa8358a721538c297dd063084 skills/engraphis-memory/SKILL.md 7ee71fb5ff9bd2b02f50b3ee8dc62f390a0e1bcd849a55739c4a376ac03d9784 skills/engraphis-memory/references/CONVENTIONS.md 8aafd2daba872be38ec8d42377e886d795d8941bf7c6a39795937ffc1d1f0d88 skills/engraphis-memory/references/SCOPING.md -4c1478453237643e7b4ee2ab4484b9fea8fd759f19f9a5fbf9eeda216d1d6f1a skills/engraphis-memory/references/TOOLS.md +6b0bbb97db4bfa4b1682f9f195bd823f05a2950384ea5c5b261b446b9461d1f1 skills/engraphis-memory/references/TOOLS.md diff --git a/.env.example b/.env.example index ce5808c0..121f1cd9 100644 --- a/.env.example +++ b/.env.example @@ -255,6 +255,10 @@ ENGRAPHIS_LLM_API_KEY=sk-your-key-here # ENGRAPHIS_CHUNK_TOKENS=512 # ENGRAPHIS_CHUNK_MAX=2048 # ENGRAPHIS_CHUNK_OVERLAP=64 +# Optional reader-tokenizer parity for chunk sizes (requires transformers). +# Pin the revision when the resulting memories support reproducible evidence. +# ENGRAPHIS_CHUNK_TOKENIZER_MODEL=Qwen/Qwen3.5-9B +# ENGRAPHIS_CHUNK_TOKENIZER_REVISION= # ENGRAPHIS_LOOP_INTERVAL=300 # ENGRAPHIS_LOOP_TOP_K=10 # ENGRAPHIS_RERANK_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2 diff --git a/AGENTS.md b/AGENTS.md index bd7b30e1..74d70567 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ task is ambiguous, decide which side it belongs to *before* editing. ```bash # ── Install ────────────────────────────────────────────────────────────────── pip install numpy pytest # v2 core + tests, fully OFFLINE (this is what CI does) -pip install -e ".[dev]" # full stack: FastAPI server, ST embeddings, ruff +pip install -e ".[all,dev]" # full stack: FastAPI server, ST embeddings, ruff cp .env.example .env # only needed for the v1 server / LLM features # ── Quality gate (offline, no API key — KEEP THIS GREEN; mirrors .github/workflows/ci.yml) ── diff --git a/BENCHMARKS.md b/BENCHMARKS.md index f0503571..16fa0f22 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -37,7 +37,43 @@ and stated everywhere the numbers appear (`eval/external.py`). disabled so repeated measurements do not mutate their corpus. It reports p50/p95/p99 latency, retrieval quality, and packed context tokens in one JSON-safe schema. `--filler-memories` provides deterministic corpus scaling, and every report records the runtime, architecture, - embedder, vector backend, corpus size, warmups, and iteration count. + embedder, vector backend, corpus size, warmups, and iteration count. `--candidate-k` and + `--retrieval-profile` make adaptive-depth/routing experiments executable instead of changing + production defaults from an unmeasured hunch. +- **Workload context economy**: `eval/context_economy.py` compares three executable strategies + across every question in a workload: uncapped full-history replay, a contiguous recency window + at the same hard budget, and shipped Engraphis hybrid recall + packing. It reports evidence and + answer-token quality, cumulative reader-context tokens, a conservative total that charges one + complete source-token pass to indexing, and the query-count break-even point. The default is + deterministic/offline; `--embed-model` enables a real retrieval model, while + `--format locomo|longmemeval` reuses the established external loaders. + +The workload benchmark is also allowed to say “this workload is too small for a memory layer.” +On the 44-memory / 26-question CodeMem regression fixture, every case already fits inside a +64-token recency window. Full-history and recency therefore use the same 1,180 cumulative reader +tokens at perfect evidence/answer-token quality, while Engraphis uses 1,375–1,377 reader tokens +plus a conservative 631-token indexing pass. That is an honest no-break-even boundary result: +the benefit being measured begins when history is long or reused enough to outweigh retrieval +framing and indexing. + +The complementary real-model LoCoMo workload diagnostic covers 10 conversations and 1,986 +questions with `all-MiniLM-L6-v2`, `k=10`, a 512-token reader budget, and conflict resolution +disabled. Engraphis used **891,857** cumulative reader-context tokens versus **49,915,394** for +uncapped full history, **98.2133% lower**. Charging one complete 246,539-token corpus pass to +indexing produces a conservative Engraphis total of **1,138,396**, still **97.7193% lower**, with +a calculated break-even at query 10. The quality tradeoff is explicit: + +| LoCoMo workload method | Retrieval recall | Hit rate | Answer-token recall | Mean reader context | +|---|---:|---:|---:|---:| +| Engraphis hybrid recall | **0.600457** | **0.657417** | **0.679614** | **449.07** tokens | +| Same-budget recency window | 0.011289 | 0.012614 | 0.339941 | 487.87 tokens | +| Uncapped full history | 0.996997 | 0.997477 | 0.917247 | 25,133.63 tokens | + +This diagnostic supports a precise statement: Engraphis recovered much more useful evidence than +a same-budget recency window while using a small fraction of full-history context. It does not +support “same quality as full history,” provider-billing, or end-to-end answer-accuracy claims. +The embedding model revision was not pinned in that run, so rerun it with an immutable revision +before treating the numbers as canonical release evidence. ### Reproduce @@ -49,6 +85,10 @@ python -m eval.harness --dataset eval/datasets/codemem.jsonl --k 5 python -m eval.harness --dataset eval/datasets/graph_multihop.jsonl --k 5 python -m eval.ablation python -m eval.performance --dataset eval/datasets/codemem.jsonl --k 5 --iterations 10 +python -m eval.performance --dataset eval/datasets/codemem.jsonl --k 5 \ + --candidate-k 25 --candidate-depth adaptive --retrieval-profile auto --iterations 10 +python -m eval.context_economy --dataset eval/datasets/codemem.jsonl \ + --token-budget 512 --k 5 python -m eval.performance --dataset eval/datasets/codemem.jsonl --k 5 \ --iterations 5 --filler-memories 1000 # Canonical latency/resource protocol: requires >=1,000 queries and five processes. @@ -57,6 +97,8 @@ python -m eval.performance --dataset fixed-1000-plus.jsonl --acceptance-matrix - # Real retrieval numbers (downloads all-MiniLM-L6-v2) python -m eval.external --dataset longmemeval_s.json --format longmemeval --k 10 python -m eval.external --dataset locomo10.json --format locomo --k 10 +python -m eval.context_economy --dataset locomo10.json --format locomo \ + --embed-model sentence-transformers/all-MiniLM-L6-v2 --token-budget 512 --k 10 --no-resolve ``` ## What we do NOT yet claim @@ -65,6 +107,9 @@ python -m eval.external --dataset locomo10.json --format locomo --k 10 - **No hosted-service latency comparison.** The in-repo p50/p95/p99 benchmark covers the local reference pipeline and records its environment; unlike environments are not compared. - **No neutral third-party ranking.** We have not run an external eval platform. +- **No provider bill estimate.** Context-economy counts reader evidence under its named counter. + It excludes system/tool prompts, questions, completions, prompt caching, provider pricing, + compute, and storage. Its indexing-inclusive total is a conservative text-volume proxy. Every publishable run should emit the `engraphis-benchmark/v2` envelope: dataset/config hashes, per-question records, explicit exclusions, fixed-budget context curves, and deterministic @@ -99,6 +144,12 @@ tokens; that single official point must not be presented as a five-point curve. cases. Retrieval-only abstention/no-evidence records remain visible in the artifact's `exclusions`; they are not counted as evidence-retrieval scores. +Official LongMemEval-V2 output can be converted into a public-safe QA artifact with +`python -m eval.longmemeval_v2_evidence`. The exporter keeps the official QA score, fixed-reader +context token count, latency, model revisions, source digests, repository state, and artifact +checksum. It removes raw questions, answers, prompts, reader output, and retrieved context before +the artifact can be written. See [`eval/EVIDENCE.md`](eval/EVIDENCE.md) for the exact command. + ### LongMemEval-V2 memory-module adapter `eval.longmemeval_v2.EngraphisLongMemEvalV2Memory` follows the official @@ -125,11 +176,15 @@ tokens. Packed sources are returned as separate context items, preserving the la evidence prefix instead of dropping one oversized monolithic item. The adapter does not download benchmark data or call the reader/evaluator; the official harness owns those steps. -## Next steps for external publishable numbers +## External evidence status and remaining executions -1. **Add a QA layer to `eval/external.py`.** Optional answering model + judge on top of the - existing retrieval pipeline, so the official datasets can report end-to-end accuracy while - reusing the retrieval harness underneath. +1. **Run the official LongMemEval-V2 reader and evaluator.** The adapter, pinned runner, and + redacted evidence exporter are implemented. The exact upstream commit boots in an isolated + Python 3.11 environment and the wrapper reaches the official harness CLI. Dataset revision + `f152293e235517d504809563c833d7190b8c713b` publishes 7,120,369,667 bytes before the pinned + Qwen reader and embedding model assets. A full official run therefore still requires those + resources, sufficient compute, and evaluator configuration; no canonical QA score is claimed + until that run completes. 2. **Publish production-backend latency.** Run `eval/performance.py` with the real embedder and sqlite-vec/backend configuration on a fixed machine class and corpus scale. 3. **Run the fixed-budget curve on the complete official datasets.** The v2 harness now measures @@ -138,6 +193,95 @@ benchmark data or call the reader/evaluator; the official harness owns those ste after complete official runs produce immutable artifacts for every point. 4. **Run an external evaluation platform** once (1)–(3) exist. +Do not make all evidence lanes variants of explicit factual recall. Executable offline adapters +now cover: + +- [MemoryAgentBench](https://github.com/HUST-AI-HYZ/MemoryAgentBench): incremental multi-turn + learning, long-range understanding, and conflict/consolidation inputs. +- [LoCoMo-Plus](https://github.com/xjtuleeyf/Locomo-Plus): an old implicit constraint must affect + a later response even when the later cue does not restate the remembered fact. +- [Mem2ActBench](https://github.com/Cantaloupe-M/Mem2ActBench): memory must select a tool and + ground its arguments, not merely return a passage. The current adapter measures retrieval and + expected tool-argument context coverage, not generated tool-call success. + +```bash +python -m eval.agent_benchmarks --dataset memoryagentbench.json \ + --format memoryagentbench +python -m eval.agent_benchmarks --dataset locomo_plus.json \ + --format locomo_plus +python -m eval.agent_benchmarks --dataset qa_dataset.jsonl \ + --conversations toolmem_conversation.jsonl --format mem2actbench \ + --artifact artifacts/mem2actbench.json +``` + +Use `--artifact` on any of these commands to write a redacted, immutable evidence envelope plus +an adjacent SHA256 file. The ordinary console/`--json` report is private run material and may +contain source questions for debugging. + +### Upstream-data diagnostic baseline (2026-07-30) + +These runs use the dependency-free deterministic embedder on upstream data. They validate the +adapters and expose product gaps; they are noncanonical diagnostics, not leaderboard or marketing +claims. The artifact validator accepted every completed envelope. + +| Upstream source | Executed scope | Result and boundary | +|---|---|---| +| LoCoMo-Plus commit `059f4e3d38f7f1f96765e8e2cb7de3097551bffb` | All 401 Cognitive cases, 40,270 source memories | Recall@10 **0.1259**, hit@10 **0.1272**, MRR@10 **0.0744**, answer-token context coverage **0.5095**. This is cue-evidence retrieval, not answer-judge accuracy. The low retrieval score is useful negative evidence: implicit-constraint recall remains a real product gap. | +| MemoryAgentBench commit `455306dcabc3842526eb83cd4e225e5d486c5c5d`, official Hugging Face `Accurate_Retrieval` first row | 100 questions | Recall@10 **0.5100**, hit@10 **0.8600**, answer-token context coverage **0.8500**. Gold evidence was derived only where an accepted answer occurred in a source chunk. | +| The same source, `Conflict_Resolution` first row | 100 questions | Recall@10 **0.4600**, hit@10 **0.6400**, answer-token context coverage **0.6800**. This plain-context export measures retrieval, not structured temporal invalidation. | +| The same source, `Long_Range_Understanding` first row | 1 question | Answer-token context coverage **0.2658**. The export supplied no evidence IDs and no accepted answer occurred verbatim in a source chunk, so retrieval was deliberately left unscored rather than reported as a false perfect score. | +| The same source, `Test_Time_Learning` first row | One 5.88 MB context | The no-resolution ingest did not complete within a five-minute local smoke ceiling. This is a measured large-ingest throughput gap, not a failed quality score; batch embedding and transaction work should precede a complete split run. | +| Mem2ActBench upstream smoke | 2 public rows | Recall@10, hit@10, MRR@10, and NDCG@10 **1.0000**; expected tool-call JSON token coverage **0.5714**. This is retrieval/context coverage, not generated action success. | + +The MemoryAgentBench loader accepts both its aligned public JSON export and the Hugging Face +dataset-server `rows[].row` envelope. Rows without gold evidence remain useful for answer-token +coverage, but are excluded from retrieval aggregates and counted separately as +`retrieval_scored_questions`. + +For paired code-agent runs, execute the same tasks with the same model, tools, machine, and +deterministic success oracle under `full_history` and `engraphis`. Then analyze the content-free +run records with: + +```bash +python -m eval.code_agent_ab --full-history full-history.jsonl \ + --engraphis engraphis.jsonl --output paired-report.json +``` + +The analyzer rejects unmatched task IDs and different success oracles, then reports paired +bootstrap intervals for task success, input/output/tool tokens, retries, latency, and optional +cost. Its aggregate output does not echo task IDs or oracle commands. It does not launch an agent +or invent a task-success oracle. + +## Optimization experiments to run before changing defaults + +1. **Budget-aware packing**: compare full source, safe summary, sentence-aligned safe summary + excerpt, and raw-source excerpt at fixed budgets. Gate on support/answer retention and + qualifier preservation, not token count alone. +2. **Adaptive retrieval work**: `--candidate-depth adaptive` is now an opt-in performance + experiment. It keeps wider graph/code pools and reduces routine lexical/balanced pools while + reporting the requested and actual depth. Sample and CodeMem kept every offline quality metric + at 1.0 with balanced depth reduced from 50 to 15; CodeMem plus 1,000 fillers reduced local + median recall latency from 20.666 ms to 18.991 ms in a 260-recall comparison, an 8.1% + reduction. These are machine-specific regression results, not production latency claims. Keep + the default fixed until complete external categories meet predeclared quality margins. +3. **Packing-pressure consolidation**: prioritize memory families that are frequently recalled, + repeatedly omitted, or costly per useful token. Count write/index/storage cost as well as later + reader-context savings. +4. **Tokenizer-aware ingestion**: implemented behind the chunk extractor. The dependency-free + default remains `engraphis.chars4.v1`; an explicitly configured Hugging Face reader tokenizer + enforces prose chunk and overlap budgets and records its identity in chunk metadata. Continue + measuring tokens-to-evidence, recall, and storage/index growth together before recommending a + model-specific default. +5. **Bulk ingestion**: add batch embedding plus a transaction-aware vector upsert path, then rerun + the 5.88 MB MemoryAgentBench Test-Time Learning row. Gate this on identical stored-memory, + provenance, graph-link, and temporal-resolution outcomes, not throughput alone. +6. **Scoped caches**: benchmark query embeddings and repeat-recall results keyed by workspace, + repo, time anchors, profile, and corpus version. Test invalidation correctness before claiming + latency gains. +7. **Privacy-safe real usage**: use `engraphis_context_savings` to let each workspace inspect + aggregate source/context/saved tokens already present in content-free receipts. Keep unlike + token counters separate and require a valid receipt chain before treating totals as auditable. + ## Evaluation question The predeclared question is whether the full vector + lexical/BM25 + sparse PPR graph + calibrated diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac6925f..2d8688af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,43 @@ All notable changes to Engraphis are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com/); versions use SemVer. +## [Unreleased] + +### Added + +- `engraphis_context_savings` aggregates validated, content-free recall receipts by workspace, + repo, operation, and token-counter identity. The same read-only view is available through the + service, Inspector, v2/read-only APIs, and dashboard receipt panel. +- Recall supports an explicit adaptive candidate-depth experiment while retaining the historical + fixed depth by default. Performance reports record requested and actual candidate depths. +- Chunk ingestion can enforce budgets with an injected or explicitly configured Hugging Face + tokenizer and records the counter identity, target, and overlap in each chunk's metadata. +- Offline adapters now cover MemoryAgentBench, LoCoMo-Plus, and Mem2ActBench. A paired code-agent + analyzer compares full-history and Engraphis runs using identical tasks and success oracles. +- Public benchmark evidence can carry source hashes, repository state, environment and model + provenance, secret-redacted commands, content digests, and adjacent immutable SHA-256 files. + +### Changed + +- Context-economy evaluation now compares uncapped full history, a same-budget recency window, + and shipped hybrid recall while charging an explicit one-time indexing token proxy. +- Official LongMemEval-V2 output has a dedicated redacted evidence exporter that retains the + official QA/token/latency measures without publishing prompts, answers, model output, or + retrieved context. +- Folder-sync dry runs no longer create a remote directory or persist a local device identity. + +### Fixed + +- Sync rejects malformed scope/repo combinations and every peer-driven visibility change for an + existing memory, including malformed legacy rows. Scope promotion or repair remains a local, + explicit governance operation. +- Workspace consolidation excludes session-private memories and partitions digests and entity + profiles by their exact visibility owner, preventing cross-repo or cross-scope summaries. +- Tokenizer-aware chunk overlap can no longer exceed the configured prose budget or emit a + duplicate overlap-only record before an oversized paragraph. Invalid token counters fail + closed instead of silently producing mis-sized chunks. +- The new evidence guide is included in wheel and source distributions. + ## [1.2.2] - 2026-07-30 ### Fixed diff --git a/MANIFEST.in b/MANIFEST.in index 7ceee97b..ad7fe66f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ include docker-entrypoint.sh Dockerfile docker-compose.yml include railway.json recursive-include eval *.py include eval/BASELINES.md +include eval/EVIDENCE.md recursive-include eval/configs *.json recursive-include eval/datasets *.jsonl recursive-include tests *.py diff --git a/README.md b/README.md index f7358671..8f73b50c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,18 @@ https://discord.com/invite/Wfr2ejBmY Knowledge Graph · run engraphis-dashboard to see it live

+--- + +> Update regularly for the latest fixes and improvements. +> +> **Open-core boundary:** this repository contains the free local engine, dashboard, MCP server, +> and customer-side clients. Hosted sync, analytics, automation, and team services run on the +> official hosted service; their server implementations are not distributed here. + +> **Support continued Engraphis development with Pro.** [Start a 3-day Pro trial](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=readme_intro&trial=pro#billing) +> or [subscribe to Pro](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=readme_intro#billing). + + ## What Engraphis gives an agent An agent should not have to reconstruct a project from scattered chat history on every task. @@ -28,10 +40,8 @@ that supports the current question; and returns a bounded, attributable context Store durable project knowledge · retrieve supporting evidence · give the agent only what it needs

-The flow is the essential path. See [measured context savings](#measured-quality-and-token-efficiency) -for reproducible fixture-level evidence of less returned content at the same tested retrieval -scores, without billing or latency claims. The sections below cover the dashboard, code graph, -local installation, governance controls, and hosted services in detail. +The flow is the essential path. See [measured token and context savings](#measured-token-and-context-savings) +for the short version of how much less history an agent has to carry. | Agent need | What Engraphis changes | |---|---| @@ -58,17 +68,6 @@ Run `python -m eval.chunking_eval` and `python -m eval.grounded` to reproduce th the former measures evidence retrieval and context size, while the latter measures the answer-versus-abstain decision. ---- - -> Open-source users: update regularly for the latest fixes and improvements. -> -> **Open-core boundary:** this repository contains the free local engine, dashboard, MCP server, -> and customer-side clients. Hosted sync, analytics, automation, and team services run on the -> official hosted service; their server implementations are not distributed here. - -> **Support continued Engraphis development with Pro.** [Start a 3-day Pro trial](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=readme_intro&trial=pro#billing) -> or [subscribe to Pro](https://api.engraphis.com/account?plan=pro&interval=monthly&utm_source=engraphis&utm_medium=docs&utm_campaign=pro_conversion&utm_content=readme_intro#billing). - ## Full Engraphis install: pip install "engraphis[all]" Engraphis-Dashboard opens `http://127.0.0.1:8700`. Local memory needs no cloud account, @@ -131,35 +130,18 @@ chunking. The activity view records outcomes, never keys, prompts, or raw provid --- -## Measured quality and token efficiency - -Engraphis ships typed and scoped memory, bi-temporal history, grounded recall, hybrid -vector/lexical/graph retrieval, deterministic context packing, and MCP-native agent tools. -The current deterministic offline regression fixtures reproduce these quality results: - -| Fixture | Reproduced result | -|---|---| -| CodeMem retrieval: 44 memories, 26 questions | **Recall@5 1.000**, hit@5 1.000, answer-token recall 1.000 | -| Grounded-answer decisions: 10 cases | **10/10 correct**: 5/5 answerable questions cited evidence and 5/5 off-topic questions abstained | - -### Proof at a glance - -| **73.0% less retrieved context** | **3.8× smaller evidence record** | **55.38% smaller MCP response** | -|---|---| -| **808.8 → 218.4** tokens per question | **162.2 → 42.4** tokens to supporting evidence | **17,172 → 7,663** serialized tokens | -| Same Recall@5 **1.000** in the long-document fixture | Same 18 fixture questions returned an evidence-holding memory | Same CodeMem retrieval scores across 260 timed recalls | - -**What it means:** agents carry less irrelevant history, leaving more room for the current task -and cited evidence. These controlled, deterministic fixtures measure context, not model billing, -task time, customer savings, or external-benchmark performance. +## Measured token and context savings

- Normalized chart: Engraphis retains 27.0 percent of retrieved content, 26.1 percent of the evidence-holding record, and 44.6 percent of the compact MCP response in separate controlled fixtures + Dark chart showing Engraphis using 98.21 percent less long-history context, 73.0 percent less retrieved content per question, 73.9 percent fewer tokens in the smallest useful memory, a 55.38 percent smaller memory response, and 47.8 percent less repeated-memory context after consolidation
- Each row uses a separate 100% baseline. The measurements have different counting boundaries and are not additive. + Less repeated history means more room for the task, tools, and useful evidence.

-#### A controlled before-and-after example +
+See benchmark details and reproduce the results + +### Controlled before-and-after example | Retrieval mode | Mean returned memory content | Recall@5 | |---|---:|---:| @@ -177,9 +159,11 @@ boundary. | What is counted | Comparison | Measured reduction | Quality held constant | |---|---|---|---| +| Cumulative reader context across a 1,986-question LoCoMo diagnostic | Full-history replay: **49,915,394** tokens → Engraphis: **891,857** tokens | **49,023,537 fewer context tokens** (**98.2133% lower**) | Focused retrieval used far less context; uncapped full history retained higher retrieval recall | | Retrieved top-5 memory content, averaged per question | Whole documents: **808.8** tokens → structure-aware chunks: **218.4** tokens | **590.4 fewer tokens per question** (**73.0% lower**, about **3.7× smaller**) | Recall@5 **1.000** in both modes across 6 documents and 18 questions | | Smallest returned memory that contains the reference evidence | Whole documents: **162.2** tokens → chunks: **42.4** tokens | **119.8 fewer tokens to evidence** (**73.9% lower**, about **3.8× smaller**) | The same 18 questions had a returned evidence-holding memory in both modes | | Serialized MCP recall response across 260 timed CodeMem recalls | Full result: **17,172** `engraphis.regex.v1` tokens → compact result: **7,663** tokens | **9,509 response tokens avoided** (**55.38% lower**) | Recall@5, hit@5, and answer-token recall all **1.000** | +| Repeated-memory consolidation fixture | 12 related episodic memories: **230** tokens → one digest: **120** tokens | **110 tokens removed from the active digest** (**47.8% lower**) | Original memories remain available for provenance and audit | | Packed prompt-context usage in the same CodeMem performance fixture | Hard budget: **1,500** tokens; observed mean: **87.73**; observed maximum: **106** | A hard cap prevents a recall from exceeding its configured context budget | This is usage accounting, not a before/after savings comparison | The compact MCP response avoids duplicating full memory bodies when the packed context and source @@ -209,6 +193,8 @@ normalized-character estimator. Chunking measures retrieved memory content, whil measures serialized MCP response size. See [`BENCHMARKS.md`](BENCHMARKS.md) for definitions, limitations, canonical external-evaluation requirements, and the no-unsupported-claims policy. +
+ --- ## Install @@ -289,7 +275,7 @@ claude mcp add engraphis -- engraphis-mcp cmd mcp add engraphis -- engraphis-mcp # Command Code CLI ``` -Your agent now has 30 tools: remember, recall context (plus full, grounded, and proactive recall), +Your agent now has 31 tools: remember, recall context (plus full, grounded, and proactive recall), proactive context, grounded answer alias, why, timeline, forget, pin, correct, promote, ingest, consolidate, index_repo, search/code path/impact/export, privacy receipts, PostgreSQL schema ingestion, link, @@ -426,7 +412,7 @@ to support the project and add hosted services. | | Free (available now) | Pro: $10/mo or $100/yr | Team: $20/seat/mo or $200/seat/yr | |---|---|---|---| | Dashboard WebUI (with built-in inspector) | ✓ | ✓ | ✓ | -| Memory engine + 30 MCP tools | ✓ | ✓ | ✓ | +| Memory engine + 31 MCP tools | ✓ | ✓ | ✓ | | Version-chain diffs, offline knowledge graph | ✓ | ✓ | ✓ | | Manual local consolidation (dry-run by default) | ✓ | ✓ | ✓ | | Local workspace export (JSON: memories, sessions, audit) | ✓ | ✓ | ✓ | @@ -466,6 +452,7 @@ to support the project and add hosted services. | Code | `engraphis_code_impact` | Rank changed files by symbols, dependents, communities, memories, and hotspots | | Code | `engraphis_export_code_graph` | Portable graph JSON + Markdown + HTML report | | Audit | `engraphis_receipts` | List content-free hashed operation receipts | +| Audit | `engraphis_context_savings` | Sum privacy-safe context usage by workspace/repo and token-counter identity | | Audit | `engraphis_verify_receipts` | Verify the receipt chain, local tail anchor, and optional externally saved head/count | | Audit | `engraphis_export_receipts` | Export the shareable receipt-only audit bundle | | Governance | `engraphis_forget` | Retire a memory: bi-temporal close, never deleted; every request is audited | @@ -496,9 +483,12 @@ repository is supplied. The operation-receipt chain is deliberately content-free. It records bounded operation metadata and chained hashes, while excluding raw memory/query text, workspace names, memory IDs, and actor identities from exported receipt payloads. Use `engraphis_receipts`, -`engraphis_verify_receipts`, and `engraphis_export_receipts` to inspect the chain or compare it -with a previously saved head/count anchor. A separately maintained local count/head anchor and -persistent integrity marker make interior edits, reordering, and tail truncation detectable. +`engraphis_context_savings`, `engraphis_verify_receipts`, and `engraphis_export_receipts` to +inspect the chain, aggregate retrieved-source versus packed-context tokens, or compare it with a +previously saved head/count anchor. Savings stay separated by token-counter identity and are +reported with chain validity; they are packing measurements, not provider bills. A separately +maintained local count/head anchor and persistent integrity marker make interior edits, reordering, +and tail truncation detectable. See [the v3 architecture document](docs/ARCHITECTURE_V3.md) for the data flow and [SECURITY.md](SECURITY.md) for the trust boundaries. @@ -613,7 +603,11 @@ Drag-and-drop or server-side import, access-controlled and bounded: a big context-reduction win on long docs. Works across all three ingest paths (dashboard upload, `import_folder`, and `engraphis_ingest`). Measure the payoff with the bundled eval: `python -m eval.chunking_eval --dataset eval/datasets/longdoc.jsonl --k 5` - (whole-file vs. chunked, same recall pipeline, offline). + (whole-file vs. chunked, same recall pipeline, offline). The dependency-free default + uses the named `engraphis.chars4.v1` estimate. Set + `ENGRAPHIS_CHUNK_TOKENIZER_MODEL` and, for reproducible runs, + `ENGRAPHIS_CHUNK_TOKENIZER_REVISION` to size prose chunks with the actual reader + tokenizer; the chosen counter identity is preserved in each chunk's metadata. - **Structured LLM extraction**: `ENGRAPHIS_EXTRACTOR=llm_structured` validates typed facts, entities, relations, and keywords before storage. Its preserved entity/relation metadata feeds the knowledge graph automatically. A successful dashboard connection test @@ -673,6 +667,8 @@ All via environment (or `.env`): | `ENGRAPHIS_DB_KEY` | Not set | Encrypt the database at rest (SQLCipher). Or use `ENGRAPHIS_DB_KEY_FILE` | | `ENGRAPHIS_EMBED_MODEL` | `sentence-transformers/all-MiniLM-L6-v2` | sentence-transformers model | | `ENGRAPHIS_EXTRACTOR` | `none` | `none` = verbatim; `chunk` = offline structure-aware chunks; `llm` = free-form LLM facts; `llm_structured` = schema-validated facts + graph metadata | +| `ENGRAPHIS_CHUNK_TOKENIZER_MODEL` | Not set | Optional Hugging Face tokenizer used to enforce chunk budgets with the downstream reader's real tokenization; requires the optional `transformers` package | +| `ENGRAPHIS_CHUNK_TOKENIZER_REVISION` | Not set | Optional immutable tokenizer/model revision recorded in the chunk-counter identity; pin this for reproducible benchmark artifacts | | `ENGRAPHIS_GRAPH_EXTRACTOR` | `regex` | `regex` = offline heuristic NER; `none` = disable heuristic text extraction (validated `llm_structured` metadata still feeds the graph) | | `ENGRAPHIS_RETENTION_SUPERVISOR` | `none` | `none` = deterministic only; `llm` = sends a bounded excerpt to the configured provider for advisory ephemeral/normal/critical classification | | `ENGRAPHIS_WHISPER_MODEL` | Not set | Enables local faster-whisper audio/video transcription | @@ -708,7 +704,7 @@ engraphis/ │ ├── core/ # v2 engine: interfaces, store, recall, scoring, schema, sync │ ├── backends/ # pluggable embedder / vector index / reranker / codegraph / sync transports / encryption │ ├── service.py # validated MemoryService facade -│ ├── mcp_server.py # MCP server: 30 tools +│ ├── mcp_server.py # MCP server: 31 tools │ ├── dashboard_app.py # dashboard WebUI (FastAPI) │ ├── dashboard_assets/ # primary Ledger interface + graph engine │ ├── classic_assets/ # selectable full operator dashboard backup @@ -748,8 +744,11 @@ ruff check . ``` Numbers, not assertions: the offline harness is a **correctness floor** (deterministic embedder). -LoCoMo / LongMemEval adapters and the pinned LongMemEval-V2 reader profile are available for -approved official evaluation runs: see +LoCoMo, LongMemEval, MemoryAgentBench, LoCoMo-Plus, and Mem2ActBench adapters are available, +along with a pinned LongMemEval-V2 reader profile, redacted evidence exporter, and paired +full-history versus Engraphis code-agent analyzer. External adapters measure only the layer they +declare; retrieval or tool-argument context coverage is not presented as end-to-end answer, +action, or task success. Reproduction commands and remaining official-run requirements are in [`BENCHMARKS.md`](BENCHMARKS.md). --- diff --git a/docker-compose.yml b/docker-compose.yml index a023d8eb..a095a8e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,8 +54,10 @@ services: ENGRAPHIS_HOST: 0.0.0.0 # This process binds all container interfaces. Even though the published host port is # loopback-only, require an explicit bearer so a changed port mapping cannot silently - # expose the legacy v1 API. Compose fails before creating the container if it is absent. - ENGRAPHIS_API_TOKEN: ${ENGRAPHIS_API_TOKEN:?Set ENGRAPHIS_API_TOKEN before starting the api profile} + # expose the legacy v1 API. Keep interpolation optional so the inactive profile does + # not break a fresh ``docker compose up``; engraphis-server fails closed at startup + # when this profile is actually launched without a token. + ENGRAPHIS_API_TOKEN: ${ENGRAPHIS_API_TOKEN:-} # The v1 server uses a DIFFERENT, incompatible memory schema from the v2 dashboard, # so it MUST NOT share the dashboard's engraphis.db (doing so corrupts both). Give it # its own file on the shared volume. diff --git a/docs/ARCHITECTURE_V3.md b/docs/ARCHITECTURE_V3.md index 69c523ce..96ece2fc 100644 --- a/docs/ARCHITECTURE_V3.md +++ b/docs/ARCHITECTURE_V3.md @@ -7,7 +7,7 @@ retention-supervision, and privacy-receipt additions introduced with schema vers flowchart LR Agent["Agent / host LLM"] --> Intent["remember · link · recall_context (compact) · recall"] CLI["engraphis-graph CLI"] --> Service["MemoryService"] - MCP["30 MCP tools"] --> Service + MCP["31 MCP tools"] --> Service HTTP["Dashboard + read-only graph HTTP"] --> Service Import["Local resources / PostgreSQL catalog"] --> Extractors["Optional local extractors"] Extractors --> Service diff --git a/docs/KILO_CODE_INTEGRATION.md b/docs/KILO_CODE_INTEGRATION.md index df371b8d..170aa02e 100644 --- a/docs/KILO_CODE_INTEGRATION.md +++ b/docs/KILO_CODE_INTEGRATION.md @@ -10,9 +10,9 @@ This manual is written for someone who wants the full technical picture: what En There are two separate questions hiding inside "connect Kilo Code to Engraphis," and they are usually where people talk past each other: -1. **Transport layer: "get the pipes connected."** This is: install the Engraphis MCP server, tell Kilo Code how to launch it, confirm the tools show up. It's a plumbing task. When it's done, Kilo Code can *see* 30 `engraphis_*` tools. Success here is binary: either the tools appear or they don't. +1. **Transport layer: "get the pipes connected."** This is: install the Engraphis MCP server, tell Kilo Code how to launch it, confirm the tools show up. It's a plumbing task. When it's done, Kilo Code can *see* 31 `engraphis_*` tools. Success here is binary: either the tools appear or they don't. -2. **Orchestration layer: "use the memory well."** This is: *when* should the agent remember vs. recall, how should memories be scoped (`workspace → repo → session`), which of the 30 tools answers which question, and how to keep the store clean over time. This is where the actual value is, and it's a discipline, not a config. +2. **Orchestration layer: "use the memory well."** This is: *when* should the agent remember vs. recall, how should memories be scoped (`workspace → repo → session`), which of the 31 tools answers which question, and how to keep the store clean over time. This is where the actual value is, and it's a discipline, not a config. You need both. A perfect config with no discipline gives you an agent that has memory tools and never uses them correctly. Good discipline with a broken config gives you an agent that wants to remember and can't. **Section 3 is the transport layer. Sections 4–6 are the orchestration layer.** Do them in order. @@ -40,7 +40,7 @@ Everything runs on your machine. The whole store is a single SQLite file. Local You interact with Engraphis through three surfaces, all backed by the *same* engine (`MemoryService`), so they can never drift apart: - **The dashboard WebUI** (`engraphis-dashboard`, `http://127.0.0.1:8700`): a visual product to see, search, and curate memory. -- **The MCP server** (`engraphis-mcp`): the 30 tools your coding agent calls. **This is the surface Kilo Code uses.** +- **The MCP server** (`engraphis-mcp`): the 31 tools your coding agent calls. **This is the surface Kilo Code uses.** - **The Python library** (`from engraphis.service import MemoryService`): for direct programmatic use. ### 2.1 The five ideas that make it more than a vector store @@ -183,7 +183,7 @@ You can also click **Approve Always** on any tool at runtime to write the same r --- -## 4. The 30 tools: the orchestration surface +## 4. The 31 tools: the orchestration surface Once connected, Kilo Code sees these. Do **not** assume only `remember`/`recall` exist. The value is in the rest. This is the full surface, grouped by what question each one answers. @@ -208,6 +208,7 @@ Once connected, Kilo Code sees these. Do **not** assume only `remember`/`recall` | Code | `engraphis_code_impact` | Rank commit/PR impact by dependents, communities, memories, and hotspots. | | Code | `engraphis_export_code_graph` | Portable graph JSON + Markdown + self-contained HTML. | | **Audit** | `engraphis_receipts` | List content-free hashed operation receipts. | +| Audit | `engraphis_context_savings` | Cumulative packed-context savings from receipts, separated by token-counter identity. | | Audit | `engraphis_verify_receipts` | Verify the tamper-evident receipt chain. | | Audit | `engraphis_export_receipts` | Export a privacy-safe receipt-only audit bundle. | | **Governance** | `engraphis_forget` | Retire a memory: bi-temporal close, never a hard delete; every request is audited. | diff --git a/docs/dashboard-button-qa.md b/docs/dashboard-button-qa.md index 5a75c582..249e0487 100644 --- a/docs/dashboard-button-qa.md +++ b/docs/dashboard-button-qa.md @@ -41,10 +41,11 @@ Classic navigation/mobile-nav controls. ## Environment notes -- One parallel lane could not start against the repository's default database because that - existing database is schema version 5 while this checkout supports schema version 4. - This is an environment/data compatibility issue, not a dashboard button failure. The - isolated schema-4 fixture started and exercised the UI successfully. +- At the time of this manual pass, one parallel lane could not start against the repository's + default database because the checkout then supported schema version 4 while that existing + database was schema version 5. This historical environment/data compatibility issue was not a + dashboard button failure. The isolated schema-4 fixture started and exercised the UI + successfully. - The browser harness did not expose programmatic download events for the PNG/JSON export anchors, but the dashboard status confirmed both exports completed. No application console errors were observed during the manual pass. diff --git a/docs/images/context-efficiency.png b/docs/images/context-efficiency.png index 0041fc91de2b0a64a45279a95cb79697f715d2ba..bd6543f4d4475d6fee0f1fa5ff87c4997b3b17f9 100644 GIT binary patch literal 187813 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