foldkeep 0.9.2__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.
foldkeep-0.9.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 foldkeep contributors
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.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: foldkeep
3
+ Version: 0.9.2
4
+ Summary: Zero-dependency lossless context folding for agent sessions: pinned facts, tolerance routing, byte-for-byte expand.
5
+ Author: foldkeep contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 foldkeep contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Keywords: llm,agent,context-window,memory,compression
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
+ Requires-Python: >=3.10
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Dynamic: license-file
37
+
38
+ # foldkeep
39
+
40
+ **Fold the context. Keep the facts.**
41
+
42
+ foldkeep is a zero-dependency, deterministic context-compression engine for
43
+ long agent conversations. It rolls old turns into compressed form while a
44
+ pinned fact layer keeps every decision reachable — and a lossless base lets
45
+ you expand ANY turn back byte-for-byte, even after rolling and merging.
46
+
47
+ Pure Python stdlib. No LLM calls. Millisecond latency. SQLite-backed.
48
+
49
+ ## Why
50
+
51
+ Agent transcripts grow until the model drowns. Naive fixes lose the past:
52
+ tail-truncation drops early decisions; naive compression mangles numbers,
53
+ code and tool payloads. foldkeep keeps three guarantees:
54
+
55
+ 1. **Pinned facts survive** — every turn pins a salient evidence line;
56
+ a set-cover knapsack picks which lines fill the leftover budget so
57
+ entity coverage is maximized, not clustered.
58
+ 2. **Zero-tolerance content is never mangled** — code fences, JSON payloads
59
+ and tool-call shapes are routed away from lossy compression.
60
+ 3. **Nothing is ever lost** — originals persist on disk; `expand(name, seq)`
61
+ recovers any turn byte-for-byte after rollup, consolidation or render
62
+ drops.
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ from foldkeep import session as sess
68
+
69
+ sess.new("my-session")
70
+ for role, text in conversation: # push turns as they happen
71
+ sess.push("my-session", role, text) # auto-rolls when long
72
+
73
+ out, used, dropped = sess.render("my-session", budget=800)
74
+ # -> compressed body + key-facts registry + pinned-fact block, <= 800 tok
75
+
76
+ orig = sess.expand("my-session", 3) # byte-for-byte original of turn 3
77
+ hits = sess.search("my-session", "TKT-9001") # recall across rolled turns
78
+ md = sess.export_md("my-session", "vault.md") # inspectable Markdown vault
79
+ ```
80
+
81
+ CLI:
82
+
83
+ ```bash
84
+ python -m foldkeep selftest # 16-case correctness battery
85
+ ```
86
+
87
+ ## Proven, not promised
88
+
89
+ | Suite | Result |
90
+ |---|---|
91
+ | selftest (correctness) | 16/16 |
92
+ | stress_audit (adversarial probes) | 22/22 |
93
+ | marathon (long-session checkpoints) | 9/9, push ≤ 12 ms, render ≤ 80 ms |
94
+ | agent_selftest (real build-session replay, equal budgets) | wins 5/6 cells vs tail-only & head-trunc |
95
+ | chat_selftest (this project's own chat replay) | 12/12, recall 9/9 under forced rolling |
96
+
97
+ Benchmarks live in this repo (`agent_selftest.py`, `chat_selftest.py`,
98
+ `stress_audit.py`, `long_session_test.py`) — every number reproducible.
99
+
100
+ ## Design
101
+
102
+ - **Rolling compression**: newest stays verbatim, oldest compresses into a
103
+ head; hierarchical re-merge keeps the head shrinking.
104
+ - **Three-layer render**: compressed body + one-line entity registry +
105
+ set-cover pinned-fact block, all inside your token budget.
106
+ - **Loss-tolerance routing**: fences / JSON / tool-call shapes bypass
107
+ lossy paths entirely.
108
+ - **Lossless base**: `orig_text` column + `originals` table + `expand()`.
109
+ - **Operator knobs**: `OC_REGISTRY_MAX` env caps registry injection.
110
+
111
+ ## Status
112
+
113
+ v0.9.2. Research notes (`RESEARCH.md`) document the competitive landscape
114
+ (LLMLingua, leanctx, mnesis, FoldAgent, pi-fold, ECC, hermes-agent) and what
115
+ we learned from each.
116
+
117
+ MIT licensed. Windows/macOS/Linux, Python 3.10+.
@@ -0,0 +1,80 @@
1
+ # foldkeep
2
+
3
+ **Fold the context. Keep the facts.**
4
+
5
+ foldkeep is a zero-dependency, deterministic context-compression engine for
6
+ long agent conversations. It rolls old turns into compressed form while a
7
+ pinned fact layer keeps every decision reachable — and a lossless base lets
8
+ you expand ANY turn back byte-for-byte, even after rolling and merging.
9
+
10
+ Pure Python stdlib. No LLM calls. Millisecond latency. SQLite-backed.
11
+
12
+ ## Why
13
+
14
+ Agent transcripts grow until the model drowns. Naive fixes lose the past:
15
+ tail-truncation drops early decisions; naive compression mangles numbers,
16
+ code and tool payloads. foldkeep keeps three guarantees:
17
+
18
+ 1. **Pinned facts survive** — every turn pins a salient evidence line;
19
+ a set-cover knapsack picks which lines fill the leftover budget so
20
+ entity coverage is maximized, not clustered.
21
+ 2. **Zero-tolerance content is never mangled** — code fences, JSON payloads
22
+ and tool-call shapes are routed away from lossy compression.
23
+ 3. **Nothing is ever lost** — originals persist on disk; `expand(name, seq)`
24
+ recovers any turn byte-for-byte after rollup, consolidation or render
25
+ drops.
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from foldkeep import session as sess
31
+
32
+ sess.new("my-session")
33
+ for role, text in conversation: # push turns as they happen
34
+ sess.push("my-session", role, text) # auto-rolls when long
35
+
36
+ out, used, dropped = sess.render("my-session", budget=800)
37
+ # -> compressed body + key-facts registry + pinned-fact block, <= 800 tok
38
+
39
+ orig = sess.expand("my-session", 3) # byte-for-byte original of turn 3
40
+ hits = sess.search("my-session", "TKT-9001") # recall across rolled turns
41
+ md = sess.export_md("my-session", "vault.md") # inspectable Markdown vault
42
+ ```
43
+
44
+ CLI:
45
+
46
+ ```bash
47
+ python -m foldkeep selftest # 16-case correctness battery
48
+ ```
49
+
50
+ ## Proven, not promised
51
+
52
+ | Suite | Result |
53
+ |---|---|
54
+ | selftest (correctness) | 16/16 |
55
+ | stress_audit (adversarial probes) | 22/22 |
56
+ | marathon (long-session checkpoints) | 9/9, push ≤ 12 ms, render ≤ 80 ms |
57
+ | agent_selftest (real build-session replay, equal budgets) | wins 5/6 cells vs tail-only & head-trunc |
58
+ | chat_selftest (this project's own chat replay) | 12/12, recall 9/9 under forced rolling |
59
+
60
+ Benchmarks live in this repo (`agent_selftest.py`, `chat_selftest.py`,
61
+ `stress_audit.py`, `long_session_test.py`) — every number reproducible.
62
+
63
+ ## Design
64
+
65
+ - **Rolling compression**: newest stays verbatim, oldest compresses into a
66
+ head; hierarchical re-merge keeps the head shrinking.
67
+ - **Three-layer render**: compressed body + one-line entity registry +
68
+ set-cover pinned-fact block, all inside your token budget.
69
+ - **Loss-tolerance routing**: fences / JSON / tool-call shapes bypass
70
+ lossy paths entirely.
71
+ - **Lossless base**: `orig_text` column + `originals` table + `expand()`.
72
+ - **Operator knobs**: `OC_REGISTRY_MAX` env caps registry injection.
73
+
74
+ ## Status
75
+
76
+ v0.9.2. Research notes (`RESEARCH.md`) document the competitive landscape
77
+ (LLMLingua, leanctx, mnesis, FoldAgent, pi-fold, ECC, hermes-agent) and what
78
+ we learned from each.
79
+
80
+ MIT licensed. Windows/macOS/Linux, Python 3.10+.
@@ -0,0 +1,16 @@
1
+ """FoldKeep -- Agent Context Infrastructure.
2
+
3
+ Layers:
4
+ compressor : FastLingua engine (entity-preserving heuristic compression)
5
+ estimate : token & cost estimation across popular models
6
+ store : named compressed-context snapshots for agent workflows
7
+
8
+ Layers: core.compressor | estimate | store(SQLite memory bank) | packing | mcp_server.
9
+ """
10
+ __version__ = "0.6.0"
11
+
12
+ from .core.compressor import FastLingua # noqa: F401
13
+ from .benchmark.suite import run_bench, evolve # noqa: F401
14
+ from .estimate import estimate_tokens, estimate_cost, savings # noqa: F401
15
+ from .memory import store # noqa: F401
16
+ from .packing import pack, render # noqa: F401
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,246 @@
1
+ """FoldKeep unified CLI: oc compress / estimate / store / bench / evolve."""
2
+ import argparse
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .core.compressor import FastLingua, GENES
9
+ from .benchmark.suite import run_bench, evolve, EVOLVE_FILE
10
+ from .memory import store
11
+ from .estimate import estimate_cost, savings
12
+ from .packing import pack, render
13
+ from .diff import commit as ctx_commit, log as ctx_log, show as ctx_show
14
+ from . import session as sess
15
+
16
+ def _read(src):
17
+ if src == "-":
18
+ raw = sys.stdin.buffer.read()
19
+ try:
20
+ return raw.decode("utf-8")
21
+ except UnicodeDecodeError:
22
+ return raw.decode("gbk", "replace")
23
+ with open(src, encoding="utf-8-sig") as f:
24
+ return f.read().lstrip("\ufeff")
25
+
26
+ def _adapt(fl, stats, target):
27
+ try:
28
+ cur = {}
29
+ if os.path.exists(EVOLVE_FILE):
30
+ with open(EVOLVE_FILE, encoding="utf-8") as f:
31
+ cur = json.load(f)
32
+ err = stats["ratio"] - target
33
+ if abs(err) > 0.05:
34
+ lo, hi = GENES["dup_th"]
35
+ d = float(cur.get("dup_th", 0.72)) - (0.01 if err > 0 else -0.008)
36
+ cur["dup_th"] = round(min(hi, max(lo, d)), 4)
37
+ lo, hi = GENES["ent_w"]
38
+ d = float(cur.get("ent_w", 3.0)) - (0.08 if err > 0 else -0.10)
39
+ cur["ent_w"] = round(min(hi, max(lo, d)), 4)
40
+ lo, hi = GENES["cont_w"]
41
+ d = float(cur.get("cont_w", 1.0)) - (0.05 if err > 0 else -0.04)
42
+ cur["cont_w"] = round(min(hi, max(lo, d)), 4)
43
+ with open(EVOLVE_FILE, "w", encoding="utf-8") as f:
44
+ json.dump(cur, f, indent=1)
45
+ except Exception:
46
+ pass
47
+
48
+ def main(argv=None):
49
+ ap = argparse.ArgumentParser(
50
+ prog="foldkeep",
51
+ description="FoldKeep: Agent Context Infrastructure (compress | estimate | store | bench | evolve)")
52
+ ap.add_argument("--version", action="version", version="foldkeep " + __version__)
53
+ sub = ap.add_subparsers(dest="cmd", required=True)
54
+
55
+ c = sub.add_parser("compress", help="compress text via FastLingua engine")
56
+ c.add_argument("input", help="file path or '-' for stdin")
57
+ c.add_argument("--ratio", type=float, default=0.5)
58
+ c.add_argument("--mode", choices=FastLingua.MODES, default="balanced")
59
+ c.add_argument("--anchors", action="store_true")
60
+ c.add_argument("--collapse", action="store_true")
61
+ c.add_argument("--adapt", action="store_true")
62
+ c.add_argument("--no-auto", action="store_true")
63
+ c.add_argument("--save", metavar="NAME", help="also store snapshot for later reuse")
64
+ c.add_argument("-o", "--output")
65
+
66
+ e = sub.add_parser("estimate", help="token & cost estimate")
67
+ e.add_argument("input")
68
+ e.add_argument("--model", default="claude-sonnet")
69
+ e.add_argument("--compress", action="store_true", help="also show post-compression savings")
70
+
71
+ st = sub.add_parser("store", help="snapshot management")
72
+ st.add_argument("op", choices=["list", "get", "delete"])
73
+ st.add_argument("name", nargs="?")
74
+
75
+ b = sub.add_parser("bench", help="run benchmark")
76
+ b.add_argument("--full", action="store_true", help="1000-sample synthetic suite")
77
+
78
+ pk = sub.add_parser("pack", help="fit multiple files into a token budget")
79
+ pk.add_argument("inputs", nargs="+")
80
+ pk.add_argument("--budget", type=int, default=8000)
81
+ pk.add_argument("--ratio", type=float, default=0.5)
82
+ pk.add_argument("--model", default=None, help="also show cost for this model")
83
+ pk.add_argument("--save", metavar="NAME")
84
+ pk.add_argument("-o", "--output")
85
+ sub.add_parser("selftest", help="run the 16-case edge battery")
86
+
87
+ ss = sub.add_parser("session", help="long-conversation rolling compression")
88
+ ss.add_argument("op", choices=["new", "push", "show", "stat"])
89
+ ss.add_argument("name")
90
+ ss.add_argument("-r", "--role", default="user", choices=["user", "assistant", "system"])
91
+ ss.add_argument("-t", "--text")
92
+ ss.add_argument("-f", "--file")
93
+ ss.add_argument("--budget", type=int, default=None)
94
+ ss.add_argument("--roll-threshold", type=int, default=None,
95
+ help="override auto-rollup token threshold")
96
+ cx = sub.add_parser("ctx", help="context version chain: commit/log/show/restore")
97
+ cx.add_argument("op", choices=["commit", "log", "show", "restore"])
98
+ cx.add_argument("target", nargs="?", help="file path (commit) or version id")
99
+ cx.add_argument("-p", "--project", default="default")
100
+ cx.add_argument("-l", "--label", default="")
101
+ cx.add_argument("-o", "--output")
102
+ ev = sub.add_parser("evolve", help="self-evolve compressor genes")
103
+ ev.add_argument("--gens", type=int, default=8)
104
+ ev.add_argument("--pop", type=int, default=8)
105
+ ev.add_argument("--seed", type=int, default=42)
106
+
107
+ a = ap.parse_args(argv)
108
+
109
+ if a.cmd == "compress":
110
+ text = _read(a.input)
111
+ fl = FastLingua(a.mode, a.ratio, a.anchors,
112
+ auto=not a.no_auto, collapse=a.collapse)
113
+ comp, stats = fl.compress(text)
114
+ if a.adapt:
115
+ _adapt(fl, stats, a.ratio)
116
+ out = comp # engine already renders [F#] anchors when enabled
117
+ if a.output:
118
+ with open(a.output, "w", encoding="utf-8") as f:
119
+ f.write(out)
120
+ else:
121
+ print(out)
122
+ if a.save:
123
+ p = store.save(a.save, text, comp,
124
+ {"ratio": stats["ratio"], "mode": a.mode})
125
+ print("[stored] %s" % p, file=sys.stderr)
126
+
127
+ elif a.cmd == "estimate":
128
+ text = _read(a.input)
129
+ info = estimate_cost(text, a.model)
130
+ print("input : %6d tokens | $%.6f (%s)" % (info["tokens"], info["usd"], a.model))
131
+ if a.compress:
132
+ comp, _st = FastLingua("balanced", 0.5).compress(text)
133
+ after = estimate_cost(comp, a.model)
134
+ sv = savings(text, comp, a.model)
135
+ print("after : %6d tokens | $%.6f" % (after["tokens"], after["usd"]))
136
+ print("saved : %6d tokens | $%.6f | compressed to %.1f%%"
137
+ % (sv["tokens_saved"], sv["usd_saved"], sv["ratio"] * 100))
138
+
139
+ elif a.cmd == "store":
140
+ if a.op == "list":
141
+ rows = store.list_all()
142
+ if not rows:
143
+ print("(empty)")
144
+ for r in rows:
145
+ print("%-24s %7d -> %7d chars" % (r["name"], r["orig"], r["comp"]))
146
+ elif a.op == "get":
147
+ r = store.get(a.name or "")
148
+ print(r["text"] if r else "(not found)")
149
+ else:
150
+ print("deleted" if store.delete(a.name or "") else "(not found)")
151
+
152
+ elif a.cmd == "bench":
153
+ run_bench(getattr(a, "full", False))
154
+
155
+ elif a.cmd == "pack":
156
+ items = []
157
+ for src in a.inputs:
158
+ try:
159
+ items.append((os.path.basename(src), _read(src)))
160
+ except Exception as ex:
161
+ print("[skip] %s: %s" % (src, ex), file=sys.stderr)
162
+ if not items:
163
+ print("(no readable inputs)", file=sys.stderr)
164
+ return
165
+ res = pack(items, a.budget, a.ratio)
166
+ out = render(res)
167
+ if a.model:
168
+ from .estimate import estimate_cost
169
+ info = estimate_cost(out, a.model)
170
+ out += "\n=== cost: %d tokens | $%.6f (%s) ===" % (
171
+ info["tokens"], info["usd"], a.model)
172
+ if a.output:
173
+ with open(a.output, "w", encoding="utf-8") as f:
174
+ f.write(out)
175
+ else:
176
+ print(out)
177
+ if a.save:
178
+ pth = store.save(a.save, "".join(t for _, t in items), out,
179
+ {"budget": a.budget, "used": res["used"]})
180
+ print("[stored] %s" % pth, file=sys.stderr)
181
+
182
+ elif a.cmd == "selftest":
183
+ from .selftest import run
184
+ R, fails = run()
185
+ print("---")
186
+ print("TOTAL %d | PASS %d | FAIL %d" % (len(R), len(R) - len(fails), len(fails)))
187
+ if fails:
188
+ raise SystemExit(1)
189
+
190
+ elif a.cmd == "session":
191
+ if a.op == "new":
192
+ sess.new(a.name)
193
+ print("session '%s' ready" % a.name)
194
+ elif a.op == "push":
195
+ text = a.text if a.text is not None else (_read(a.file) if a.file else None)
196
+ if not text:
197
+ print("need -t TEXT or -f FILE", file=sys.stderr)
198
+ return
199
+ kw = {} if a.roll_threshold is None else {"roll_threshold": a.roll_threshold}
200
+ r = sess.push(a.name, a.role, text, **kw)
201
+ st = sess.stat(a.name)
202
+ print("pushed #%d (%s) | rolled %d | session: %d turns, ~%d tok"
203
+ % (r["seq"], a.role, r["rolled"], st["turns"], st["est_tokens"]))
204
+ elif a.op == "show":
205
+ out, used, dropped = sess.render(a.name, a.budget)
206
+ print(out)
207
+ else:
208
+ print(sess.stat(a.name))
209
+
210
+ elif a.cmd == "ctx":
211
+ if a.op == "commit":
212
+ if not a.target:
213
+ print("need file path", file=sys.stderr)
214
+ return
215
+ text = _read(a.target)
216
+ r = ctx_commit(a.project, text, a.label)
217
+ print("committed v%d [%s] %d -> %d chars (%.1f%%), stored %s"
218
+ % (r["id"], a.project, r["orig"], r["comp"],
219
+ r["ratio"] * 100, r["stored"]))
220
+ elif a.op == "log":
221
+ rows = ctx_log(a.project)
222
+ if not rows:
223
+ print("(empty)")
224
+ for r in rows:
225
+ print("v%-4d %-16s parent=%-4s %6d->%6d %s"
226
+ % (r["id"], r["label"] or "-", r["parent"] or "-",
227
+ r["orig"], r["comp"], "FULL" if r["full"] else "delta"))
228
+ else:
229
+ if not a.target or not a.target.isdigit():
230
+ print("need version id", file=sys.stderr)
231
+ return
232
+ text = ctx_show(int(a.target))
233
+ if text is None:
234
+ print("(version not found)", file=sys.stderr)
235
+ return
236
+ if a.output:
237
+ with open(a.output, "w", encoding="utf-8") as f:
238
+ f.write(text)
239
+ else:
240
+ print(text)
241
+
242
+ elif a.cmd == "evolve":
243
+ evolve(a.gens, a.pop, a.seed)
244
+
245
+ if __name__ == "__main__":
246
+ main()
File without changes