cluxion-Agentplugin-AutoClearMemory 0.2.0__py3-none-any.whl
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.
- cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/METADATA +145 -0
- cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/RECORD +19 -0
- cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/WHEEL +4 -0
- cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/entry_points.txt +5 -0
- forgetforge/__init__.py +5 -0
- forgetforge/adapters/__init__.py +3 -0
- forgetforge/adapters/hermes.py +180 -0
- forgetforge/archive.py +47 -0
- forgetforge/cli.py +236 -0
- forgetforge/config.py +49 -0
- forgetforge/contradiction.py +78 -0
- forgetforge/db.py +321 -0
- forgetforge/hot_inject.py +31 -0
- forgetforge/import_brief.py +45 -0
- forgetforge/pruner.py +76 -0
- forgetforge/recall.py +131 -0
- forgetforge/rust_bridge.py +137 -0
- forgetforge/schemas.py +79 -0
- forgetforge/store.py +89 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cluxion-Agentplugin-AutoClearMemory
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Universal agent memory plugin (ForgetForge): recall-centric retention, tiered forgetting, Rust scoring engine for Hermes, Claude Code, and Codex.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
|
|
6
|
+
Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
|
|
7
|
+
Project-URL: Issues, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory/issues
|
|
8
|
+
Author-email: cluxion <algocean1204@users.noreply.github.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: claude-code,cluxion,codex,forgetforge,hermes-agent,memory,plugin,recall
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Plugins
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: pyarrow>=15.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
26
|
+
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# cluxion-Agentplugin-AutoClearMemory (ForgetForge)
|
|
30
|
+
|
|
31
|
+
범용 에이전트 **망각·기억 강화 플러그인** — **Hermes, Claude Code, Codex, Grok Build**에서 동일 core로 동작합니다.
|
|
32
|
+
|
|
33
|
+
**Repository:** https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
|
|
34
|
+
|
|
35
|
+
## 한 줄 요약
|
|
36
|
+
|
|
37
|
+
세션 메모리가 쌓일수록 컨텍스트가 비대해집니다. ForgetForge는 **회상(Recall) 횟수**로 기억 강도를 계산하고, 오래되거나 쓰이지 않은 기억을 자동으로 낮은 tier로 내립니다. **연결된 AI**가 skill·도구 지시에 따라 recall/status/keep/forget을 호출합니다.
|
|
38
|
+
|
|
39
|
+
## 범용 에이전트 + Rust-First
|
|
40
|
+
|
|
41
|
+
| 계층 | 구현 |
|
|
42
|
+
|------|------|
|
|
43
|
+
| **Rust** (`forgetforge-engine`) | Retention scoring, tier decision |
|
|
44
|
+
| **Python** (`forgetforge`) | SQLite, recall tracker, pruner, CLI |
|
|
45
|
+
| **Agent adapter** | Hermes plugin, Claude skill, Codex CLI 가이드 |
|
|
46
|
+
|
|
47
|
+
내부 로직은 **Rust 중심**. Python·Skill은 등록·DB·JSON bridge **thin wrapper**입니다.
|
|
48
|
+
Rust 바이너리가 없어도 **Python fallback**으로 동일 공식이 동작합니다.
|
|
49
|
+
|
|
50
|
+
## 이 플러그인의 역할
|
|
51
|
+
|
|
52
|
+
- **Recall이 reinforcement**: 저장 횟수가 아니라 실제 회상 \(N_r\)가 강도를 결정
|
|
53
|
+
- **Tiered Memory**: Hot / Warm-Episodic / Warm-Semantic / Warm-Procedural / Cold
|
|
54
|
+
- **Decay + Boost**: Ebbinghaus forgetting curve + spaced repetition
|
|
55
|
+
- **FTS5 recall**: SQLite full-text search + LIKE fallback
|
|
56
|
+
- **Brief handoff**: preprocessing/supercoder brief → episodic memory (`import-brief`)
|
|
57
|
+
- **Hot inject**: Hermes `pre_llm_call` hook으로 hot tier 자동 주입
|
|
58
|
+
- **Contradiction hints**: store 시 유사·부정 기억 경고
|
|
59
|
+
- **Parquet archive**: cold tier → `~/.forgetforge/archive/` (parquet + jsonl + txt)
|
|
60
|
+
- **Background pruner**: 6시간 주기 tier migration (`forgetforge prune`)
|
|
61
|
+
- **Safety**: `#keep_forever`, `#forget_this` 사용자 태그
|
|
62
|
+
|
|
63
|
+
ForgetForge는 **모델·OAuth를 소유하지 않습니다.** 연결된 AI가 도구 결과를 읽고 맥락에 반영합니다.
|
|
64
|
+
|
|
65
|
+
## 연결된 AI가 하는 일
|
|
66
|
+
|
|
67
|
+
| 상황 | 연결된 AI 동작 |
|
|
68
|
+
|------|----------------|
|
|
69
|
+
| 맥락이 커짐 | `forgetforge status` 또는 `forgetforge_status` |
|
|
70
|
+
| 과거 사실 필요 | `forgetforge recall <topic>` (explicit) |
|
|
71
|
+
| 응답에 기억 사용 | `forgetforge_recall` + `layer: implicit` |
|
|
72
|
+
| 세션 마무리 점검 | `forgetforge_recall` + `layer: reflection` |
|
|
73
|
+
| `#keep_forever` | `forgetforge keep <id>` |
|
|
74
|
+
| `#forget_this` | `forgetforge forget <id>` |
|
|
75
|
+
|
|
76
|
+
Hermes는 `forgetforge_*` 도구, Claude/Codex는 skill + CLI를 동일 규칙으로 따릅니다.
|
|
77
|
+
|
|
78
|
+
## Retention formula
|
|
79
|
+
|
|
80
|
+
\[
|
|
81
|
+
R = e^{-t / S} \times \left(1 + 0.45 \cdot N_r + 0.30 \cdot I + 0.25 \cdot F \right), \quad S = \ln(1 + N_r)
|
|
82
|
+
\]
|
|
83
|
+
|
|
84
|
+
## Tier 요약
|
|
85
|
+
|
|
86
|
+
| Tier | 조건 | 연결된 AI 동작 |
|
|
87
|
+
|------|------|----------------|
|
|
88
|
+
| **Hot** | 최근 7일 + \(N_r \geq 1\) | recall 결과를 맥락에 우선 반영 |
|
|
89
|
+
| **Warm-Episodic** | \(R \geq 0.65\) | 필요 시 recall |
|
|
90
|
+
| **Warm-Semantic** | \(R \geq 0.80\) | 장기 사실로 유지 |
|
|
91
|
+
| **Warm-Procedural** | skill + \(N_r \geq 3\) | 절차·스킬로 유지 |
|
|
92
|
+
| **Cold** | \(R < 0.40\) or 180일 무회상 | archive, 필요 시에만 recall |
|
|
93
|
+
|
|
94
|
+
## 빠른 시작
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install cluxion-Agentplugin-AutoClearMemory
|
|
98
|
+
forgetforge init --agents=all
|
|
99
|
+
forgetforge check
|
|
100
|
+
hermes plugins enable forgetforge # Hermes
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Rust 가속 (선택)
|
|
105
|
+
cargo build --release --manifest-path rust/forgetforge_engine/Cargo.toml
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 공통 명령
|
|
109
|
+
|
|
110
|
+
| Command | Description |
|
|
111
|
+
|---------|-------------|
|
|
112
|
+
| `forgetforge store <id> --content "..."` | Store or update memory |
|
|
113
|
+
| `forgetforge recall <topic>` | Explicit retrieval + tier boost |
|
|
114
|
+
| `forgetforge keep <id>` | `#keep_forever` |
|
|
115
|
+
| `forgetforge forget <id>` | `#forget_this` |
|
|
116
|
+
| `forgetforge status` | Memory health |
|
|
117
|
+
| `forgetforge prune` | Pruner 1회 실행 |
|
|
118
|
+
| `forgetforge pruner-daemon` | Background pruner (interval from config) |
|
|
119
|
+
| `forgetforge import-brief --source supercoder --brief "..."` | Brief → episodic memory |
|
|
120
|
+
| `forgetforge hot-context` | Hot tier context block (CLI) |
|
|
121
|
+
|
|
122
|
+
## Hermes 도구 (`forgetforge` toolset)
|
|
123
|
+
|
|
124
|
+
| Tool | Description |
|
|
125
|
+
|------|-------------|
|
|
126
|
+
| `forgetforge_store` | Save or update memory (contradiction warnings when similar) |
|
|
127
|
+
| `forgetforge_recall` | FTS search + retrieval event (`layer`: explicit / implicit / reflection) |
|
|
128
|
+
| `forgetforge_status` | Tier counts, engine status |
|
|
129
|
+
| `forgetforge_keep` | Pin memory forever |
|
|
130
|
+
| `forgetforge_forget` | Mark for forgetting |
|
|
131
|
+
| `forgetforge_import_brief` | Import preprocessing/supercoder brief |
|
|
132
|
+
| `forgetforge_hot_context` | Hot tier block (also via `pre_llm_call` hook) |
|
|
133
|
+
|
|
134
|
+
## 문서
|
|
135
|
+
|
|
136
|
+
- [Docs/README.md](Docs/README.md) — **처음 읽는 분** + 목차
|
|
137
|
+
- [Docs/architecture.md](Docs/architecture.md)
|
|
138
|
+
- [Docs/design.md](Docs/design.md)
|
|
139
|
+
- [Docs/installation.md](Docs/installation.md)
|
|
140
|
+
- [Docs/agent-surfaces.md](Docs/agent-surfaces.md)
|
|
141
|
+
- [Docs/rust-architecture.md](Docs/rust-architecture.md)
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
Apache-2.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
forgetforge/__init__.py,sha256=Wkwg9PUkaGjHLMEaYqFYYRLoVSlb80kQ1nRzGUSsuSg,134
|
|
2
|
+
forgetforge/archive.py,sha256=Qt4pM1mGPAzy0RVd90P_SmuSviCWhlWP5A5wKSnJOxk,1482
|
|
3
|
+
forgetforge/cli.py,sha256=R8kU8zUbbK5DqPPAycjTqgLVrMdiB81kkX4hnIsJ68k,7898
|
|
4
|
+
forgetforge/config.py,sha256=1FRtN9xdLaA989tNmBvfoATzb2nsrEKmraavxJaQHpk,1571
|
|
5
|
+
forgetforge/contradiction.py,sha256=f0S0C3zBQIPyPK2FJkleujKsXt_BXsPXUe4NK2t3WyA,2187
|
|
6
|
+
forgetforge/db.py,sha256=U4Cf1Tj3pAj9ofqg0yM-40279iP06T-8MdOEcpWQsiQ,9574
|
|
7
|
+
forgetforge/hot_inject.py,sha256=rH_tUL9LCaBWYiap2Vi8_fA-dFpAFTpqQR3XFN9vmQA,843
|
|
8
|
+
forgetforge/import_brief.py,sha256=QAp-usJ1Ih2HWfSrGDjaw2gfpa4_VRFeOrSk_SWAj_E,1318
|
|
9
|
+
forgetforge/pruner.py,sha256=aUW48-inawfOG8ljNRrfI_GrNlyRq5A-_CB1s3Kl-x4,2589
|
|
10
|
+
forgetforge/recall.py,sha256=5_DLGNMBmvwNFPqGdjZI1cl3T0H-2gpXY_O5E4PPfnY,3754
|
|
11
|
+
forgetforge/rust_bridge.py,sha256=rnsYMk95bXynzXjNr-_Ar8PYEgWd37WlwgnaR4lzRbA,4161
|
|
12
|
+
forgetforge/schemas.py,sha256=d23VRrG3iqPaog3igcdkaugulZHKqU0CmyyXIB6C4jc,2236
|
|
13
|
+
forgetforge/store.py,sha256=pTApLBXk-zN2gLccUqWb-Qfms6MURFgqOLvxQaop9uo,2687
|
|
14
|
+
forgetforge/adapters/__init__.py,sha256=OcCz6Ib0sIILnkPL4xqNEU0TldjdQIi1NOxiGx6UeuU,70
|
|
15
|
+
forgetforge/adapters/hermes.py,sha256=5W_sqEy3uGdoT5i2FZayGFqfwGXtlvBuljhAMISCa4A,5170
|
|
16
|
+
cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/METADATA,sha256=xMpKzjiwZRScCvzDbcHao-DBwFb3xAgLFK5NFAmhONM,6401
|
|
17
|
+
cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
18
|
+
cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/entry_points.txt,sha256=144bKiDTBBCqpsiev-IoSdW5xmdZPLgWbvgT8K9NW30,128
|
|
19
|
+
cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/RECORD,,
|
forgetforge/__init__.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from forgetforge import db, hot_inject, import_brief
|
|
7
|
+
from forgetforge.config import load_config
|
|
8
|
+
from forgetforge.schemas import (
|
|
9
|
+
FORGET_SCHEMA,
|
|
10
|
+
HOT_CONTEXT_SCHEMA,
|
|
11
|
+
IMPORT_BRIEF_SCHEMA,
|
|
12
|
+
KEEP_SCHEMA,
|
|
13
|
+
RECALL_SCHEMA,
|
|
14
|
+
STATUS_SCHEMA,
|
|
15
|
+
STORE_SCHEMA,
|
|
16
|
+
)
|
|
17
|
+
from forgetforge import store as store_api
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register(ctx: object) -> None:
|
|
21
|
+
ctx.register_tool(
|
|
22
|
+
name="forgetforge_store",
|
|
23
|
+
toolset="forgetforge",
|
|
24
|
+
schema=STORE_SCHEMA,
|
|
25
|
+
handler=_wrap(_handle_store),
|
|
26
|
+
emoji="💾",
|
|
27
|
+
)
|
|
28
|
+
ctx.register_tool(
|
|
29
|
+
name="forgetforge_recall",
|
|
30
|
+
toolset="forgetforge",
|
|
31
|
+
schema=RECALL_SCHEMA,
|
|
32
|
+
handler=_wrap(_handle_recall),
|
|
33
|
+
emoji="🧠",
|
|
34
|
+
)
|
|
35
|
+
ctx.register_tool(
|
|
36
|
+
name="forgetforge_status",
|
|
37
|
+
toolset="forgetforge",
|
|
38
|
+
schema=STATUS_SCHEMA,
|
|
39
|
+
handler=_wrap(_handle_status),
|
|
40
|
+
emoji="📊",
|
|
41
|
+
)
|
|
42
|
+
ctx.register_tool(
|
|
43
|
+
name="forgetforge_keep",
|
|
44
|
+
toolset="forgetforge",
|
|
45
|
+
schema=KEEP_SCHEMA,
|
|
46
|
+
handler=_wrap(_handle_keep),
|
|
47
|
+
emoji="📌",
|
|
48
|
+
)
|
|
49
|
+
ctx.register_tool(
|
|
50
|
+
name="forgetforge_forget",
|
|
51
|
+
toolset="forgetforge",
|
|
52
|
+
schema=FORGET_SCHEMA,
|
|
53
|
+
handler=_wrap(_handle_forget),
|
|
54
|
+
emoji="🗑️",
|
|
55
|
+
)
|
|
56
|
+
ctx.register_tool(
|
|
57
|
+
name="forgetforge_import_brief",
|
|
58
|
+
toolset="forgetforge",
|
|
59
|
+
schema=IMPORT_BRIEF_SCHEMA,
|
|
60
|
+
handler=_wrap(_handle_import_brief),
|
|
61
|
+
emoji="📥",
|
|
62
|
+
)
|
|
63
|
+
ctx.register_tool(
|
|
64
|
+
name="forgetforge_hot_context",
|
|
65
|
+
toolset="forgetforge",
|
|
66
|
+
schema=HOT_CONTEXT_SCHEMA,
|
|
67
|
+
handler=_wrap(_handle_hot_context),
|
|
68
|
+
emoji="🔥",
|
|
69
|
+
)
|
|
70
|
+
register_hook = getattr(ctx, "register_hook", None)
|
|
71
|
+
if callable(register_hook):
|
|
72
|
+
register_hook("pre_llm_call", _pre_llm_hot_inject)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _pre_llm_hot_inject(**_: object) -> dict[str, str]:
|
|
76
|
+
return hot_inject.hot_context_payload()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _wrap(callback: Callable[[dict[str, object]], dict[str, object]]) -> Callable[[dict[str, object]], str]:
|
|
80
|
+
def handler(args: dict[str, object], **_: object) -> str:
|
|
81
|
+
try:
|
|
82
|
+
return json.dumps(callback(args), ensure_ascii=False, sort_keys=True)
|
|
83
|
+
except (ValueError, OSError) as exc:
|
|
84
|
+
return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True)
|
|
85
|
+
|
|
86
|
+
return handler
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _conn():
|
|
90
|
+
cfg = load_config()
|
|
91
|
+
return db.connect(cfg.db_path)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _handle_store(args: dict[str, object]) -> dict[str, object]:
|
|
95
|
+
conn = _conn()
|
|
96
|
+
try:
|
|
97
|
+
stored = store_api.store_memory(
|
|
98
|
+
conn,
|
|
99
|
+
memory_id=str(args.get("memory_id", "")),
|
|
100
|
+
content=str(args.get("content", "")),
|
|
101
|
+
importance=float(args.get("importance", 0.5)),
|
|
102
|
+
frequency=float(args.get("frequency", 0.0)),
|
|
103
|
+
is_procedural=bool(args.get("is_procedural", False)),
|
|
104
|
+
)
|
|
105
|
+
return {"ok": True, "stored": stored}
|
|
106
|
+
finally:
|
|
107
|
+
conn.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _handle_recall(args: dict[str, object]) -> dict[str, object]:
|
|
111
|
+
query = str(args.get("query", "")).strip()
|
|
112
|
+
layer = str(args.get("layer", "explicit"))
|
|
113
|
+
if not query:
|
|
114
|
+
raise ValueError("query is required")
|
|
115
|
+
conn = _conn()
|
|
116
|
+
try:
|
|
117
|
+
return store_api.recall_with_feedback(conn, query, layer=layer)
|
|
118
|
+
finally:
|
|
119
|
+
conn.close()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _handle_status(_: dict[str, object]) -> dict[str, object]:
|
|
123
|
+
from forgetforge import rust_bridge
|
|
124
|
+
|
|
125
|
+
conn = _conn()
|
|
126
|
+
try:
|
|
127
|
+
return {"ok": True, "stats": db.memory_stats(conn), "rust_engine": rust_bridge.engine_available()}
|
|
128
|
+
finally:
|
|
129
|
+
conn.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _handle_keep(args: dict[str, object]) -> dict[str, object]:
|
|
133
|
+
memory_id = str(args.get("memory_id", "")).strip()
|
|
134
|
+
if not memory_id:
|
|
135
|
+
raise ValueError("memory_id is required")
|
|
136
|
+
conn = _conn()
|
|
137
|
+
try:
|
|
138
|
+
ok = db.mark_keep_forever(conn, memory_id)
|
|
139
|
+
return {"ok": ok, "memory_id": memory_id}
|
|
140
|
+
finally:
|
|
141
|
+
conn.close()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _handle_forget(args: dict[str, object]) -> dict[str, object]:
|
|
145
|
+
memory_id = str(args.get("memory_id", "")).strip()
|
|
146
|
+
if not memory_id:
|
|
147
|
+
raise ValueError("memory_id is required")
|
|
148
|
+
conn = _conn()
|
|
149
|
+
try:
|
|
150
|
+
ok = db.mark_forget(conn, memory_id)
|
|
151
|
+
return {"ok": ok, "memory_id": memory_id}
|
|
152
|
+
finally:
|
|
153
|
+
conn.close()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _handle_import_brief(args: dict[str, object]) -> dict[str, object]:
|
|
157
|
+
conn = _conn()
|
|
158
|
+
try:
|
|
159
|
+
return import_brief.import_brief(
|
|
160
|
+
conn,
|
|
161
|
+
source=str(args.get("source", "manual")),
|
|
162
|
+
brief=str(args.get("brief", "")),
|
|
163
|
+
memory_id=str(args.get("memory_id", "")).strip() or None,
|
|
164
|
+
importance=float(args.get("importance", 0.65)),
|
|
165
|
+
)
|
|
166
|
+
finally:
|
|
167
|
+
conn.close()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _handle_hot_context(args: dict[str, object]) -> dict[str, object]:
|
|
171
|
+
limit = int(args.get("limit", 8))
|
|
172
|
+
conn = _conn()
|
|
173
|
+
try:
|
|
174
|
+
context = hot_inject.build_hot_context(conn, limit=limit)
|
|
175
|
+
return {"ok": True, "context": context, "has_hot": bool(context)}
|
|
176
|
+
finally:
|
|
177
|
+
conn.close()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = ["register"]
|
forgetforge/archive.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from forgetforge.config import ForgetForgeConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def write_cold_archive(
|
|
12
|
+
cfg: ForgetForgeConfig,
|
|
13
|
+
*,
|
|
14
|
+
memory_id: str,
|
|
15
|
+
content: str,
|
|
16
|
+
retention: float,
|
|
17
|
+
tier: str,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
cfg.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
summary = content.strip().splitlines()[0][:500] if content.strip() else ""
|
|
21
|
+
record = {
|
|
22
|
+
"memory_id": memory_id,
|
|
23
|
+
"retention": retention,
|
|
24
|
+
"tier": tier,
|
|
25
|
+
"summary": summary,
|
|
26
|
+
"archived_at": datetime.now(UTC).replace(microsecond=0).isoformat(),
|
|
27
|
+
}
|
|
28
|
+
parquet_path = cfg.archive_dir / f"{memory_id}.parquet"
|
|
29
|
+
jsonl_path = cfg.archive_dir / "cold_archive.jsonl"
|
|
30
|
+
written = "jsonl"
|
|
31
|
+
try:
|
|
32
|
+
import pyarrow as pa
|
|
33
|
+
import pyarrow.parquet as pq
|
|
34
|
+
|
|
35
|
+
table = pa.Table.from_pylist([record])
|
|
36
|
+
pq.write_table(table, parquet_path)
|
|
37
|
+
written = "parquet"
|
|
38
|
+
except ImportError:
|
|
39
|
+
parquet_path = None
|
|
40
|
+
with jsonl_path.open("a", encoding="utf-8") as handle:
|
|
41
|
+
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
42
|
+
txt_path = cfg.archive_dir / f"{memory_id}.txt"
|
|
43
|
+
txt_path.write_text(f"# retention={retention:.3f}\n{summary}\n", encoding="utf-8")
|
|
44
|
+
return {"format": written, "parquet": str(parquet_path) if parquet_path else None, "jsonl": str(jsonl_path)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["write_cold_archive"]
|
forgetforge/cli.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from forgetforge import __version__, db, hot_inject, import_brief, pruner, rust_bridge, store
|
|
11
|
+
from forgetforge.config import default_home, load_config
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
18
|
+
parser = _parser()
|
|
19
|
+
args = parser.parse_args(argv)
|
|
20
|
+
if args.command == "check":
|
|
21
|
+
return _check()
|
|
22
|
+
if args.command == "init":
|
|
23
|
+
return _init(args)
|
|
24
|
+
if args.command == "status":
|
|
25
|
+
return _status()
|
|
26
|
+
if args.command == "recall":
|
|
27
|
+
return _recall(args)
|
|
28
|
+
if args.command == "keep":
|
|
29
|
+
return _keep(args)
|
|
30
|
+
if args.command == "forget":
|
|
31
|
+
return _forget(args)
|
|
32
|
+
if args.command == "prune":
|
|
33
|
+
return _prune()
|
|
34
|
+
if args.command == "store":
|
|
35
|
+
return _store(args)
|
|
36
|
+
if args.command == "pruner-daemon":
|
|
37
|
+
return _pruner_daemon(args)
|
|
38
|
+
if args.command == "import-brief":
|
|
39
|
+
return _import_brief(args)
|
|
40
|
+
if args.command == "hot-context":
|
|
41
|
+
return _hot_context(args)
|
|
42
|
+
parser.print_help(sys.stderr)
|
|
43
|
+
return 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parser() -> argparse.ArgumentParser:
|
|
47
|
+
parser = argparse.ArgumentParser(prog="forgetforge")
|
|
48
|
+
parser.add_argument("--version", action="version", version=f"forgetforge {__version__}")
|
|
49
|
+
sub = parser.add_subparsers(dest="command")
|
|
50
|
+
sub.add_parser("check", help="Check Rust engine and database paths")
|
|
51
|
+
init = sub.add_parser("init", help="Initialize ~/.forgetforge and optional agent adapters")
|
|
52
|
+
init.add_argument("--agents", default="all", help="Comma-separated: hermes,claude,codex or all")
|
|
53
|
+
sub.add_parser("status", help="Memory health summary")
|
|
54
|
+
recall_cmd = sub.add_parser("recall", help="Recall memories matching a query")
|
|
55
|
+
recall_cmd.add_argument("query")
|
|
56
|
+
keep = sub.add_parser("keep", help="Mark memory as keep_forever")
|
|
57
|
+
keep.add_argument("memory_id")
|
|
58
|
+
forget = sub.add_parser("forget", help="Mark memory for forgetting")
|
|
59
|
+
forget.add_argument("memory_id")
|
|
60
|
+
sub.add_parser("prune", help="Run background pruner once")
|
|
61
|
+
store_cmd = sub.add_parser("store", help="Store or update a memory")
|
|
62
|
+
store_cmd.add_argument("memory_id")
|
|
63
|
+
store_cmd.add_argument("--content", required=True)
|
|
64
|
+
store_cmd.add_argument("--importance", type=float, default=0.5)
|
|
65
|
+
store_cmd.add_argument("--frequency", type=float, default=0.0)
|
|
66
|
+
store_cmd.add_argument("--procedural", action="store_true")
|
|
67
|
+
daemon = sub.add_parser("pruner-daemon", help="Run pruner on interval (background)")
|
|
68
|
+
daemon.add_argument("--interval-hours", type=int, default=None)
|
|
69
|
+
daemon.add_argument("--once", action="store_true", help="Run one cycle then exit")
|
|
70
|
+
brief = sub.add_parser("import-brief", help="Import preprocessing/supercoder brief into memory")
|
|
71
|
+
brief.add_argument("--source", choices=["preprocessing", "supercoder", "manual"], required=True)
|
|
72
|
+
brief.add_argument("--brief", required=True)
|
|
73
|
+
brief.add_argument("--memory-id", default=None)
|
|
74
|
+
brief.add_argument("--importance", type=float, default=0.65)
|
|
75
|
+
hot = sub.add_parser("hot-context", help="Print hot-tier context block")
|
|
76
|
+
hot.add_argument("--limit", type=int, default=8)
|
|
77
|
+
return parser
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _check() -> int:
|
|
81
|
+
home = default_home()
|
|
82
|
+
payload = {
|
|
83
|
+
"ok": True,
|
|
84
|
+
"home": str(home),
|
|
85
|
+
"rust_engine": rust_bridge.engine_available(),
|
|
86
|
+
"db_exists": (home / "db.sqlite").exists(),
|
|
87
|
+
}
|
|
88
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _init(args: argparse.Namespace) -> int:
|
|
93
|
+
cfg = load_config()
|
|
94
|
+
cfg.home.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
cfg.archive_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
conn = db.connect(cfg.db_path)
|
|
97
|
+
conn.close()
|
|
98
|
+
agents = _parse_agents(str(args.agents))
|
|
99
|
+
adapter_root = Path(__file__).resolve().parents[2] / "adapters"
|
|
100
|
+
installed = []
|
|
101
|
+
for agent in agents:
|
|
102
|
+
src = adapter_root / agent
|
|
103
|
+
if src.exists():
|
|
104
|
+
installed.append(agent)
|
|
105
|
+
example = Path(__file__).resolve().parents[2] / "config.yaml.example"
|
|
106
|
+
target = cfg.home / "config.yaml"
|
|
107
|
+
if example.exists() and not target.exists():
|
|
108
|
+
shutil.copy(example, target)
|
|
109
|
+
print(
|
|
110
|
+
json.dumps(
|
|
111
|
+
{
|
|
112
|
+
"ok": True,
|
|
113
|
+
"home": str(cfg.home),
|
|
114
|
+
"db": str(cfg.db_path),
|
|
115
|
+
"agents": installed,
|
|
116
|
+
"config": str(target),
|
|
117
|
+
},
|
|
118
|
+
ensure_ascii=False,
|
|
119
|
+
indent=2,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _status() -> int:
|
|
126
|
+
from forgetforge import recall
|
|
127
|
+
|
|
128
|
+
cfg = load_config()
|
|
129
|
+
conn = db.connect(cfg.db_path)
|
|
130
|
+
stats = db.memory_stats(conn)
|
|
131
|
+
hot = [recall.score_memory(row) for row in db.list_memories(conn, limit=50) if row.tier == "hot"]
|
|
132
|
+
conn.close()
|
|
133
|
+
print(
|
|
134
|
+
json.dumps(
|
|
135
|
+
{
|
|
136
|
+
"ok": True,
|
|
137
|
+
"rust_engine": rust_bridge.engine_available(),
|
|
138
|
+
"stats": stats,
|
|
139
|
+
"hot_samples": hot[:5],
|
|
140
|
+
},
|
|
141
|
+
ensure_ascii=False,
|
|
142
|
+
indent=2,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _recall(args: argparse.Namespace) -> int:
|
|
149
|
+
cfg = load_config()
|
|
150
|
+
conn = db.connect(cfg.db_path)
|
|
151
|
+
payload = store.recall_with_feedback(conn, str(args.query))
|
|
152
|
+
conn.close()
|
|
153
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _store(args: argparse.Namespace) -> int:
|
|
158
|
+
cfg = load_config()
|
|
159
|
+
conn = db.connect(cfg.db_path)
|
|
160
|
+
stored = store.store_memory(
|
|
161
|
+
conn,
|
|
162
|
+
memory_id=str(args.memory_id),
|
|
163
|
+
content=str(args.content),
|
|
164
|
+
importance=float(args.importance),
|
|
165
|
+
frequency=float(args.frequency),
|
|
166
|
+
is_procedural=bool(args.procedural),
|
|
167
|
+
)
|
|
168
|
+
conn.close()
|
|
169
|
+
print(json.dumps({"ok": True, "stored": stored}, ensure_ascii=False, indent=2))
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _pruner_daemon(args: argparse.Namespace) -> int:
|
|
174
|
+
pruner.run_pruner_daemon(interval_hours=args.interval_hours, run_once=bool(args.once))
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _import_brief(args: argparse.Namespace) -> int:
|
|
179
|
+
cfg = load_config()
|
|
180
|
+
conn = db.connect(cfg.db_path)
|
|
181
|
+
result = import_brief.import_brief(
|
|
182
|
+
conn,
|
|
183
|
+
source=str(args.source),
|
|
184
|
+
brief=str(args.brief),
|
|
185
|
+
memory_id=str(args.memory_id) if args.memory_id else None,
|
|
186
|
+
importance=float(args.importance),
|
|
187
|
+
)
|
|
188
|
+
conn.close()
|
|
189
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _hot_context(args: argparse.Namespace) -> int:
|
|
194
|
+
cfg = load_config()
|
|
195
|
+
conn = db.connect(cfg.db_path)
|
|
196
|
+
context = hot_inject.build_hot_context(conn, limit=int(args.limit))
|
|
197
|
+
conn.close()
|
|
198
|
+
print(json.dumps({"ok": True, "context": context, "has_hot": bool(context)}, ensure_ascii=False, indent=2))
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _keep(args: argparse.Namespace) -> int:
|
|
203
|
+
cfg = load_config()
|
|
204
|
+
conn = db.connect(cfg.db_path)
|
|
205
|
+
ok = db.mark_keep_forever(conn, str(args.memory_id))
|
|
206
|
+
conn.close()
|
|
207
|
+
print(json.dumps({"ok": ok, "memory_id": args.memory_id}, ensure_ascii=False))
|
|
208
|
+
return 0 if ok else 1
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _forget(args: argparse.Namespace) -> int:
|
|
212
|
+
cfg = load_config()
|
|
213
|
+
conn = db.connect(cfg.db_path)
|
|
214
|
+
ok = db.mark_forget(conn, str(args.memory_id))
|
|
215
|
+
conn.close()
|
|
216
|
+
print(json.dumps({"ok": ok, "memory_id": args.memory_id}, ensure_ascii=False))
|
|
217
|
+
return 0 if ok else 1
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _prune() -> int:
|
|
221
|
+
cfg = load_config()
|
|
222
|
+
conn = db.connect(cfg.db_path)
|
|
223
|
+
result = pruner.run_pruner(conn, config=cfg)
|
|
224
|
+
conn.close()
|
|
225
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _parse_agents(raw: str) -> list[str]:
|
|
230
|
+
if raw.strip().lower() == "all":
|
|
231
|
+
return ["hermes", "claude", "codex"]
|
|
232
|
+
return [part.strip().lower() for part in raw.split(",") if part.strip()]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
raise SystemExit(main())
|