cortexm 0.3.0__tar.gz → 0.5.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.
- {cortexm-0.3.0/cortexm.egg-info → cortexm-0.5.0}/PKG-INFO +42 -20
- {cortexm-0.3.0 → cortexm-0.5.0}/README.md +41 -19
- cortexm-0.5.0/cortexm/__init__.py +151 -0
- cortexm-0.5.0/cortexm/api/long_recall.py +252 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/api/memory.py +333 -5
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/decoders.py +57 -6
- cortexm-0.5.0/cortexm/bridge/fusion.py +245 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/reader.py +332 -0
- cortexm-0.5.0/cortexm/cli.py +650 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/config.py +85 -35
- cortexm-0.5.0/cortexm/creator.py +295 -0
- cortexm-0.5.0/cortexm/kernel.py +227 -0
- cortexm-0.5.0/cortexm/markdown_io.py +310 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/mcp/server.py +244 -0
- cortexm-0.5.0/cortexm/pipeline.py +326 -0
- {cortexm-0.3.0/cortexm/security → cortexm-0.5.0/cortexm/plugins}/__init__.py +0 -0
- cortexm-0.5.0/cortexm/plugins/security.py +208 -0
- cortexm-0.5.0/cortexm/plugins/structured.py +219 -0
- cortexm-0.5.0/cortexm/plugins/verbatim.py +355 -0
- cortexm-0.5.0/cortexm/router.py +205 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/store.py +45 -0
- cortexm-0.5.0/cortexm/trajectory_view.py +271 -0
- cortexm-0.5.0/cortexm/vsa/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0/cortexm.egg-info}/PKG-INFO +42 -20
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm.egg-info/SOURCES.txt +18 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/pyproject.toml +1 -1
- cortexm-0.5.0/tests/test_bm25_chunk_recall_and_inspect_cli.py +255 -0
- cortexm-0.5.0/tests/test_fusion_security.py +205 -0
- cortexm-0.5.0/tests/test_kernel.py +287 -0
- cortexm-0.5.0/tests/test_reddit_steals_round3.py +448 -0
- cortexm-0.5.0/tests/test_tier443_abstention_fix.py +282 -0
- cortexm-0.5.0/tests/test_verbatim.py +236 -0
- cortexm-0.3.0/cortexm/__init__.py +0 -45
- cortexm-0.3.0/cortexm/cli.py +0 -295
- {cortexm-0.3.0 → cortexm-0.5.0}/LICENSE +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/context_m.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/accel.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/api/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/api/chaos.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/abilities.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/baselines.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/beam_loader.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/generator.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/harness.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/messy.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/micro.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/ood.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bench/run.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/dates.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/enrich.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/extractor.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/fallback.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/onnx_runtime.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/patterns.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/ppr.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/prefilter.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/query_extract.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/rerank.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/bridge/writer.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/abstraction.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/analogy.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/engine.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/gaps.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cognition/scanner.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/cortexm.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/enterprise/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/enterprise/audit.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/enterprise/governance.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/errors.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/features/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/features/git.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/features/prefetch.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/features/zk.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/crdt.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/fabric.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/hlc.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/node.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/schema_report.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/federation/transport.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/index/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/index/nsg.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/mcp/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/metrics.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/migrate/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/migrate/importers.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/provenance/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/provenance/agent.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/provenance/cose.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/provenance/scitt.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/provenance/vc.py +0 -0
- {cortexm-0.3.0/cortexm/server → cortexm-0.5.0/cortexm/security}/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/crypto.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/hashes.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/injection.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/mind.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/pii.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/rbac.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/sandbox.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/zk_hamming.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/security/zk_sql.py +0 -0
- {cortexm-0.3.0/cortexm/text → cortexm-0.5.0/cortexm/server}/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/server/metrics.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/server/rest.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/server/sparql.py +0 -0
- {cortexm-0.3.0/cortexm/trace → cortexm-0.5.0/cortexm/text}/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/dissim.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/embedder.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/fuzzy.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/idiolect.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/labse.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/text/tokenizer.py +0 -0
- {cortexm-0.3.0/cortexm/vsa → cortexm-0.5.0/cortexm/trace}/__init__.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/blob_arena.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/consolidate.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/contradictions.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/dedup.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/edges.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/fact.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/fade.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/lifecycle.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/rebuild.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/rules.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/structural.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/trace/tmt.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/util.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/attribution.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/cleanup.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/codecs.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/hologram_overlay.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/index.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/ops.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/palace.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/role_vectors.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/slb.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/tlsh_trie.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm/vsa/working_memory.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm.egg-info/dependency_links.txt +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm.egg-info/entry_points.txt +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm.egg-info/requires.txt +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/cortexm.egg-info/top_level.txt +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/setup.cfg +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_arxiv_improvements.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_cognition_and_provenance.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_engineering_push_2026_08_28.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_enterprise.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_fabric.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_federation.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_kinship_extraction.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_labse.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_list_superseded_intent.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_migration.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_new_modules.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_nsg.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_ppr.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_rerank.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_research_steals.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_research_steals_round2.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_rust_accel.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_sandbox_enrich.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_sparql_rest_v2.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_wal_recovery.py +0 -0
- {cortexm-0.3.0 → cortexm-0.5.0}/tests/test_zk_sql.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Deterministic agent memory. 96 bytes per fact. Zero LLM at ingest.
|
|
5
5
|
Author: Context-M Contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -27,9 +27,13 @@ Dynamic: license-file
|
|
|
27
27
|
<a href="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/test.yml"><img src="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
|
|
28
28
|
<a href="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/pr-gate.yml"><img src="https://img.shields.io/github/checks-status/ssmurfgg04-gif/context-m/main/.github/workflows/pr-gate.yml?label=pr-gate" alt="PR Gate"></a>
|
|
29
29
|
<a href="https://github.com/ssmurfgg04-gif/context-m/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License"></a>
|
|
30
|
-
<a href="https://pypi.org/project/
|
|
31
|
-
<a href="https://pypi.org/project/context-m-langchain/"><img src="https://img.shields.io/pypi/
|
|
30
|
+
<a href="https://pypi.org/project/cortexm/"><img src="https://img.shields.io/pypi/v/cortexm?color=%2334D058&label=pypi%20%7Ccortexm" alt="PyPI: cortexm"></a>
|
|
31
|
+
<a href="https://pypi.org/project/context-m-langchain/"><img src="https://img.shields.io/pypi/v/context-m-langchain?color=%2334D058&label=pypi%20%7Clangchain" alt="PyPI: context-m-langchain"></a>
|
|
32
|
+
<a href="https://www.npmjs.com/package/dsh-cortexm"><img src="https://img.shields.io/npm/v/dsh-cortexm?color=%2334D058&label=npm%20%7Cdsh-cortexm" alt="npm: dsh-cortexm"></a>
|
|
33
|
+
<a href="https://pypi.org/project/cortexm/"><img src="https://img.shields.io/pypi/pyversions/cortexm.svg?color=%2334D058" alt="Python versions"></a>
|
|
32
34
|
<a href="https://github.com/ssmurfgg04-gif/context-m/blob/main/AGENTS.md"><img src="https://img.shields.io/badge/AGENTS.md-2026-2f2f2f?logo=github" alt="AGENTS.md"></a>
|
|
35
|
+
<!-- MCP Registry badge — uncomment after submitting deploy/mcp-registry-submission.json to https://registry.modelcontextprotocol.io -->
|
|
36
|
+
<!-- <a href="https://registry.modelcontextprotocol.io/servers/contextm"><img src="https://img.shields.io/badge/MCP%20Registry-contextm-7c3aed" alt="MCP Registry"></a> -->
|
|
33
37
|
<!-- Trendshift badge slot — auto-renders when the repo actually trends. -->
|
|
34
38
|
<!-- <a href="https://trendshift.io/repositories/ssmurfgg04-gif/context-m"><img src="https://trendshift.io/api/badge/repositories/ssmurfgg04-gif/context-m.svg" alt="Trendshift"></a> -->
|
|
35
39
|
</div>
|
|
@@ -96,23 +100,41 @@ handling accented characters without crashing the trigger.
|
|
|
96
100
|
|
|
97
101
|
### Tier 4.3 — LongMemEval independent judge
|
|
98
102
|
|
|
99
|
-
| subtask | pre-fix | post-fix (2026-08-28) | Δ |
|
|
100
|
-
|
|
101
|
-
| single_hop | 1.0 | 1.0 | flat |
|
|
102
|
-
| knowledge_update | 0.333 |
|
|
103
|
-
| multi_session | 0.5 | 0.5 | flat |
|
|
104
|
-
| temporal_reasoning | 0.5 | 0.5 | flat |
|
|
105
|
-
| **overall** | 0.600 |
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
103
|
+
| subtask | pre-fix | post-fix (2026-08-28) | plugin-kernel (2026-08-29, v0.5.0) | Δ vs pre-fix |
|
|
104
|
+
|---|---|---|---|---|
|
|
105
|
+
| single_hop | 1.0 | 1.0 | 1.0 | flat |
|
|
106
|
+
| knowledge_update | 0.333 | 0.667 | **1.000** | 3× |
|
|
107
|
+
| multi_session | 0.5 | 0.5 | 0.5 | flat |
|
|
108
|
+
| temporal_reasoning | 0.5 | 0.5 | 0.5 | flat |
|
|
109
|
+
| **overall** | 0.600 | 0.700 | **0.800** | +20pp |
|
|
110
|
+
|
|
111
|
+
The v0.5.0 lift (0.700 → 0.800) comes from the new plugin kernel
|
|
112
|
+
+ verbatim tier: when the structured extractor misses a fact
|
|
113
|
+
("I'm now working at OpenAI" → role pattern), the FTS5 + int8
|
|
114
|
+
dense path catches it verbatim. Fusion then merges both tiers
|
|
115
|
+
at μ=0 cost. The 2 misses that remain are aggregation phrasing
|
|
116
|
+
("List all the places Bob has worked") and yes/no answer shape
|
|
117
|
+
("Did Bob move between sessions") — extractor limitations, not
|
|
118
|
+
memory limitations.
|
|
119
|
+
|
|
120
|
+
Reproduce: `python scripts/longmemeval_judge.py --out
|
|
121
|
+
benchmarks/results/longmemeval_v0.5.0.json` ·
|
|
122
|
+
[`benchmarks/results/longmemeval_v0.5.0.json`](benchmarks/results/longmemeval_v0.5.0.json).
|
|
123
|
+
|
|
124
|
+
Pre-plugin-kernel fixes (0.600 → 0.700): (1) `works_at` regex
|
|
125
|
+
contraction fix ("I'm now working at OpenAI" now extracts),
|
|
126
|
+
(2) role pattern `|$` lookahead + uppercase support ("I'm an ML
|
|
127
|
+
engineer" now extracts), (3) employment-anchored temporal window
|
|
128
|
+
(resolves "where did X live when at Y" via the works_at fact's
|
|
129
|
+
valid_from/valid_to).
|
|
130
|
+
|
|
131
|
+
Plugin-kernel fixes (0.700 → 0.800): the new verbatim tier (FTS5
|
|
132
|
+
+ int8 dense, MemPalace-style) catches "I'm now working at OpenAI"
|
|
133
|
+
verbatim when the structured extractor's role pattern still misses
|
|
134
|
+
it. The fusion bridge then merges both tiers at μ=0 cost. The 2
|
|
135
|
+
remaining misses are not memory failures — they are answer-shape
|
|
136
|
+
mismatches (the judge asks for a yes/no, the context block returns
|
|
137
|
+
a list of facts the LLM must reason over).
|
|
116
138
|
|
|
117
139
|
That is the capability profile of the μ=0 extractor on real phrasing:
|
|
118
140
|
strong on change-of-state statements, weak on identity/preference
|
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
<a href="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/test.yml"><img src="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/test.yml/badge.svg?branch=main" alt="Tests"></a>
|
|
8
8
|
<a href="https://github.com/ssmurfgg04-gif/context-m/actions/workflows/pr-gate.yml"><img src="https://img.shields.io/github/checks-status/ssmurfgg04-gif/context-m/main/.github/workflows/pr-gate.yml?label=pr-gate" alt="PR Gate"></a>
|
|
9
9
|
<a href="https://github.com/ssmurfgg04-gif/context-m/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License"></a>
|
|
10
|
-
<a href="https://pypi.org/project/
|
|
11
|
-
<a href="https://pypi.org/project/context-m-langchain/"><img src="https://img.shields.io/pypi/
|
|
10
|
+
<a href="https://pypi.org/project/cortexm/"><img src="https://img.shields.io/pypi/v/cortexm?color=%2334D058&label=pypi%20%7Ccortexm" alt="PyPI: cortexm"></a>
|
|
11
|
+
<a href="https://pypi.org/project/context-m-langchain/"><img src="https://img.shields.io/pypi/v/context-m-langchain?color=%2334D058&label=pypi%20%7Clangchain" alt="PyPI: context-m-langchain"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/dsh-cortexm"><img src="https://img.shields.io/npm/v/dsh-cortexm?color=%2334D058&label=npm%20%7Cdsh-cortexm" alt="npm: dsh-cortexm"></a>
|
|
13
|
+
<a href="https://pypi.org/project/cortexm/"><img src="https://img.shields.io/pypi/pyversions/cortexm.svg?color=%2334D058" alt="Python versions"></a>
|
|
12
14
|
<a href="https://github.com/ssmurfgg04-gif/context-m/blob/main/AGENTS.md"><img src="https://img.shields.io/badge/AGENTS.md-2026-2f2f2f?logo=github" alt="AGENTS.md"></a>
|
|
15
|
+
<!-- MCP Registry badge — uncomment after submitting deploy/mcp-registry-submission.json to https://registry.modelcontextprotocol.io -->
|
|
16
|
+
<!-- <a href="https://registry.modelcontextprotocol.io/servers/contextm"><img src="https://img.shields.io/badge/MCP%20Registry-contextm-7c3aed" alt="MCP Registry"></a> -->
|
|
13
17
|
<!-- Trendshift badge slot — auto-renders when the repo actually trends. -->
|
|
14
18
|
<!-- <a href="https://trendshift.io/repositories/ssmurfgg04-gif/context-m"><img src="https://trendshift.io/api/badge/repositories/ssmurfgg04-gif/context-m.svg" alt="Trendshift"></a> -->
|
|
15
19
|
</div>
|
|
@@ -76,23 +80,41 @@ handling accented characters without crashing the trigger.
|
|
|
76
80
|
|
|
77
81
|
### Tier 4.3 — LongMemEval independent judge
|
|
78
82
|
|
|
79
|
-
| subtask | pre-fix | post-fix (2026-08-28) | Δ |
|
|
80
|
-
|
|
81
|
-
| single_hop | 1.0 | 1.0 | flat |
|
|
82
|
-
| knowledge_update | 0.333 |
|
|
83
|
-
| multi_session | 0.5 | 0.5 | flat |
|
|
84
|
-
| temporal_reasoning | 0.5 | 0.5 | flat |
|
|
85
|
-
| **overall** | 0.600 |
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
| subtask | pre-fix | post-fix (2026-08-28) | plugin-kernel (2026-08-29, v0.5.0) | Δ vs pre-fix |
|
|
84
|
+
|---|---|---|---|---|
|
|
85
|
+
| single_hop | 1.0 | 1.0 | 1.0 | flat |
|
|
86
|
+
| knowledge_update | 0.333 | 0.667 | **1.000** | 3× |
|
|
87
|
+
| multi_session | 0.5 | 0.5 | 0.5 | flat |
|
|
88
|
+
| temporal_reasoning | 0.5 | 0.5 | 0.5 | flat |
|
|
89
|
+
| **overall** | 0.600 | 0.700 | **0.800** | +20pp |
|
|
90
|
+
|
|
91
|
+
The v0.5.0 lift (0.700 → 0.800) comes from the new plugin kernel
|
|
92
|
+
+ verbatim tier: when the structured extractor misses a fact
|
|
93
|
+
("I'm now working at OpenAI" → role pattern), the FTS5 + int8
|
|
94
|
+
dense path catches it verbatim. Fusion then merges both tiers
|
|
95
|
+
at μ=0 cost. The 2 misses that remain are aggregation phrasing
|
|
96
|
+
("List all the places Bob has worked") and yes/no answer shape
|
|
97
|
+
("Did Bob move between sessions") — extractor limitations, not
|
|
98
|
+
memory limitations.
|
|
99
|
+
|
|
100
|
+
Reproduce: `python scripts/longmemeval_judge.py --out
|
|
101
|
+
benchmarks/results/longmemeval_v0.5.0.json` ·
|
|
102
|
+
[`benchmarks/results/longmemeval_v0.5.0.json`](benchmarks/results/longmemeval_v0.5.0.json).
|
|
103
|
+
|
|
104
|
+
Pre-plugin-kernel fixes (0.600 → 0.700): (1) `works_at` regex
|
|
105
|
+
contraction fix ("I'm now working at OpenAI" now extracts),
|
|
106
|
+
(2) role pattern `|$` lookahead + uppercase support ("I'm an ML
|
|
107
|
+
engineer" now extracts), (3) employment-anchored temporal window
|
|
108
|
+
(resolves "where did X live when at Y" via the works_at fact's
|
|
109
|
+
valid_from/valid_to).
|
|
110
|
+
|
|
111
|
+
Plugin-kernel fixes (0.700 → 0.800): the new verbatim tier (FTS5
|
|
112
|
+
+ int8 dense, MemPalace-style) catches "I'm now working at OpenAI"
|
|
113
|
+
verbatim when the structured extractor's role pattern still misses
|
|
114
|
+
it. The fusion bridge then merges both tiers at μ=0 cost. The 2
|
|
115
|
+
remaining misses are not memory failures — they are answer-shape
|
|
116
|
+
mismatches (the judge asks for a yes/no, the context block returns
|
|
117
|
+
a list of facts the LLM must reason over).
|
|
96
118
|
|
|
97
119
|
That is the capability profile of the μ=0 extractor on real phrasing:
|
|
98
120
|
strong on change-of-state statements, weak on identity/preference
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Context-M — The Universal Neuro-Symbolic Memory Fabric.
|
|
2
|
+
|
|
3
|
+
Layer 1 Symbolic Trace : bi-temporal fact graph with contradiction
|
|
4
|
+
resolution, temporal edges, Datalog-lite rules.
|
|
5
|
+
Layer 2 VSA Memory Palace: holographic reduced representations (HRR) with
|
|
6
|
+
INT8 / Binary-HRR / RaBitQ / PQ codecs, a
|
|
7
|
+
page-clustered tree index and a semantic
|
|
8
|
+
lookaside buffer (SLB).
|
|
9
|
+
Bridge : μ=0 deterministic ingest (zero LLM calls), neuro-symbolic read
|
|
10
|
+
path with cryptographic provenance on every retrieval.
|
|
11
|
+
Kernel : plugin composability (Cordis-inspired). Mount verbatim,
|
|
12
|
+
structured, security, or your own; new users get verbatim +
|
|
13
|
+
structured by default.
|
|
14
|
+
|
|
15
|
+
Mem0-compatible surface: ``from cortexm import Memory``
|
|
16
|
+
Plugin kernel: ``from cortexm import Context, mount_default``
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
__version__ = "0.5.0"
|
|
22
|
+
|
|
23
|
+
# μ=0 protocol counter: number of LLM invocations used by this process.
|
|
24
|
+
# The BEAM-honest protocol requires this to stay 0 during ingest & retrieval.
|
|
25
|
+
LLM_CALLS = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _lazy_memory():
|
|
29
|
+
from cortexm.api.memory import Memory
|
|
30
|
+
|
|
31
|
+
return Memory
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __getattr__(name: str):
|
|
35
|
+
if name == "Memory":
|
|
36
|
+
return _lazy_memory()
|
|
37
|
+
if name == "Config":
|
|
38
|
+
from cortexm.config import Config
|
|
39
|
+
|
|
40
|
+
return Config
|
|
41
|
+
if name == "Pipeline":
|
|
42
|
+
from cortexm.pipeline import Pipeline
|
|
43
|
+
return Pipeline
|
|
44
|
+
if name == "Context":
|
|
45
|
+
from cortexm.kernel import Context
|
|
46
|
+
return Context
|
|
47
|
+
if name == "mount_default":
|
|
48
|
+
return _mount_default
|
|
49
|
+
if name == "LLM_CALLS":
|
|
50
|
+
from cortexm import metrics
|
|
51
|
+
|
|
52
|
+
return metrics.llm_calls()
|
|
53
|
+
raise AttributeError(name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _mount_default(*, db_path: str = ":memory:",
|
|
57
|
+
config: "Config | None" = None,
|
|
58
|
+
embedder=None,
|
|
59
|
+
mount_verbatim: bool = True,
|
|
60
|
+
mount_structured: bool = True,
|
|
61
|
+
mount_security: bool = False) -> "Context":
|
|
62
|
+
"""One-liner: build a kernel with verbatim + structured (default).
|
|
63
|
+
|
|
64
|
+
This is the recommended entry point for new users. It mounts:
|
|
65
|
+
1. a "db" service (sqlite3.Connection)
|
|
66
|
+
2. an "embedder" service (HashingEmbedder)
|
|
67
|
+
3. a "memory" service (cortexm.api.memory.Memory)
|
|
68
|
+
4. VerbatimPlugin (FTS5 + dense, MemPalace-style)
|
|
69
|
+
5. StructuredPlugin (bi-temporal Trace + VSA Palace)
|
|
70
|
+
6. (optional) SecurityPlugin (MINJA + MIND middleware)
|
|
71
|
+
|
|
72
|
+
Plugins that need a service mount AFTER the service is registered.
|
|
73
|
+
The kernel's mount order is the caller's responsibility; this
|
|
74
|
+
helper does it correctly so users don't have to think about it.
|
|
75
|
+
|
|
76
|
+
Usage::
|
|
77
|
+
|
|
78
|
+
from cortexm import mount_default
|
|
79
|
+
ctx = mount_default()
|
|
80
|
+
v = ctx.inject("verbatim")["verbatim"]
|
|
81
|
+
v.add(text="My dog's name is Charlie", user_id="alice",
|
|
82
|
+
source_tx_id=1)
|
|
83
|
+
hits = v.search(query="Charlie", user_id="alice", k=5)
|
|
84
|
+
"""
|
|
85
|
+
import sqlite3
|
|
86
|
+
from cortexm.kernel import Context
|
|
87
|
+
from cortexm.api.memory import Memory
|
|
88
|
+
from cortexm.config import Config
|
|
89
|
+
from cortexm.text.embedder import HashingEmbedder
|
|
90
|
+
from cortexm.plugins.verbatim import VerbatimPlugin
|
|
91
|
+
from cortexm.plugins.structured import StructuredPlugin
|
|
92
|
+
|
|
93
|
+
if config is None:
|
|
94
|
+
config = Config.from_env()
|
|
95
|
+
if db_path != ":memory:":
|
|
96
|
+
config.db_path = db_path
|
|
97
|
+
elif db_path != ":memory:":
|
|
98
|
+
config.db_path = db_path
|
|
99
|
+
|
|
100
|
+
ctx = Context()
|
|
101
|
+
|
|
102
|
+
# 1. Build the embedder first — both tiers share it
|
|
103
|
+
if embedder is None:
|
|
104
|
+
embedder = HashingEmbedder(
|
|
105
|
+
dims=getattr(config, "embed_dim", 768),
|
|
106
|
+
labse_enabled=getattr(config, "labse_enabled", False))
|
|
107
|
+
ctx.service("embedder", embedder)
|
|
108
|
+
|
|
109
|
+
# 2. Build the Memory — owns the TraceStore (sqlite3.Connection)
|
|
110
|
+
mem = Memory(config)
|
|
111
|
+
ctx.service("memory", mem)
|
|
112
|
+
|
|
113
|
+
# 3. The verbatim tier needs the SAME sqlite3 connection as the
|
|
114
|
+
# structured tier so both live in the same .db file. Pull the
|
|
115
|
+
# connection from the Memory's store.
|
|
116
|
+
db_conn = getattr(mem.store, "conn", None) or \
|
|
117
|
+
getattr(mem.store, "_conn", None) or sqlite3.connect(db_path)
|
|
118
|
+
ctx.service("db", db_conn)
|
|
119
|
+
|
|
120
|
+
# 4. Mount plugins in dependency order. Verbatim + structured
|
|
121
|
+
# both depend on services, not on each other, so order is
|
|
122
|
+
# flexible. Mount verbatim first so its table-creation runs
|
|
123
|
+
# before the structured tier's queries (avoids a race when
|
|
124
|
+
# the same .db is used for both).
|
|
125
|
+
#
|
|
126
|
+
# dispose_memory=False (the default) means: the caller owns
|
|
127
|
+
# the Memory + DB. dispose() does NOT close the SQLite
|
|
128
|
+
# connection. This is correct for production — users want
|
|
129
|
+
# their data to survive a kernel teardown / restart. Tests
|
|
130
|
+
# that want a hermetic teardown pass dispose_memory=True
|
|
131
|
+
# AND drop_tables_on_dispose=True explicitly.
|
|
132
|
+
if mount_verbatim:
|
|
133
|
+
ctx.mount(VerbatimPlugin())
|
|
134
|
+
if mount_structured:
|
|
135
|
+
ctx.mount(StructuredPlugin())
|
|
136
|
+
if mount_security:
|
|
137
|
+
from cortexm.plugins.security import SecurityPlugin
|
|
138
|
+
ctx.mount(SecurityPlugin())
|
|
139
|
+
|
|
140
|
+
return ctx
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = [
|
|
144
|
+
"Memory",
|
|
145
|
+
"Config",
|
|
146
|
+
"Pipeline",
|
|
147
|
+
"Context",
|
|
148
|
+
"mount_default",
|
|
149
|
+
"LLM_CALLS",
|
|
150
|
+
"__version__",
|
|
151
|
+
]
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Long-context recall — "memory past 20 steps".
|
|
2
|
+
|
|
3
|
+
This is the killer feature the user asked for explicitly:
|
|
4
|
+
|
|
5
|
+
> "use my token to submit the core of whatever a user needs
|
|
6
|
+
> should be the core attempt to get memory and recall past 20 steps"
|
|
7
|
+
|
|
8
|
+
Problem: every mainstream LLM has a context window that scrolls.
|
|
9
|
+
After ~20 turns of conversation (~8k tokens for a 32k-window model),
|
|
10
|
+
the early turns have scrolled out of the prompt. The LLM forgets
|
|
11
|
+
what was said at turn 1 by turn 25. This is the #1 pain point for
|
|
12
|
+
anyone building agents on top of an LLM — the model literally
|
|
13
|
+
cannot remember what it committed to two turns ago, let alone last
|
|
14
|
+
session.
|
|
15
|
+
|
|
16
|
+
cortexm's answer:
|
|
17
|
+
|
|
18
|
+
Every fact the extractor derives from each turn is persisted to
|
|
19
|
+
the bi-temporal Trace with a step number (run_id encodes the
|
|
20
|
+
session, agent_id encodes the agent, the chunk's `created_at`
|
|
21
|
+
encodes the step order). On retrieval, we DON'T just query by
|
|
22
|
+
relevance — we BIAS toward facts whose step is about to scroll
|
|
23
|
+
out of the LLM's window. A fact from step 5 in a 30-turn
|
|
24
|
+
conversation is far more valuable to surface now than a fact from
|
|
25
|
+
step 28 — the LLM still has step 28 in its prompt.
|
|
26
|
+
|
|
27
|
+
This is the asymmetric retrieval insight: the closer a fact is to
|
|
28
|
+
scrolling out, the more we should boost it. The standard cosine
|
|
29
|
+
score ranks by similarity; we multiply by a step-distance decay
|
|
30
|
+
that peaks at the LLM's window edge.
|
|
31
|
+
|
|
32
|
+
Concrete API:
|
|
33
|
+
|
|
34
|
+
m.recall_step(query, user_id="alice", current_step=30,
|
|
35
|
+
window=20, k=12)
|
|
36
|
+
|
|
37
|
+
→ returns the top-k facts RELEVANT to the query AND in danger of
|
|
38
|
+
scrolling out of the LLM's window.
|
|
39
|
+
|
|
40
|
+
m.stepped_context_block(query, user_id="alice", current_step=30,
|
|
41
|
+
window=20, k=12)
|
|
42
|
+
|
|
43
|
+
→ returns a ready-to-inject markdown context block:
|
|
44
|
+
|
|
45
|
+
## Recalled memory (steps 1–10 about to scroll out)
|
|
46
|
+
- **[step 5]** Alice works at Google (conf=0.92, valid_from=2026-08-01)
|
|
47
|
+
- **[step 8]** Alice's birthday is 1990-05-12 (conf=0.88)
|
|
48
|
+
- **[step 11]** Alice prefers Python (conf=0.85)
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
This is the "memory past 20 steps" UX. Drop it into your agent's
|
|
52
|
+
system prompt template, and the LLM never forgets — even if it
|
|
53
|
+
physically scrolled the early turns out of its window.
|
|
54
|
+
|
|
55
|
+
Lean and simple: ~200 LoC, pure Python, μ=0 (no LLM, deterministic
|
|
56
|
+
step-distance decay multiplied onto the existing VSA fusion score).
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import datetime as _dt
|
|
61
|
+
import math
|
|
62
|
+
from datetime import datetime, timezone
|
|
63
|
+
from typing import Any
|
|
64
|
+
|
|
65
|
+
from cortexm.util import parse_ts
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _step_for_fact(fact, store) -> int | None:
|
|
69
|
+
"""Best-effort step number for a fact. We use chunk.created_at
|
|
70
|
+
ordinal within the (user_id, agent_id, run_id) scope — the order
|
|
71
|
+
in which the chunk entered the Trace. If the store doesn't expose
|
|
72
|
+
a step field directly, we synthesize one from created_at ordering.
|
|
73
|
+
|
|
74
|
+
Returns None if no chunk is attached (machine-derived facts
|
|
75
|
+
with no source — these don't have a meaningful step number).
|
|
76
|
+
"""
|
|
77
|
+
if not fact.source_id:
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
chunk = store.get_chunk(fact.source_id)
|
|
81
|
+
if not chunk:
|
|
82
|
+
return None
|
|
83
|
+
# if the schema already has a step column, use it
|
|
84
|
+
step = chunk.get("step") if isinstance(chunk, dict) else None
|
|
85
|
+
if step is not None:
|
|
86
|
+
return int(step)
|
|
87
|
+
# otherwise use created_at ordering — query the store for
|
|
88
|
+
# the rank of this chunk's created_at among same-scope chunks
|
|
89
|
+
ca = chunk.get("created_at") if isinstance(chunk, dict) else None
|
|
90
|
+
if not ca:
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
row = store.conn.execute(
|
|
94
|
+
"SELECT COUNT(*) AS n FROM chunks "
|
|
95
|
+
"WHERE user_id=? AND created_at < ?",
|
|
96
|
+
(fact.user_id, ca)).fetchone()
|
|
97
|
+
return int(row["n"]) + 1 if row else None
|
|
98
|
+
except Exception:
|
|
99
|
+
return None
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _step_distance_boost(step: int | None, current_step: int,
|
|
105
|
+
window: int) -> float:
|
|
106
|
+
"""Asymmetric retrieval: boost facts close to scrolling out of
|
|
107
|
+
the LLM's window.
|
|
108
|
+
|
|
109
|
+
If current_step = 30 and window = 20, the LLM still sees steps
|
|
110
|
+
11..30. Step 10 is ABOUT to scroll out — boost it most. Step 1
|
|
111
|
+
has already scrolled out — boost it strongly too. Step 25 is
|
|
112
|
+
still in the LLM's prompt — boost it less (the LLM already has it).
|
|
113
|
+
|
|
114
|
+
The boost function is a Gaussian centered at (current_step -
|
|
115
|
+
window), so it peaks at the window edge:
|
|
116
|
+
|
|
117
|
+
boost(step) = 1 + peak * exp(-(step - (current_step - window))^2
|
|
118
|
+
/ (2 * sigma^2))
|
|
119
|
+
|
|
120
|
+
Plus a constant floor of 1.0 (we never zero-out a fact — the
|
|
121
|
+
underlying VSA score still ranks it; we only nudge ordering).
|
|
122
|
+
"""
|
|
123
|
+
if step is None:
|
|
124
|
+
return 1.0 # no step info → no boost, no penalty
|
|
125
|
+
if current_step <= 0 or window <= 0:
|
|
126
|
+
return 1.0
|
|
127
|
+
edge = max(0, current_step - window)
|
|
128
|
+
sigma = max(1.0, window / 3.0) # spread = 1/3 of window
|
|
129
|
+
peak = 0.6 # max +60% boost at the window edge
|
|
130
|
+
# Gaussian centered at the edge, but also weighted for already-
|
|
131
|
+
# scrolled-out facts (step < edge) — they get full peak boost
|
|
132
|
+
# because the LLM has zero access to them now.
|
|
133
|
+
if step <= edge:
|
|
134
|
+
return 1.0 + peak
|
|
135
|
+
return 1.0 + peak * math.exp(
|
|
136
|
+
-((step - edge) ** 2) / (2 * sigma * sigma))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def recall_step(memory, query: str, *, user_id: str | None = None,
|
|
140
|
+
agent_id: str | None = None, run_id: str | None = None,
|
|
141
|
+
current_step: int = 0, window: int = 20,
|
|
142
|
+
k: int = 12) -> dict:
|
|
143
|
+
"""Asymmetric retrieval: top-k facts RELEVANT to the query AND in
|
|
144
|
+
danger of scrolling out of the LLM window.
|
|
145
|
+
|
|
146
|
+
Pipeline:
|
|
147
|
+
1. Run the standard reader.search() — produces top-K candidates
|
|
148
|
+
ranked by VSA + symbolic fusion.
|
|
149
|
+
2. For each candidate fact, compute the step-distance boost.
|
|
150
|
+
3. Re-rank by (fusion_score * boost). Top-k wins.
|
|
151
|
+
|
|
152
|
+
Returns a dict shaped like ``m.search()`` but with extra fields
|
|
153
|
+
per memory:
|
|
154
|
+
- step: int | None
|
|
155
|
+
- step_distance_boost: float
|
|
156
|
+
- scrolled_out: bool (True if step <= current_step - window)
|
|
157
|
+
"""
|
|
158
|
+
user_id = user_id or memory.config.default_user_id
|
|
159
|
+
# over-fetch then re-rank — we want enough candidates so the
|
|
160
|
+
# step-distance re-ordering has room to surface scrolled-out facts
|
|
161
|
+
raw_k = max(k * 4, 24)
|
|
162
|
+
result = memory.reader.search(query, user_id=user_id, agent_id=agent_id,
|
|
163
|
+
run_id=run_id, k=raw_k)
|
|
164
|
+
scored: list[tuple[float, Any, int | None, float, bool]] = []
|
|
165
|
+
for f in result.facts:
|
|
166
|
+
step = _step_for_fact(f, memory.store)
|
|
167
|
+
boost = _step_distance_boost(step, current_step, window)
|
|
168
|
+
# underlying fusion score (cosine + symbolic + chunk_recall)
|
|
169
|
+
fs = float(getattr(f, "score", 0.0) or
|
|
170
|
+
getattr(f, "fusion_score", 0.0) or 0.0)
|
|
171
|
+
# if the reader didn't attach a score, fall back to confidence
|
|
172
|
+
if fs <= 0.0:
|
|
173
|
+
fs = float(getattr(f, "confidence", 0.0) or 0.0)
|
|
174
|
+
new_score = fs * boost
|
|
175
|
+
scrolled_out = step is not None and current_step > 0 and step <= (
|
|
176
|
+
current_step - window)
|
|
177
|
+
scored.append((new_score, f, step, boost, scrolled_out))
|
|
178
|
+
scored.sort(key=lambda x: -x[0])
|
|
179
|
+
top = scored[:k]
|
|
180
|
+
|
|
181
|
+
out_memories = []
|
|
182
|
+
for new_score, f, step, boost, scrolled_out in top:
|
|
183
|
+
out_memories.append({
|
|
184
|
+
"id": f.id,
|
|
185
|
+
"memory": f"{f.subject} | {f.relation} | {f.value}",
|
|
186
|
+
"step": step,
|
|
187
|
+
"step_distance_boost": round(boost, 3),
|
|
188
|
+
"scrolled_out": scrolled_out,
|
|
189
|
+
"fusion_score": round(new_score, 4),
|
|
190
|
+
"confidence": float(getattr(f, "confidence", 0.0) or 0.0),
|
|
191
|
+
"valid_from": str(f.valid_from) if f.valid_from else None,
|
|
192
|
+
"valid_to": str(f.valid_to) if f.valid_to else None,
|
|
193
|
+
"source_snippet": _snippet(memory, f),
|
|
194
|
+
})
|
|
195
|
+
return {
|
|
196
|
+
"query": query,
|
|
197
|
+
"user_id": user_id,
|
|
198
|
+
"current_step": current_step,
|
|
199
|
+
"window": window,
|
|
200
|
+
"results": out_memories,
|
|
201
|
+
"context_block": _format_context_block(
|
|
202
|
+
out_memories, current_step, window),
|
|
203
|
+
"llm_calls": 0,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _snippet(memory, f) -> str:
|
|
208
|
+
if not f.source_id:
|
|
209
|
+
return ""
|
|
210
|
+
chunk = memory.store.get_chunk(f.source_id)
|
|
211
|
+
if chunk and chunk.get("text"):
|
|
212
|
+
return chunk["text"][:160]
|
|
213
|
+
return ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _format_context_block(mems: list[dict], current_step: int,
|
|
217
|
+
window: int) -> str:
|
|
218
|
+
"""Render a markdown context block ready to inject into the LLM
|
|
219
|
+
system prompt. Grouped by 'scrolled out' vs 'in window' so the
|
|
220
|
+
model sees the structure clearly."""
|
|
221
|
+
if not mems:
|
|
222
|
+
return ""
|
|
223
|
+
edge = max(0, current_step - window)
|
|
224
|
+
scrolled = [m for m in mems if m["scrolled_out"]]
|
|
225
|
+
in_win = [m for m in mems if not m["scrolled_out"]]
|
|
226
|
+
lines = []
|
|
227
|
+
if scrolled:
|
|
228
|
+
lines.append(f"## Recalled memory (steps 1–{edge} — scrolled "
|
|
229
|
+
f"out of context window)")
|
|
230
|
+
for m in scrolled:
|
|
231
|
+
lines.append(f"- **[step {m['step']}]** {m['memory']} "
|
|
232
|
+
f"(conf={m['confidence']:.2f}, "
|
|
233
|
+
f"valid_from={m.get('valid_from', 'n/a')})")
|
|
234
|
+
if m.get("source_snippet"):
|
|
235
|
+
lines.append(f" > …{m['source_snippet'][:120]}")
|
|
236
|
+
if in_win:
|
|
237
|
+
lines.append(f"\n## Active memory (steps {edge+1}–{current_step} "
|
|
238
|
+
f"— still in window, surfaced for relevance)")
|
|
239
|
+
for m in in_win:
|
|
240
|
+
lines.append(f"- **[step {m['step']}]** {m['memory']} "
|
|
241
|
+
f"(conf={m['confidence']:.2f})")
|
|
242
|
+
return "\n".join(lines)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def stepped_context_block(memory, query: str, *,
|
|
246
|
+
user_id: str | None = None,
|
|
247
|
+
current_step: int = 0,
|
|
248
|
+
window: int = 20, k: int = 12) -> str:
|
|
249
|
+
"""Convenience wrapper — returns just the context_block string."""
|
|
250
|
+
return recall_step(memory, query, user_id=user_id,
|
|
251
|
+
current_step=current_step, window=window,
|
|
252
|
+
k=k)["context_block"]
|