liquid-loop 0.6.1__tar.gz → 0.6.4__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.
Files changed (23) hide show
  1. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/PKG-INFO +3 -3
  2. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/README.md +2 -2
  3. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/cli.py +1 -1
  4. liquid_loop-0.6.4/liquid_loop/cognitive_budget.py +105 -0
  5. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/entropy.py +1 -1
  6. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/workspace.py +82 -9
  7. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/PKG-INFO +3 -3
  8. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/SOURCES.txt +3 -0
  9. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/pyproject.toml +1 -1
  10. liquid_loop-0.6.4/tests/test_peek_seal.py +115 -0
  11. liquid_loop-0.6.4/tests/test_seal_persistence.py +63 -0
  12. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/tests/test_workspace.py +29 -0
  13. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/LICENSE +0 -0
  14. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/__init__.py +0 -0
  15. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/__main__.py +0 -0
  16. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop/storage.py +0 -0
  17. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/dependency_links.txt +0 -0
  18. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/entry_points.txt +0 -0
  19. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/requires.txt +0 -0
  20. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/liquid_loop.egg-info/top_level.txt +0 -0
  21. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/setup.cfg +0 -0
  22. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/tests/test_entropy.py +0 -0
  23. {liquid_loop-0.6.1 → liquid_loop-0.6.4}/tests/test_self_evolve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liquid-loop
3
- Version: 0.6.1
3
+ Version: 0.6.4
4
4
  Summary: Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation
5
5
  Author: fishbook0001
6
6
  Maintainer: fishbook0001
@@ -72,7 +72,7 @@ Dynamic: license-file
72
72
  | **Anchor** 锚点 | 晶种 | 认知关注点,有稳定性值 s ∈ [0,1] |
73
73
  | **Evidence** 证据 | 附着粒子 | 锚点下的具体观察,权重指数衰减 w×0.95ᵗ |
74
74
  | **Memory** 结晶 | 结晶体 | 2+ 条一致 Evidence 自动凝聚,有置信度 c |
75
- | **Entropy** 熵值 | 流体无序度 | 四维加权:drift×0.25 + conflict×0.25 + fragmentation×0.25 + gap×0.25 |
75
+ | **Entropy** 熵值 | 流体无序度 | 八维加权(锚点漂移 / 冲突密度 / 碎片 / 活跃间隔 / 价值衰减 / 锚定强度 / CPE 三维) |
76
76
 
77
77
  **状态判定:**
78
78
  ```
@@ -123,7 +123,7 @@ save(state, Path("."))
123
123
  # 初始化工作区(创建 .liquid/state.json)
124
124
  liquid-loop init
125
125
 
126
- # 添加锚点(支持自动四维分类)
126
+ # 添加锚点(支持自动三维分类:密度 / 认知阶段 / 流动性)
127
127
  liquid-loop anchor_add "项目目标" "完成液环论文与开源"
128
128
 
129
129
  # 注入证据
@@ -33,7 +33,7 @@
33
33
  | **Anchor** 锚点 | 晶种 | 认知关注点,有稳定性值 s ∈ [0,1] |
34
34
  | **Evidence** 证据 | 附着粒子 | 锚点下的具体观察,权重指数衰减 w×0.95ᵗ |
35
35
  | **Memory** 结晶 | 结晶体 | 2+ 条一致 Evidence 自动凝聚,有置信度 c |
36
- | **Entropy** 熵值 | 流体无序度 | 四维加权:drift×0.25 + conflict×0.25 + fragmentation×0.25 + gap×0.25 |
36
+ | **Entropy** 熵值 | 流体无序度 | 八维加权(锚点漂移 / 冲突密度 / 碎片 / 活跃间隔 / 价值衰减 / 锚定强度 / CPE 三维) |
37
37
 
38
38
  **状态判定:**
39
39
  ```
@@ -84,7 +84,7 @@ save(state, Path("."))
84
84
  # 初始化工作区(创建 .liquid/state.json)
85
85
  liquid-loop init
86
86
 
87
- # 添加锚点(支持自动四维分类)
87
+ # 添加锚点(支持自动三维分类:密度 / 认知阶段 / 流动性)
88
88
  liquid-loop anchor_add "项目目标" "完成液环论文与开源"
89
89
 
90
90
  # 注入证据
@@ -104,7 +104,7 @@ def status():
104
104
 
105
105
  @main.command()
106
106
  def anchor_list():
107
- """列出所有锚点(含四维分类 + 价值衰减)"""
107
+ """列出所有锚点(含三维分类 + 价值衰减)"""
108
108
  s = _load()
109
109
  if not s.anchors:
110
110
  click.echo("暂无锚点。使用 liquid anchor add <名称> <描述> 添加。")
@@ -0,0 +1,105 @@
1
+ """认知预算稳态器(落地 PEEK arXiv:2604.09932 固定预算驱逐)
2
+
3
+ 把 PEEK 三模块蒸馏并适配液环:
4
+ Distiller evidence.weight + anchor.value_score + recency → 蒸馏价值
5
+ Cartographer 价值 × (0.5+0.5*访问热度) × 流动性 → 重要性
6
+ Evictor 超额时驱逐最低重要性证据,标记 archived(冷归档,零丢失)
7
+
8
+ 设计哲学(呼应液环零向量/零丢失):
9
+ 驱逐 ≠ 删除。超预算证据标记 archived=True,从活跃检索集移出(recall 跳过),
10
+ 但数据与审计链完整保留,可随时解冻。记忆系统因此维持"稳态预算"而非无限膨胀。
11
+
12
+ 预算来源:环境变量 LIQUID_EVIDENCE_BUDGET(默认 0 = 不限制)。
13
+ """
14
+ from __future__ import annotations
15
+ import os
16
+ from dataclasses import dataclass
17
+ from datetime import datetime, timezone
18
+ from typing import Optional
19
+
20
+ LIQUIDITY_HEAT = {"hot": 1.0, "warm": 0.7, "cold": 0.4, "frozen": 0.1}
21
+
22
+
23
+ @dataclass
24
+ class _BudgetItem:
25
+ id: str
26
+ weight: float = 1.0
27
+ anchor_value: float = 1.0
28
+ access_count: int = 0
29
+ liquidity: str = "warm"
30
+ gap_hours: float = 0.0
31
+ distilled_value: float = 0.0
32
+ importance: float = 0.0
33
+
34
+
35
+ def _distill(it: _BudgetItem) -> float:
36
+ recency = 1.0 / (1.0 + it.gap_hours / 168.0)
37
+ v = 0.4 * it.weight + 0.3 * it.anchor_value + 0.3 * recency
38
+ it.distilled_value = max(0.0, min(1.0, v))
39
+ return it.distilled_value
40
+
41
+
42
+ def _rank(items: list[_BudgetItem]) -> list[_BudgetItem]:
43
+ for it in items:
44
+ if it.distilled_value == 0.0:
45
+ _distill(it)
46
+ access_heat = min(it.access_count / 10.0, 1.0)
47
+ liquidity_heat = LIQUIDITY_HEAT.get(it.liquidity, 0.4)
48
+ it.importance = it.distilled_value * (0.5 + 0.5 * access_heat) * liquidity_heat
49
+ return sorted(items, key=lambda x: x.importance, reverse=True)
50
+
51
+
52
+ def _evict(items: list[_BudgetItem], budget: int) -> list[_BudgetItem]:
53
+ if len(items) <= budget:
54
+ return []
55
+ ranked = _rank(items)
56
+ return ranked[budget:] # 尾部最低重要性
57
+
58
+
59
+ class CognitiveBudgetStabilizer:
60
+ """接 WorkspaceState,超预算时冷归档低价值证据"""
61
+
62
+ def __init__(self, state, budget: Optional[int] = None):
63
+ self.state = state
64
+ self.budget = budget if budget is not None else int(os.environ.get("LIQUID_EVIDENCE_BUDGET", "0"))
65
+
66
+ @staticmethod
67
+ def _gap_hours(anchor) -> float:
68
+ if not anchor or not anchor.last_accessed:
69
+ return 9999.0
70
+ try:
71
+ last = datetime.fromisoformat(anchor.last_accessed)
72
+ return (datetime.now(timezone.utc) - last).total_seconds() / 3600
73
+ except ValueError:
74
+ return 9999.0
75
+
76
+ def stabilize(self) -> dict:
77
+ if self.budget <= 0:
78
+ return {"evicted": 0, "budget": 0, "total": len(self.state.evidences),
79
+ "note": "预算未设置(LIQUID_EVIDENCE_BUDGET=0),跳过"}
80
+ evs = self.state.evidences
81
+ if len(evs) <= self.budget:
82
+ return {"evicted": 0, "budget": self.budget, "total": len(evs)}
83
+ anchor_map = {a.id: a for a in self.state.anchors}
84
+ items: list[_BudgetItem] = []
85
+ for e in evs:
86
+ a = anchor_map.get(e.anchor_id)
87
+ items.append(_BudgetItem(
88
+ id=e.id,
89
+ weight=e.weight,
90
+ anchor_value=a.value_score if a else 0.5,
91
+ access_count=a.access_count if a else 0,
92
+ liquidity=a.liquidity if a else "warm",
93
+ gap_hours=self._gap_hours(a),
94
+ ))
95
+ evicted = _evict(items, self.budget)
96
+ evicted_ids = {it.id for it in evicted}
97
+ for e in evs:
98
+ if e.id in evicted_ids:
99
+ e.archived = True # 冷归档,零丢失
100
+ return {
101
+ "evicted": len(evicted_ids),
102
+ "evicted_ids": list(evicted_ids),
103
+ "budget": self.budget,
104
+ "total": len(evs),
105
+ }
@@ -52,7 +52,7 @@ def activity_gap(state: WorkspaceState) -> float:
52
52
  return min(gap / timedelta(days=7), 1.0)
53
53
 
54
54
 
55
- # 【借鉴KFG】四维熵值分量
55
+ # 熵值分量(综合见 calculate,八维加权含 CPE 三维)
56
56
 
57
57
  def value_decay_entropy(state: WorkspaceState) -> float:
58
58
  """锚点价值衰减熵。0=全部高价值, 1=全部低价值 — 纯函数,不改 state"""
@@ -9,6 +9,8 @@ import os
9
9
  import re
10
10
  from collections import Counter
11
11
 
12
+ from .cognitive_budget import CognitiveBudgetStabilizer
13
+
12
14
 
13
15
  def now() -> str:
14
16
  return datetime.now(timezone.utc).isoformat()
@@ -63,7 +65,7 @@ class AuditChain:
63
65
  return expected == stored_hash
64
66
 
65
67
 
66
- # --- 四维分类(借鉴 KFG 4D Classifier)---
68
+ # --- 三维锚点分类(密度 / 认知阶段 / 流动性)+ 一维证据质量 ---
67
69
  DensityLevel = str # "high" | "medium" | "low"
68
70
  CognitiveStage = str # "raw" | "wip" | "crystallized" | "tooling"
69
71
  LiquidityLevel = str # "hot" | "warm" | "cold" | "frozen"
@@ -84,6 +86,7 @@ class Anchor:
84
86
  last_accessed: str = ""
85
87
  value_score: float = 1.0
86
88
  anchor_strength: float = 1.0
89
+ seal_adjust: float = 0.0 # SEAL 自评层增量;_recalc 合成进 stability,不被覆盖(解 v0.6.3 假落地)
87
90
 
88
91
  def decay_value(self, factor: float = 0.95, evidence_count: int = 0) -> float:
89
92
  if not self.created_at:
@@ -149,6 +152,7 @@ class Evidence:
149
152
  timestamp: str = field(default_factory=now)
150
153
  weight: float = 1.0
151
154
  quality: str = "normal"
155
+ archived: bool = False # 预算超额冷归档标记(PEEK 落地,零丢失)
152
156
 
153
157
 
154
158
  @dataclass
@@ -266,11 +270,18 @@ class WorkspaceState:
266
270
  return e
267
271
 
268
272
  def _on_evidence_added(self, anchor_id: str):
269
- """证据添加后的统一后处理:衰减→成核→稳定性刷新→冲突检测"""
273
+ """证据添加后的统一后处理:衰减→成核→稳定性刷新→冲突检测→预算稳态"""
270
274
  self._decay_anchor(anchor_id)
271
275
  self._nucleate(anchor_id)
272
276
  self._recalc_anchor(anchor_id)
273
277
  self._detect_conflicts(anchor_id)
278
+ self._stabilize_budget()
279
+
280
+ def _stabilize_budget(self):
281
+ """认知预算稳态(PEEK 落地):超预算时冷归档最低价值证据。默认无预算不动作。"""
282
+ result = CognitiveBudgetStabilizer(self).stabilize()
283
+ if result.get("evicted", 0) > 0:
284
+ self.updated_at = now()
274
285
 
275
286
  def _decay_anchor(self, anchor_id: str):
276
287
  for e in self.evidences:
@@ -278,16 +289,25 @@ class WorkspaceState:
278
289
  e.weight = max(e.weight * 0.95, 0.1)
279
290
 
280
291
  def _nucleate(self, anchor_id: str):
281
- """成核:同锚点下 >= 2 条一致证据形成记忆结晶"""
292
+ """成核:同锚点下 >= 2 条一致证据形成记忆结晶。
293
+
294
+ 复合键判定:查重限定在当前锚点内((anchor_id, content)),避免跨锚点
295
+ 同 content 证据被错误吞掉或并入同一结晶(P0 跨锚点误结晶修复, v0.6.2)。
296
+ """
282
297
  group = [e for e in self.evidences if e.anchor_id == anchor_id]
283
298
  if len(group) < 2:
284
299
  return
285
- from collections import Counter
300
+ # 本锚点已结晶的 content 集合(复合键判定,杜绝跨锚点污染)
301
+ crystallized_contents: set[str] = set()
302
+ for m in self.memories:
303
+ for eid in m.evidence_ids:
304
+ ev = next((e for e in self.evidences if e.id == eid), None)
305
+ if ev is not None and ev.anchor_id == anchor_id:
306
+ crystallized_contents.add(m.content)
307
+ break
286
308
  content_counts = Counter(e.content for e in group)
287
309
  for content, count in content_counts.items():
288
- if count >= 2:
289
- if any(m.content == content for m in self.memories):
290
- continue
310
+ if count >= 2 and content not in crystallized_contents:
291
311
  evidence_ids = [e.id for e in group if e.content == content]
292
312
  confidence = min(count / len(group), 1.0)
293
313
  self.memories.append(Memory(
@@ -351,7 +371,10 @@ class WorkspaceState:
351
371
  avg_weight = sum(e.weight for e in group) / len(group)
352
372
  for a in self.anchors:
353
373
  if a.id == anchor_id:
354
- a.stability = avg_weight
374
+ # base = 证据权重均值;stability = base + SEAL 自评层(seal_adjust 不被覆盖)
375
+ base = avg_weight
376
+ a.base_stability = base
377
+ a.stability = round(min(1.0, max(0.1, base + a.seal_adjust)), 3)
355
378
  evidence_count = len(group)
356
379
  a.decay_value(evidence_count=evidence_count)
357
380
  a.recalc_strength(evidence_count)
@@ -842,6 +865,51 @@ class SelfRefineEngine:
842
865
  repairs.append(f"REPAIR: 为锚点[{target_anchor.name}]新增evidence: {gold[:40]}")
843
866
  return repairs
844
867
 
868
+ # ── SEAL 落地(arXiv:2605.24426 失败诊断双优化)──
869
+ def diagnose(self, failure: Dict[str, Any]) -> Dict[str, Any]:
870
+ """SEAL 诊断:失败探测归因到锚点,判定失败类型。
871
+
872
+ fail_type:
873
+ retrieval 召回失败(记忆存在但未检索到)→ 策略侧 boost_stability
874
+ reason 推理失败(检索到但判断不中) → 策略侧 downweight_noise
875
+ """
876
+ reason = failure.get("reason", "")
877
+ if "检索" in reason or "无相关" in reason:
878
+ fail_type = "retrieval"
879
+ else:
880
+ fail_type = "reason"
881
+ source = failure.get("source", "")
882
+ target = None
883
+ if source:
884
+ target = next((a for a in self.state.anchors if a.id == source), None)
885
+ if not target:
886
+ ev = next((e for e in self.state.evidences if e.id == source), None)
887
+ if ev:
888
+ target = next((a for a in self.state.anchors if a.id == ev.anchor_id), None)
889
+ if fail_type == "retrieval":
890
+ tune = "boost_stability"
891
+ root = "召回失败:记忆存在但未检索到(权重/描述召回不足)"
892
+ else:
893
+ tune = "downweight_noise"
894
+ root = "推理失败:检索到但关键词判断不中(噪声/描述歧义)"
895
+ return {"fail_type": fail_type, "target_anchor": target.id if target else None,
896
+ "root_cause": root, "tune": tune}
897
+
898
+ def apply_strategy(self, diag: Dict[str, Any]) -> List[Dict[str, Any]]:
899
+ """SEAL 双优化 - 策略侧:修复确认→boost_stability;噪声→downweight。"""
900
+ if not diag.get("target_anchor"):
901
+ return []
902
+ a = next((x for x in self.state.anchors if x.id == diag["target_anchor"]), None)
903
+ if not a:
904
+ return []
905
+ if diag["tune"] == "boost_stability":
906
+ a.seal_adjust = min(0.5, round(a.seal_adjust + 0.1, 3))
907
+ else:
908
+ a.seal_adjust = max(-0.5, round(a.seal_adjust - 0.1, 3))
909
+ base = getattr(a, "base_stability", a.stability)
910
+ projected = round(min(1.0, max(0.1, base + a.seal_adjust)), 3)
911
+ return [{"op": diag["tune"], "anchor": a.name, "seal_adjust": a.seal_adjust, "projected_stability": projected}]
912
+
845
913
  def run(self, evidence_ids: List[str] = None) -> Dict[str, Any]:
846
914
  """执行完整后向自进化周期。"""
847
915
  probes = self.generate_probes(evidence_ids)
@@ -853,10 +921,15 @@ class SelfRefineEngine:
853
921
  failures = [r for p, r in zip(probes, results)
854
922
  if not r.get("passed") and p.get("type") != "overview"]
855
923
  repairs = self.repair(failures)
924
+ # SEAL 双优化 - 策略侧:诊断失败 → 调锚点 stability
925
+ diagnoses = [self.diagnose(f) for f in failures]
926
+ strategy_actions = []
927
+ for d in diagnoses:
928
+ strategy_actions.extend(self.apply_strategy(d))
856
929
  self.state.self_refine_probes = probes
857
930
  self.state.self_refine_results = results
858
931
  self.state.self_refine_repair_count += len(repairs)
859
- return {"total": len(probes), "passed": passed, "failed": len(failures), "pass_rate": round(passed / len(probes), 2), "repairs": repairs, "failed_details": [{"q": r.get("question", "")[:60], "reason": r.get("reason", "")} for r in failures[:5]]}
932
+ return {"total": len(probes), "passed": passed, "failed": len(failures), "pass_rate": round(passed / len(probes), 2), "repairs": repairs, "diagnoses": diagnoses, "strategy_actions": strategy_actions, "failed_details": [{"q": r.get("question", "")[:60], "reason": r.get("reason", "")} for r in failures[:5]]}
860
933
 
861
934
 
862
935
  # ==============================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liquid-loop
3
- Version: 0.6.1
3
+ Version: 0.6.4
4
4
  Summary: Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation
5
5
  Author: fishbook0001
6
6
  Maintainer: fishbook0001
@@ -72,7 +72,7 @@ Dynamic: license-file
72
72
  | **Anchor** 锚点 | 晶种 | 认知关注点,有稳定性值 s ∈ [0,1] |
73
73
  | **Evidence** 证据 | 附着粒子 | 锚点下的具体观察,权重指数衰减 w×0.95ᵗ |
74
74
  | **Memory** 结晶 | 结晶体 | 2+ 条一致 Evidence 自动凝聚,有置信度 c |
75
- | **Entropy** 熵值 | 流体无序度 | 四维加权:drift×0.25 + conflict×0.25 + fragmentation×0.25 + gap×0.25 |
75
+ | **Entropy** 熵值 | 流体无序度 | 八维加权(锚点漂移 / 冲突密度 / 碎片 / 活跃间隔 / 价值衰减 / 锚定强度 / CPE 三维) |
76
76
 
77
77
  **状态判定:**
78
78
  ```
@@ -123,7 +123,7 @@ save(state, Path("."))
123
123
  # 初始化工作区(创建 .liquid/state.json)
124
124
  liquid-loop init
125
125
 
126
- # 添加锚点(支持自动四维分类)
126
+ # 添加锚点(支持自动三维分类:密度 / 认知阶段 / 流动性)
127
127
  liquid-loop anchor_add "项目目标" "完成液环论文与开源"
128
128
 
129
129
  # 注入证据
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  liquid_loop/__init__.py
5
5
  liquid_loop/__main__.py
6
6
  liquid_loop/cli.py
7
+ liquid_loop/cognitive_budget.py
7
8
  liquid_loop/entropy.py
8
9
  liquid_loop/storage.py
9
10
  liquid_loop/workspace.py
@@ -14,5 +15,7 @@ liquid_loop.egg-info/entry_points.txt
14
15
  liquid_loop.egg-info/requires.txt
15
16
  liquid_loop.egg-info/top_level.txt
16
17
  tests/test_entropy.py
18
+ tests/test_peek_seal.py
19
+ tests/test_seal_persistence.py
17
20
  tests/test_self_evolve.py
18
21
  tests/test_workspace.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "liquid-loop"
7
- version = "0.6.1"
7
+ version = "0.6.4"
8
8
  description = "Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,115 @@
1
+ """PEEK 预算稳态 + SEAL 双优化 落地测试(液环仓库内)"""
2
+ import pytest
3
+ from liquid_loop.workspace import WorkspaceState, SelfRefineEngine
4
+ from liquid_loop.cognitive_budget import CognitiveBudgetStabilizer
5
+
6
+
7
+ # ───────────── PEEK 落地:认知预算稳态器 ─────────────
8
+
9
+ def test_budget_stabilizer_evicts_low_value(monkeypatch):
10
+ monkeypatch.setenv("LIQUID_EVIDENCE_BUDGET", "3")
11
+ st = WorkspaceState()
12
+ a_hot = st.add_anchor("hot")
13
+ a_hot.value_score = 0.9
14
+ a_hot.access_count = 20
15
+ a_hot.liquidity = "hot"
16
+ a_cold = st.add_anchor("cold")
17
+ a_cold.value_score = 0.2
18
+ a_cold.access_count = 0
19
+ a_cold.liquidity = "frozen"
20
+ for i in range(3):
21
+ st.add_evidence("hot", f"重要证据{i} 液环锚点")
22
+ for i in range(3):
23
+ st.add_evidence("cold", f"陈旧证据{i} 旧记忆")
24
+ # 6 条证据,预算 3 → 应冷归档 3 条最低价值(cold 锚点)
25
+ arch = [e for e in st.evidences if e.archived]
26
+ assert len(arch) == 3
27
+ assert all(e.anchor_id == a_cold.id for e in arch)
28
+
29
+
30
+ def test_budget_no_limit_when_unset(monkeypatch):
31
+ monkeypatch.delenv("LIQUID_EVIDENCE_BUDGET", raising=False)
32
+ st = WorkspaceState()
33
+ a = st.add_anchor("x")
34
+ for i in range(10):
35
+ st.add_evidence("x", f"证据{i}")
36
+ arch = [e for e in st.evidences if e.archived]
37
+ assert arch == [] # 无预算不驱逐
38
+
39
+
40
+ def test_budget_stabilizer_direct_call(monkeypatch):
41
+ monkeypatch.setenv("LIQUID_EVIDENCE_BUDGET", "2")
42
+ st = WorkspaceState()
43
+ a = st.add_anchor("x")
44
+ a.value_score = 0.5
45
+ for i in range(5):
46
+ st.add_evidence("x", f"证据{i}")
47
+ res = CognitiveBudgetStabilizer(st).stabilize()
48
+ assert res["evicted"] == 3
49
+ assert res["total"] == 5
50
+ assert res["budget"] == 2
51
+
52
+
53
+ # ───────────── SEAL 落地:失败诊断双优化 ─────────────
54
+
55
+ def test_seal_diagnose_retrieval():
56
+ st = WorkspaceState()
57
+ a = st.add_anchor("液环", description="液环记忆理论")
58
+ st.add_evidence("液环", "液环锚点结晶")
59
+ eng = SelfRefineEngine(st)
60
+ failure = {"source": a.id, "reason": "检索失败,无相关记忆", "question": "?", "gold": "?"}
61
+ diag = eng.diagnose(failure)
62
+ assert diag["fail_type"] == "retrieval"
63
+ assert diag["target_anchor"] == a.id
64
+ assert diag["tune"] == "boost_stability"
65
+
66
+
67
+ def test_seal_diagnose_reason_downweights():
68
+ st = WorkspaceState()
69
+ a = st.add_anchor("稳态")
70
+ eng = SelfRefineEngine(st)
71
+ failure = {"source": a.id, "reason": "关键词不匹配,gold=稳态"}
72
+ diag = eng.diagnose(failure)
73
+ assert diag["fail_type"] == "reason"
74
+ eng.apply_strategy(diag)
75
+ # v0.6.4:改 seal_adjust 层,不直接动 stability
76
+ assert a.seal_adjust == -0.1
77
+ st.add_evidence(a, "补充证据") # 触发 _recalc 合成
78
+ assert a.stability == round(min(1.0, max(0.1, a.base_stability + a.seal_adjust)), 3)
79
+ assert a.stability < a.base_stability # 负增量生效,自洽性降权
80
+
81
+
82
+ def test_seal_apply_strategy_boost():
83
+ st = WorkspaceState()
84
+ a = st.add_anchor("液环")
85
+ eng = SelfRefineEngine(st)
86
+ diag = {"fail_type": "retrieval", "target_anchor": a.id, "tune": "boost_stability"}
87
+ acts = eng.apply_strategy(diag)
88
+ assert acts[0]["op"] == "boost_stability"
89
+ assert a.seal_adjust == 0.1
90
+ st.add_evidence(a, "补充证据") # 触发 _recalc 合成
91
+ assert a.stability == round(min(1.0, max(0.1, a.base_stability + a.seal_adjust)), 3)
92
+ assert a.stability >= a.base_stability # 正增量生效
93
+
94
+
95
+ def test_seal_run_integration_dual_optimize(monkeypatch):
96
+ st = WorkspaceState()
97
+ a = st.add_anchor("液环", description="液环")
98
+ st.add_evidence("液环", "液环锚点")
99
+ eng = SelfRefineEngine(st)
100
+
101
+ def fake_verify(p):
102
+ if p.get("type") == "fact":
103
+ # 模拟"检索失败但记忆应存在":source 指向锚点,gold 为新内容(需补证据)
104
+ return {"passed": False, "reason": "检索失败,无相关记忆",
105
+ "question": p.get("question", ""), "gold": "全新记忆内容需补回",
106
+ "source": a.id}
107
+ return {"passed": True, "reason": "ok"}
108
+
109
+ monkeypatch.setattr(eng, "verify", fake_verify)
110
+ out = eng.run()
111
+ assert out["failed"] >= 1
112
+ assert len(out["strategy_actions"]) >= 1
113
+ assert out["strategy_actions"][0]["op"] == "boost_stability"
114
+ # 双优化:memory 侧(repairs)与 strategy 侧(strategy_actions)都应存在
115
+ assert len(out["repairs"]) >= 1
@@ -0,0 +1,63 @@
1
+ """E8/v0.6.4 — 验证 SEAL 双优化不再被 _recalc_anchor 覆盖(解 v0.6.3 假落地)。
2
+
3
+ 修复核心:stability = base(证据权重均值) + seal_adjust(SEAL 自评层增量);
4
+ apply_strategy 改 seal_adjust 而非直接动 stability,_recalc 合成时保留。
5
+
6
+ 本测试直接构造诊断(绕过 SelfRefineEngine.verify 的脆弱归因),专注验证
7
+ apply_strategy → seal_adjust → _recalc 合成的持久性。
8
+ """
9
+ from liquid_loop.workspace import WorkspaceState, SelfRefineEngine
10
+
11
+
12
+ def _boost(a, eng):
13
+ diag = {"fail_type": "retrieval", "target_anchor": a.id,
14
+ "root_cause": "召回失败", "tune": "boost_stability"}
15
+ return eng.apply_strategy(diag)
16
+
17
+
18
+ def _down(a, eng):
19
+ diag = {"fail_type": "reason", "target_anchor": a.id,
20
+ "root_cause": "推理失败", "tune": "downweight_noise"}
21
+ return eng.apply_strategy(diag)
22
+
23
+
24
+ def test_seal_boost_persists_through_recalc():
25
+ """SEAL boost 后即便再 add_evidence 触发 _recalc,stability 仍含 seal_adjust。"""
26
+ st = WorkspaceState()
27
+ eng = SelfRefineEngine(st)
28
+ a = st.add_anchor("probe_seal")
29
+ st.add_evidence(a, "初始证据")
30
+ _boost(a, eng)
31
+ assert a.seal_adjust == 0.1
32
+ st.add_evidence(a, "再来一条证据") # 触发 _recalc_anchor
33
+ assert a.seal_adjust == 0.1 # 增量持久
34
+ expected = round(min(1.0, max(0.1, a.base_stability + a.seal_adjust)), 3)
35
+ assert a.stability == expected
36
+ assert a.stability >= a.base_stability # 正增量应 >= base
37
+
38
+
39
+ def test_seal_downweight():
40
+ st = WorkspaceState()
41
+ eng = SelfRefineEngine(st)
42
+ a = st.add_anchor("noise")
43
+ st.add_evidence(a, "x")
44
+ acts = _down(a, eng)
45
+ assert a.seal_adjust == -0.1
46
+ assert acts[0]["op"] == "downweight_noise"
47
+
48
+
49
+ def test_seal_adjust_applied_in_recalc():
50
+ """开 SEAL 的锚点 stability = base + seal_adjust;对照无 SEAL 锚点 adjust=0。"""
51
+ st = WorkspaceState()
52
+ eng = SelfRefineEngine(st)
53
+ a = st.add_anchor("x")
54
+ st.add_evidence(a, "e1")
55
+ _boost(a, eng)
56
+ st.add_evidence(a, "e2") # 触发 _recalc
57
+ # 开 SEAL:stability 合成 seal_adjust(含 clamp,与期望值精确匹配)
58
+ assert a.stability == round(min(1.0, max(0.1, a.base_stability + a.seal_adjust)), 3)
59
+ # 对照:无 SEAL 的锚点 seal_adjust=0,stability=base
60
+ b = st.add_anchor("y")
61
+ st.add_evidence(b, "f1")
62
+ assert b.seal_adjust == 0.0
63
+ assert abs(b.stability - b.base_stability) < 1e-6
@@ -43,3 +43,32 @@ def test_auto_describe_anchor():
43
43
  state._nucleate(a.id)
44
44
  # 如果有高置信结晶,描述会被填充
45
45
  # 这里主要测试不报错
46
+
47
+
48
+ def test_nucleate_no_cross_anchor_pollution():
49
+ """P0 回归(v0.6.2):两锚点存在相同 content 证据时各自独立成核,不互相吞噬/污染。
50
+
51
+ 修复前 `_nucleate` 用全局 `m.content == content` 查重,不限定 anchor_id:
52
+ 锚点 A 已结晶 content=X 后,锚点 B 同 content 证据被 `continue` 跳过 →
53
+ 仅 1 条结晶且 B 的证据丢失。修复后应为 2 条结晶,且各自 evidence_ids 不跨锚点。
54
+ """
55
+ state = WorkspaceState()
56
+ a1 = state.add_anchor('锚点A')
57
+ a2 = state.add_anchor('锚点B')
58
+ state.add_evidence(a1.id, 'shared fact')
59
+ state.add_evidence(a1.id, 'shared fact')
60
+ state.add_evidence(a2.id, 'shared fact')
61
+ state.add_evidence(a2.id, 'shared fact')
62
+
63
+ # 每个锚点各成核 1 条结晶(共 2 条)
64
+ assert len(state.memories) == 2
65
+ # 每条结晶的 evidence_ids 必须只属于单一锚点(无跨锚点污染)
66
+ for m in state.memories:
67
+ evs = [e for e in state.evidences if e.id in m.evidence_ids]
68
+ assert len({e.anchor_id for e in evs}) == 1
69
+ # 锚点 A 与 B 的结晶分别存在
70
+ a1_ev_ids = {e.id for e in state.evidences if e.anchor_id == a1.id}
71
+ a2_ev_ids = {e.id for e in state.evidences if e.anchor_id == a2.id}
72
+ assert any(set(m.evidence_ids) == a1_ev_ids for m in state.memories)
73
+ assert any(set(m.evidence_ids) == a2_ev_ids for m in state.memories)
74
+
File without changes
File without changes