memgov 0.1.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.
memgov-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wangkevin2100-cell
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.
memgov-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: memgov
3
+ Version: 0.1.0
4
+ Summary: Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it.
5
+ Author: wangkevin2100-cell
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/wangkevin2100-cell/memgov
8
+ Project-URL: Repository, https://github.com/wangkevin2100-cell/memgov
9
+ Keywords: llm,ai-agents,memory,rag,decay,context,governance,mem0,zep
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: hypothesis>=6.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # memgov
28
+
29
+ > Provider-agnostic **governance layer** for AI agent memory. It doesn't store
30
+ > your memory — it governs the memory you already have (mem0 / Zep / a raw
31
+ > vector DB): decays it by recency × reuse, gates recall injection behind a
32
+ > confidence threshold so stale junk never reaches the prompt, and surfaces
33
+ > memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
34
+
35
+ ## The problem
36
+
37
+ The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
38
+ *retrieve*. But after a quarter of accumulation, teams hit the second-order
39
+ problem: **memory useful in week 1 is actively harmful in week 12, and nothing
40
+ ages it out.** Stores answer "what do I remember?" — none answer "what should I
41
+ have *forgotten*, and what's safe to inject right now?" That's governance, and
42
+ it's missing.
43
+
44
+ memgov fills that lane:
45
+
46
+ - **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
47
+ - **Confidence-gated recall** — always search, but only *inject* when retrieval
48
+ confidence clears a threshold and the memory isn't stale.
49
+ - **Memory health** — `active / fading / stale` ratios so silent degradation
50
+ becomes observable *before* answers get bad.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install memgov # zero required deps
56
+ ```
57
+
58
+ Python 3.11+.
59
+
60
+ ## Quick start
61
+
62
+ ```python
63
+ from memgov import Governor, Candidate
64
+
65
+ gov = Governor() # local SQLite sidecar
66
+
67
+ # register memories your store already holds, keyed by the store's native id
68
+ gov.track("mem-1", agent_id="bot")
69
+ gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
70
+
71
+ gov.recompute(agent_id="bot") # recompute decay scores + states
72
+
73
+ # on recall: your store returns candidates; memgov gates what actually injects
74
+ candidates = [Candidate("mem-1", confidence=0.82),
75
+ Candidate("mem-9", confidence=0.31)] # low-confidence
76
+ inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
77
+ print([c.mem_id for c in inject]) # -> ['mem-1']
78
+
79
+ print(gov.health(agent_id="bot")) # active/fading/stale snapshot
80
+ ```
81
+
82
+ ## How it works
83
+
84
+ - **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
85
+ the memory text — it tracks the signals decay needs).
86
+ - **`track / touch / reinforce`** record usage signals; `reinforce` (user
87
+ restated/confirmed) is the strongest keep signal.
88
+ - **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
89
+ and a `state`: `active | fading | stale | pinned`.
90
+ - **`gate`** is the core: it receives recall candidates and passes only those
91
+ with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
92
+ ever decaying.
93
+
94
+ ## See it (no deps, no API key)
95
+
96
+ ```bash
97
+ python examples/demo_governance.py
98
+ ```
99
+
100
+ ## Status
101
+
102
+ Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
103
+ SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
104
+ embedding-confidence helpers, health dashboard.
105
+
106
+ ## License
107
+
108
+ MIT.
memgov-0.1.0/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # memgov
2
+
3
+ > Provider-agnostic **governance layer** for AI agent memory. It doesn't store
4
+ > your memory — it governs the memory you already have (mem0 / Zep / a raw
5
+ > vector DB): decays it by recency × reuse, gates recall injection behind a
6
+ > confidence threshold so stale junk never reaches the prompt, and surfaces
7
+ > memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
8
+
9
+ ## The problem
10
+
11
+ The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
12
+ *retrieve*. But after a quarter of accumulation, teams hit the second-order
13
+ problem: **memory useful in week 1 is actively harmful in week 12, and nothing
14
+ ages it out.** Stores answer "what do I remember?" — none answer "what should I
15
+ have *forgotten*, and what's safe to inject right now?" That's governance, and
16
+ it's missing.
17
+
18
+ memgov fills that lane:
19
+
20
+ - **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
21
+ - **Confidence-gated recall** — always search, but only *inject* when retrieval
22
+ confidence clears a threshold and the memory isn't stale.
23
+ - **Memory health** — `active / fading / stale` ratios so silent degradation
24
+ becomes observable *before* answers get bad.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install memgov # zero required deps
30
+ ```
31
+
32
+ Python 3.11+.
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from memgov import Governor, Candidate
38
+
39
+ gov = Governor() # local SQLite sidecar
40
+
41
+ # register memories your store already holds, keyed by the store's native id
42
+ gov.track("mem-1", agent_id="bot")
43
+ gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
44
+
45
+ gov.recompute(agent_id="bot") # recompute decay scores + states
46
+
47
+ # on recall: your store returns candidates; memgov gates what actually injects
48
+ candidates = [Candidate("mem-1", confidence=0.82),
49
+ Candidate("mem-9", confidence=0.31)] # low-confidence
50
+ inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
51
+ print([c.mem_id for c in inject]) # -> ['mem-1']
52
+
53
+ print(gov.health(agent_id="bot")) # active/fading/stale snapshot
54
+ ```
55
+
56
+ ## How it works
57
+
58
+ - **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
59
+ the memory text — it tracks the signals decay needs).
60
+ - **`track / touch / reinforce`** record usage signals; `reinforce` (user
61
+ restated/confirmed) is the strongest keep signal.
62
+ - **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
63
+ and a `state`: `active | fading | stale | pinned`.
64
+ - **`gate`** is the core: it receives recall candidates and passes only those
65
+ with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
66
+ ever decaying.
67
+
68
+ ## See it (no deps, no API key)
69
+
70
+ ```bash
71
+ python examples/demo_governance.py
72
+ ```
73
+
74
+ ## Status
75
+
76
+ Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
77
+ SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
78
+ embedding-confidence helpers, health dashboard.
79
+
80
+ ## License
81
+
82
+ MIT.
@@ -0,0 +1,40 @@
1
+ """memgov — provider-agnostic governance layer for AI agent memory.
2
+
3
+ 不存储记忆,而是治理你已有的记忆(mem0 / Zep / 向量库):
4
+ - 按 recency × reuse 衰减,让陈旧记忆自然淡出
5
+ - 召回时按置信度门控注入,陈旧垃圾不进 prompt
6
+ - 出"记忆健康度"指标,让静默退化变可观测
7
+
8
+ Quick start::
9
+
10
+ from memgov import Governor, Candidate
11
+
12
+ gov = Governor()
13
+ gov.track("mem-1", agent_id="bot") # 登记一条记忆进治理层
14
+ gov.reinforce("mem-1", agent_id="bot") # 用户重申 -> 强化保留
15
+
16
+ gov.recompute(agent_id="bot") # 重算衰减分 + 状态
17
+ # 召回时门控:只放行置信度够且没陈旧的
18
+ keep = gov.gate([Candidate("mem-1", confidence=0.8)], agent_id="bot")
19
+ print(gov.health(agent_id="bot"))
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from .decay import decay_score, recency_factor, reuse_bonus
24
+ from .governor import Governor
25
+ from .models import Candidate, HealthReport, MemoryMeta, MemoryState
26
+ from .store import MetaStore
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "Governor",
32
+ "MetaStore",
33
+ "MemoryMeta",
34
+ "MemoryState",
35
+ "Candidate",
36
+ "HealthReport",
37
+ "decay_score",
38
+ "recency_factor",
39
+ "reuse_bonus",
40
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,42 @@
1
+ """memgov CLI:查看记忆健康度、重算衰减。
2
+
3
+ 用法:
4
+ memgov health [--agent NAME] [--db PATH] 查看健康度
5
+ memgov recompute [--agent NAME] [--db PATH] 重算衰减分+状态
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+
12
+ from .governor import Governor
13
+ from .store import MetaStore
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ p = argparse.ArgumentParser(prog="memgov")
18
+ sub = p.add_subparsers(dest="cmd", required=True)
19
+ for name in ("health", "recompute"):
20
+ sp = sub.add_parser(name)
21
+ sp.add_argument("--agent", default="default")
22
+ sp.add_argument("--db", default=".memgov/memgov.db")
23
+
24
+ args = p.parse_args(argv)
25
+ gov = Governor(store=MetaStore(args.db))
26
+
27
+ if args.cmd == "recompute":
28
+ n = gov.recompute(agent_id=args.agent)
29
+ print(f"recomputed {n} memories for agent '{args.agent}'")
30
+ return 0
31
+ if args.cmd == "health":
32
+ h = gov.health(agent_id=args.agent)
33
+ print(f"agent={h.agent_id} total={h.total} active={h.active} "
34
+ f"fading={h.fading} stale={h.stale} pinned={h.pinned} "
35
+ f"stale_ratio={h.stale_ratio:.2f} avg_score={h.avg_decay_score:.3f}")
36
+ return 0
37
+ p.print_help()
38
+ return 1
39
+
40
+
41
+ if __name__ == "__main__":
42
+ sys.exit(main())
@@ -0,0 +1,45 @@
1
+ """衰减评分:recency(时间新近)× reuse(复用/强化)。
2
+
3
+ 这是 memgov 的核心——决定一条记忆"现在还该不该算数"。纯函数,无副作用,
4
+ 便于属性测试。返回 [0,1] 的分数:越高 = 越该保留并参与注入。
5
+
6
+ 设计:
7
+ - recency:自上次访问起按半衰期指数衰减。越久没碰,分越低。
8
+ - reuse:访问/强化越多,越该保留——给一个有上限的加成(防止被刷爆)。
9
+ - pinned 由上层处理(钉住的不走衰减)。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import math
14
+
15
+ # 默认参数(可调)
16
+ DEFAULT_HALF_LIFE_DAYS = 14.0 # 14 天没访问,recency 减半
17
+ REINFORCE_BONUS = 0.15 # 每次强化的加成
18
+ MAX_REUSE_BONUS = 0.5 # reuse 加成上限
19
+ _SECONDS_PER_DAY = 86400.0
20
+
21
+
22
+ def recency_factor(age_seconds: float, half_life_days: float = DEFAULT_HALF_LIFE_DAYS) -> float:
23
+ """自上次访问的时间衰减因子,∈ (0, 1]。age=0 -> 1.0,越久越接近 0。"""
24
+ if age_seconds <= 0:
25
+ return 1.0
26
+ half_life_seconds = max(half_life_days, 1e-9) * _SECONDS_PER_DAY
27
+ return math.pow(0.5, age_seconds / half_life_seconds)
28
+
29
+
30
+ def reuse_bonus(access_count: int, reinforce_count: int) -> float:
31
+ """复用加成,∈ [0, MAX_REUSE_BONUS]。强化比普通访问权重高。"""
32
+ raw = REINFORCE_BONUS * reinforce_count + 0.02 * max(access_count, 0)
33
+ return min(max(raw, 0.0), MAX_REUSE_BONUS)
34
+
35
+
36
+ def decay_score(
37
+ age_seconds: float,
38
+ access_count: int = 0,
39
+ reinforce_count: int = 0,
40
+ half_life_days: float = DEFAULT_HALF_LIFE_DAYS,
41
+ ) -> float:
42
+ """综合衰减分,∈ [0,1]。recency 为基底,reuse 在其上加成后截断到 1。"""
43
+ base = recency_factor(age_seconds, half_life_days)
44
+ score = base + reuse_bonus(access_count, reinforce_count) * base
45
+ return min(max(score, 0.0), 1.0)
@@ -0,0 +1,137 @@
1
+ """Governor:记忆治理中间件主类。
2
+
3
+ 套在你已有的记忆存储前面,做三件事(spec 的三大核心):
4
+ 1. 衰减治理:track/touch/reinforce 记录信号,recompute 按 recency×reuse 重算分数+状态
5
+ 2. 召回门控:gate() 总是接收存储召回的候选,但只放行"置信度够 + 没陈旧"的注入
6
+ 3. 健康度:health() 出 active/fading/stale 比例,让"静默退化"变可观测
7
+ 它不替你存记忆,只治理你已有的记忆。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ from .decay import DEFAULT_HALF_LIFE_DAYS, decay_score
14
+ from .models import Candidate, HealthReport, MemoryMeta, MemoryState
15
+ from .store import MetaStore
16
+
17
+
18
+ class Governor:
19
+ def __init__(
20
+ self,
21
+ store: MetaStore | None = None,
22
+ *,
23
+ half_life_days: float = DEFAULT_HALF_LIFE_DAYS,
24
+ active_threshold: float = 0.5, # >= 视为 active
25
+ fading_threshold: float = 0.2, # >= 视为 fading,否则 stale
26
+ min_confidence: float = 0.6, # 召回注入的默认置信度门槛
27
+ ):
28
+ self.store = store or MetaStore()
29
+ self.half_life_days = half_life_days
30
+ self.active_threshold = active_threshold
31
+ self.fading_threshold = fading_threshold
32
+ self.min_confidence = min_confidence
33
+
34
+ # ---- 记录治理信号 ----
35
+ def track(self, mem_id: str, *, agent_id: str = "default",
36
+ source: str = "user_msg", now: float | None = None) -> MemoryMeta:
37
+ """登记一条新记忆进治理层。"""
38
+ t = now if now is not None else time.time()
39
+ m = MemoryMeta(mem_id=mem_id, agent_id=agent_id, created_utc=t,
40
+ last_access_utc=t, access_count=0, reinforce_count=0,
41
+ decay_score=1.0, state=MemoryState.ACTIVE,
42
+ source=source, last_eval_utc=t)
43
+ self.store.upsert(m)
44
+ return m
45
+
46
+ def touch(self, mem_id: str, *, agent_id: str = "default",
47
+ now: float | None = None) -> None:
48
+ """记忆被召回/读取——更新访问时间和计数。"""
49
+ m = self.store.get(mem_id, agent_id)
50
+ if m is None:
51
+ m = self.track(mem_id, agent_id=agent_id, now=now)
52
+ m.last_access_utc = now if now is not None else time.time()
53
+ m.access_count += 1
54
+ self.store.upsert(m)
55
+
56
+ def reinforce(self, mem_id: str, *, agent_id: str = "default",
57
+ now: float | None = None) -> None:
58
+ """记忆被显式强化(用户重申/确认)——最强的保留信号。"""
59
+ m = self.store.get(mem_id, agent_id)
60
+ if m is None:
61
+ m = self.track(mem_id, agent_id=agent_id, now=now)
62
+ m.reinforce_count += 1
63
+ m.last_access_utc = now if now is not None else time.time()
64
+ self.store.upsert(m)
65
+
66
+ def pin(self, mem_id: str, *, agent_id: str = "default") -> None:
67
+ m = self.store.get(mem_id, agent_id)
68
+ if m:
69
+ m.state = MemoryState.PINNED
70
+ self.store.upsert(m)
71
+
72
+ # ---- 重算衰减 ----
73
+ def recompute(self, *, agent_id: str = "default", now: float | None = None) -> int:
74
+ """重算某 agent 所有记忆的 decay_score 和 state。返回处理条数。"""
75
+ t = now if now is not None else time.time()
76
+ metas = self.store.list_for_agent(agent_id)
77
+ for m in metas:
78
+ if m.state == MemoryState.PINNED:
79
+ continue
80
+ age = max(t - m.last_access_utc, 0.0)
81
+ m.decay_score = decay_score(age, m.access_count, m.reinforce_count,
82
+ self.half_life_days)
83
+ m.state = self._state_for(m.decay_score)
84
+ m.last_eval_utc = t
85
+ self.store.upsert(m)
86
+ return len(metas)
87
+
88
+ def _state_for(self, score: float) -> MemoryState:
89
+ if score >= self.active_threshold:
90
+ return MemoryState.ACTIVE
91
+ if score >= self.fading_threshold:
92
+ return MemoryState.FADING
93
+ return MemoryState.STALE
94
+
95
+ # ---- 召回门控(核心价值)----
96
+ def gate(self, candidates: list[Candidate], *, agent_id: str = "default",
97
+ min_confidence: float | None = None,
98
+ include_fading: bool = True) -> list[Candidate]:
99
+ """门控召回注入:总是接收候选,但只放行该注入的。
100
+
101
+ 规则:置信度 >= 门槛 且 状态不是 stale(fading 可选放行)。
102
+ chosen 的会顺便 touch(被注入即被使用)。
103
+ """
104
+ thr = min_confidence if min_confidence is not None else self.min_confidence
105
+ chosen: list[Candidate] = []
106
+ for c in candidates:
107
+ if c.confidence < thr:
108
+ continue
109
+ m = self.store.get(c.mem_id, agent_id)
110
+ state = m.state if m else MemoryState.ACTIVE
111
+ if state == MemoryState.STALE:
112
+ continue
113
+ if state == MemoryState.FADING and not include_fading:
114
+ continue
115
+ chosen.append(c)
116
+ for c in chosen:
117
+ self.touch(c.mem_id, agent_id=agent_id)
118
+ return chosen
119
+
120
+ # ---- 健康度 ----
121
+ def health(self, *, agent_id: str = "default") -> HealthReport:
122
+ metas = self.store.list_for_agent(agent_id)
123
+ rep = HealthReport(agent_id=agent_id, total=len(metas))
124
+ if not metas:
125
+ return rep
126
+ for m in metas:
127
+ if m.state == MemoryState.ACTIVE:
128
+ rep.active += 1
129
+ elif m.state == MemoryState.FADING:
130
+ rep.fading += 1
131
+ elif m.state == MemoryState.STALE:
132
+ rep.stale += 1
133
+ elif m.state == MemoryState.PINNED:
134
+ rep.pinned += 1
135
+ rep.stale_ratio = rep.stale / rep.total
136
+ rep.avg_decay_score = sum(m.decay_score for m in metas) / rep.total
137
+ return rep
@@ -0,0 +1,52 @@
1
+ """数据模型:每条被治理记忆的 sidecar 元数据。
2
+
3
+ memgov 不存储记忆本身——它在你已有的记忆存储(mem0/Zep/向量库)旁边,
4
+ 按存储原生的 mem_id 镜像一份"治理信号",用来算衰减、决定注入、出健康度。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+
11
+
12
+ class MemoryState(str, Enum):
13
+ ACTIVE = "active" # 活跃,正常参与召回注入
14
+ FADING = "fading" # 在衰退,注入需更高置信度
15
+ STALE = "stale" # 陈旧,默认不注入(除非显式)
16
+ PINNED = "pinned" # 钉住,永不衰减/永不 stale
17
+
18
+
19
+ @dataclass
20
+ class MemoryMeta:
21
+ """一条记忆的治理元数据(对应 spec 的 sidecar schema)。"""
22
+ mem_id: str # 存储原生 id
23
+ agent_id: str = "default"
24
+ created_utc: float = 0.0
25
+ last_access_utc: float = 0.0
26
+ access_count: int = 0 # 被召回/读取次数
27
+ reinforce_count: int = 0 # 被显式强化(用户重申/确认)次数
28
+ decay_score: float = 1.0 # 0..1,越高越该保留
29
+ state: MemoryState = MemoryState.ACTIVE
30
+ source: str = "user_msg" # user_msg | tool_result | summary
31
+ last_eval_utc: float = 0.0
32
+
33
+
34
+ @dataclass
35
+ class Candidate:
36
+ """一次召回返回的候选记忆:存储给的 id + 相似度/置信度。"""
37
+ mem_id: str
38
+ confidence: float # 召回置信度(如向量相似度 0..1)
39
+ text: str = ""
40
+
41
+
42
+ @dataclass
43
+ class HealthReport:
44
+ """记忆健康度快照。"""
45
+ agent_id: str
46
+ total: int = 0
47
+ active: int = 0
48
+ fading: int = 0
49
+ stale: int = 0
50
+ pinned: int = 0
51
+ stale_ratio: float = 0.0 # stale / total,越高越该清理
52
+ avg_decay_score: float = 0.0
@@ -0,0 +1,88 @@
1
+ """SQLite 持久化:记忆治理元数据的 sidecar 存储。本地优先,零外部依赖。"""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ from pathlib import Path
6
+
7
+ from .models import MemoryMeta, MemoryState
8
+
9
+ _SCHEMA = """
10
+ CREATE TABLE IF NOT EXISTS memory_meta (
11
+ mem_id TEXT NOT NULL,
12
+ agent_id TEXT NOT NULL DEFAULT 'default',
13
+ created_utc REAL DEFAULT 0,
14
+ last_access_utc REAL DEFAULT 0,
15
+ access_count INTEGER DEFAULT 0,
16
+ reinforce_count INTEGER DEFAULT 0,
17
+ decay_score REAL DEFAULT 1.0,
18
+ state TEXT DEFAULT 'active',
19
+ source TEXT DEFAULT 'user_msg',
20
+ last_eval_utc REAL DEFAULT 0,
21
+ PRIMARY KEY (agent_id, mem_id)
22
+ );
23
+ CREATE INDEX IF NOT EXISTS idx_meta_agent_state ON memory_meta(agent_id, state);
24
+ """
25
+
26
+
27
+ class MetaStore:
28
+ """治理元数据的 SQLite 存储。"""
29
+
30
+ def __init__(self, db_path: str | Path = ".memgov/memgov.db"):
31
+ self.db_path = Path(db_path)
32
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
33
+ self._init()
34
+
35
+ def _conn(self) -> sqlite3.Connection:
36
+ c = sqlite3.connect(str(self.db_path))
37
+ c.row_factory = sqlite3.Row
38
+ return c
39
+
40
+ def _init(self) -> None:
41
+ with self._conn() as c:
42
+ c.executescript(_SCHEMA)
43
+
44
+ def upsert(self, m: MemoryMeta) -> None:
45
+ with self._conn() as c:
46
+ c.execute(
47
+ """INSERT INTO memory_meta
48
+ (mem_id, agent_id, created_utc, last_access_utc, access_count,
49
+ reinforce_count, decay_score, state, source, last_eval_utc)
50
+ VALUES (?,?,?,?,?,?,?,?,?,?)
51
+ ON CONFLICT(agent_id, mem_id) DO UPDATE SET
52
+ created_utc=excluded.created_utc,
53
+ last_access_utc=excluded.last_access_utc,
54
+ access_count=excluded.access_count,
55
+ reinforce_count=excluded.reinforce_count,
56
+ decay_score=excluded.decay_score,
57
+ state=excluded.state,
58
+ source=excluded.source,
59
+ last_eval_utc=excluded.last_eval_utc""",
60
+ (m.mem_id, m.agent_id, m.created_utc, m.last_access_utc,
61
+ m.access_count, m.reinforce_count, m.decay_score,
62
+ m.state.value, m.source, m.last_eval_utc),
63
+ )
64
+
65
+ def get(self, mem_id: str, agent_id: str = "default") -> MemoryMeta | None:
66
+ with self._conn() as c:
67
+ r = c.execute(
68
+ "SELECT * FROM memory_meta WHERE agent_id=? AND mem_id=?",
69
+ (agent_id, mem_id),
70
+ ).fetchone()
71
+ return _row_to_meta(r) if r else None
72
+
73
+ def list_for_agent(self, agent_id: str = "default") -> list[MemoryMeta]:
74
+ with self._conn() as c:
75
+ rows = c.execute(
76
+ "SELECT * FROM memory_meta WHERE agent_id=?", (agent_id,)
77
+ ).fetchall()
78
+ return [_row_to_meta(r) for r in rows]
79
+
80
+
81
+ def _row_to_meta(r: sqlite3.Row) -> MemoryMeta:
82
+ return MemoryMeta(
83
+ mem_id=r["mem_id"], agent_id=r["agent_id"],
84
+ created_utc=r["created_utc"], last_access_utc=r["last_access_utc"],
85
+ access_count=r["access_count"], reinforce_count=r["reinforce_count"],
86
+ decay_score=r["decay_score"], state=MemoryState(r["state"]),
87
+ source=r["source"], last_eval_utc=r["last_eval_utc"],
88
+ )
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: memgov
3
+ Version: 0.1.0
4
+ Summary: Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it.
5
+ Author: wangkevin2100-cell
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/wangkevin2100-cell/memgov
8
+ Project-URL: Repository, https://github.com/wangkevin2100-cell/memgov
9
+ Keywords: llm,ai-agents,memory,rag,decay,context,governance,mem0,zep
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: hypothesis>=6.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # memgov
28
+
29
+ > Provider-agnostic **governance layer** for AI agent memory. It doesn't store
30
+ > your memory — it governs the memory you already have (mem0 / Zep / a raw
31
+ > vector DB): decays it by recency × reuse, gates recall injection behind a
32
+ > confidence threshold so stale junk never reaches the prompt, and surfaces
33
+ > memory-health metrics. Local-first (SQLite), zero required deps, BYO store.
34
+
35
+ ## The problem
36
+
37
+ The 2025–2026 memory stores (mem0, Zep, LangMem, Letta) solved *write* and
38
+ *retrieve*. But after a quarter of accumulation, teams hit the second-order
39
+ problem: **memory useful in week 1 is actively harmful in week 12, and nothing
40
+ ages it out.** Stores answer "what do I remember?" — none answer "what should I
41
+ have *forgotten*, and what's safe to inject right now?" That's governance, and
42
+ it's missing.
43
+
44
+ memgov fills that lane:
45
+
46
+ - **Reuse-aware decay** — a tunable `recency × reuse` score, not a crude global TTL.
47
+ - **Confidence-gated recall** — always search, but only *inject* when retrieval
48
+ confidence clears a threshold and the memory isn't stale.
49
+ - **Memory health** — `active / fading / stale` ratios so silent degradation
50
+ becomes observable *before* answers get bad.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install memgov # zero required deps
56
+ ```
57
+
58
+ Python 3.11+.
59
+
60
+ ## Quick start
61
+
62
+ ```python
63
+ from memgov import Governor, Candidate
64
+
65
+ gov = Governor() # local SQLite sidecar
66
+
67
+ # register memories your store already holds, keyed by the store's native id
68
+ gov.track("mem-1", agent_id="bot")
69
+ gov.reinforce("mem-1", agent_id="bot") # user restated it -> reinforce
70
+
71
+ gov.recompute(agent_id="bot") # recompute decay scores + states
72
+
73
+ # on recall: your store returns candidates; memgov gates what actually injects
74
+ candidates = [Candidate("mem-1", confidence=0.82),
75
+ Candidate("mem-9", confidence=0.31)] # low-confidence
76
+ inject = gov.gate(candidates, agent_id="bot") # only the worthy ones
77
+ print([c.mem_id for c in inject]) # -> ['mem-1']
78
+
79
+ print(gov.health(agent_id="bot")) # active/fading/stale snapshot
80
+ ```
81
+
82
+ ## How it works
83
+
84
+ - **Sidecar metadata** keyed by your store's native `mem_id` (memgov never holds
85
+ the memory text — it tracks the signals decay needs).
86
+ - **`track / touch / reinforce`** record usage signals; `reinforce` (user
87
+ restated/confirmed) is the strongest keep signal.
88
+ - **`recompute`** rescans an agent's memories and assigns `decay_score` (0..1)
89
+ and a `state`: `active | fading | stale | pinned`.
90
+ - **`gate`** is the core: it receives recall candidates and passes only those
91
+ with confidence ≥ threshold **and** not `stale`. `pin` protects a memory from
92
+ ever decaying.
93
+
94
+ ## See it (no deps, no API key)
95
+
96
+ ```bash
97
+ python examples/demo_governance.py
98
+ ```
99
+
100
+ ## Status
101
+
102
+ Early MVP (v0.1). Implemented: decay scorer, gate middleware, health metrics,
103
+ SQLite sidecar, CLI. Planned: store adapters (mem0/Zep), MCP-server option,
104
+ embedding-confidence helpers, health dashboard.
105
+
106
+ ## License
107
+
108
+ MIT.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ memgov/__init__.py
5
+ memgov/__main__.py
6
+ memgov/cli.py
7
+ memgov/decay.py
8
+ memgov/governor.py
9
+ memgov/models.py
10
+ memgov/store.py
11
+ memgov.egg-info/PKG-INFO
12
+ memgov.egg-info/SOURCES.txt
13
+ memgov.egg-info/dependency_links.txt
14
+ memgov.egg-info/entry_points.txt
15
+ memgov.egg-info/requires.txt
16
+ memgov.egg-info/top_level.txt
17
+ tests/test_decay.py
18
+ tests/test_governor.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memgov = memgov.cli:main
@@ -0,0 +1,4 @@
1
+
2
+ [dev]
3
+ pytest>=8.0
4
+ hypothesis>=6.0
@@ -0,0 +1 @@
1
+ memgov
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "memgov"
3
+ version = "0.1.0"
4
+ description = "Provider-agnostic governance layer for AI agent memory: reuse-aware decay, confidence-gated recall injection, and memory-health metrics. Governs the memory you already have (mem0/Zep/vector DB) instead of storing it."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "wangkevin2100-cell" }]
9
+ keywords = ["llm", "ai-agents", "memory", "rag", "decay", "context", "governance", "mem0", "zep"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=8.0", "hypothesis>=6.0"]
25
+
26
+ [project.scripts]
27
+ memgov = "memgov.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/wangkevin2100-cell/memgov"
31
+ Repository = "https://github.com/wangkevin2100-cell/memgov"
32
+
33
+ [build-system]
34
+ requires = ["setuptools>=68"]
35
+ build-backend = "setuptools.build_meta"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["."]
39
+ include = ["memgov*"]
memgov-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,56 @@
1
+ """decay 评分的单元测试 + 正确性属性。"""
2
+ from __future__ import annotations
3
+
4
+ from hypothesis import given, settings
5
+ from hypothesis import strategies as st
6
+
7
+ from memgov.decay import decay_score, recency_factor, reuse_bonus
8
+
9
+ _DAY = 86400.0
10
+
11
+
12
+ # Feature: memgov, Property: decay_score 永远落在 [0, 1]
13
+ @settings(max_examples=200)
14
+ @given(
15
+ age=st.floats(min_value=0, max_value=10_000 * _DAY, allow_nan=False),
16
+ acc=st.integers(min_value=0, max_value=10_000),
17
+ rein=st.integers(min_value=0, max_value=10_000),
18
+ )
19
+ def test_score_in_unit_interval(age, acc, rein):
20
+ s = decay_score(age, acc, rein)
21
+ assert 0.0 <= s <= 1.0
22
+
23
+
24
+ # Feature: memgov, Property: 其它不变时,越久没访问分越低(recency 单调)
25
+ @settings(max_examples=100)
26
+ @given(
27
+ age1=st.floats(min_value=0, max_value=100 * _DAY),
28
+ extra=st.floats(min_value=1.0, max_value=100 * _DAY),
29
+ )
30
+ def test_recency_monotonic(age1, extra):
31
+ assert recency_factor(age1) >= recency_factor(age1 + extra) - 1e-12
32
+
33
+
34
+ # Feature: memgov, Property: 强化越多,保留分不降(reuse 单调不减)
35
+ @settings(max_examples=100)
36
+ @given(
37
+ age=st.floats(min_value=0, max_value=30 * _DAY),
38
+ rein=st.integers(min_value=0, max_value=50),
39
+ )
40
+ def test_reinforce_does_not_decrease_score(age, rein):
41
+ s_low = decay_score(age, 0, rein)
42
+ s_high = decay_score(age, 0, rein + 1)
43
+ assert s_high >= s_low - 1e-12
44
+
45
+
46
+ def test_fresh_memory_scores_high():
47
+ assert decay_score(0) == 1.0
48
+
49
+
50
+ def test_old_unused_memory_decays():
51
+ # 一年没碰、没复用 -> 应该很低
52
+ assert decay_score(365 * _DAY) < 0.05
53
+
54
+
55
+ def test_reuse_bonus_capped():
56
+ assert reuse_bonus(10_000, 10_000) <= 0.5
@@ -0,0 +1,74 @@
1
+ """Governor 中间件测试:衰减治理、召回门控、健康度。"""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ from memgov import Candidate, Governor, MemoryState
7
+ from memgov.store import MetaStore
8
+
9
+ _DAY = 86400.0
10
+
11
+
12
+ @pytest.fixture
13
+ def gov(tmp_path):
14
+ return Governor(store=MetaStore(tmp_path / "t.db"))
15
+
16
+
17
+ def test_track_and_get(gov):
18
+ gov.track("m1", agent_id="bot", now=1000.0)
19
+ m = gov.store.get("m1", "bot")
20
+ assert m is not None and m.state == MemoryState.ACTIVE
21
+
22
+
23
+ def test_unused_memory_goes_stale_after_recompute(gov):
24
+ gov.track("old", agent_id="bot", now=0.0)
25
+ # 一年后重算,没访问没强化 -> stale
26
+ gov.recompute(agent_id="bot", now=365 * _DAY)
27
+ assert gov.store.get("old", "bot").state == MemoryState.STALE
28
+
29
+
30
+ def test_reinforced_memory_stays_active(gov):
31
+ gov.track("keep", agent_id="bot", now=0.0)
32
+ for _ in range(5):
33
+ gov.reinforce("keep", agent_id="bot", now=300 * _DAY)
34
+ gov.recompute(agent_id="bot", now=300 * _DAY)
35
+ assert gov.store.get("keep", "bot").state != MemoryState.STALE
36
+
37
+
38
+ def test_pinned_never_decays(gov):
39
+ gov.track("pin", agent_id="bot", now=0.0)
40
+ gov.pin("pin", agent_id="bot")
41
+ gov.recompute(agent_id="bot", now=10_000 * _DAY)
42
+ assert gov.store.get("pin", "bot").state == MemoryState.PINNED
43
+
44
+
45
+ # 核心价值:召回门控
46
+ def test_gate_blocks_low_confidence(gov):
47
+ gov.track("m1", agent_id="bot", now=1000.0)
48
+ kept = gov.gate([Candidate("m1", confidence=0.3)], agent_id="bot",
49
+ min_confidence=0.6)
50
+ assert kept == [] # 置信度不够,不注入
51
+
52
+
53
+ def test_gate_blocks_stale(gov):
54
+ gov.track("s1", agent_id="bot", now=0.0)
55
+ gov.recompute(agent_id="bot", now=365 * _DAY) # 变 stale
56
+ kept = gov.gate([Candidate("s1", confidence=0.99)], agent_id="bot")
57
+ assert kept == [] # 置信度高但已陈旧,仍不注入
58
+
59
+
60
+ def test_gate_passes_good_candidate(gov):
61
+ gov.track("good", agent_id="bot", now=1000.0)
62
+ gov.recompute(agent_id="bot", now=1000.0)
63
+ kept = gov.gate([Candidate("good", confidence=0.9)], agent_id="bot")
64
+ assert [c.mem_id for c in kept] == ["good"]
65
+
66
+
67
+ def test_health_counts(gov):
68
+ gov.track("a", agent_id="bot", now=0.0)
69
+ gov.track("b", agent_id="bot", now=0.0)
70
+ gov.reinforce("a", agent_id="bot", now=300 * _DAY)
71
+ gov.recompute(agent_id="bot", now=300 * _DAY)
72
+ rep = gov.health(agent_id="bot")
73
+ assert rep.total == 2
74
+ assert rep.active + rep.fading + rep.stale + rep.pinned == 2