liquid-loop 1.8.4__tar.gz → 2.0.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.
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/PKG-INFO +11 -8
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/README.md +10 -7
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/__init__.py +9 -2
- liquid_loop-2.0.0/liquid_loop/audit.py +63 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/cli.py +12 -0
- liquid_loop-2.0.0/liquid_loop/cpe.py +247 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/entropy.py +6 -1
- liquid_loop-2.0.0/liquid_loop/guard.py +174 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/rar.py +2 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/recall_filter.py +56 -0
- liquid_loop-2.0.0/liquid_loop/self_refine.py +264 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/selfspin.py +87 -3
- liquid_loop-2.0.0/liquid_loop/session.py +113 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/storage.py +45 -0
- liquid_loop-2.0.0/liquid_loop/textutil.py +132 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/workspace.py +65 -643
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/PKG-INFO +11 -8
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/SOURCES.txt +22 -0
- liquid_loop-2.0.0/liquid_loop_exp/__init__.py +0 -0
- liquid_loop-2.0.0/liquid_loop_exp/error_loop/__init__.py +0 -0
- liquid_loop-2.0.0/liquid_loop_exp/error_loop/low_stability_recall.py +70 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/__init__.py +0 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/ab_multigran.py +95 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/hard_ab_multigran.py +156 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/multigran_core.py +178 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/sim_structured.py +40 -0
- liquid_loop-2.0.0/liquid_loop_exp/multigran/test_multigran.py +50 -0
- liquid_loop-2.0.0/liquid_loop_exp/r16_repro/__init__.py +0 -0
- liquid_loop-2.0.0/liquid_loop_exp/r16_repro/compare_rar.py +89 -0
- liquid_loop-2.0.0/liquid_loop_exp/r16_repro/r16_repro.py +204 -0
- liquid_loop-2.0.0/liquid_loop_exp/skill_refine/__init__.py +0 -0
- liquid_loop-2.0.0/liquid_loop_exp/skill_refine/skill_refine_loop.py +128 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/pyproject.toml +1 -1
- liquid_loop-2.0.0/tests/test_guard_session_recall.py +188 -0
- liquid_loop-2.0.0/tests/test_perception_gate.py +152 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/LICENSE +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/__main__.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/cognitive_budget.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/context_compress.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/liquid_reweight.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/procedural_memory.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop/self_eval.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/dependency_links.txt +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/entry_points.txt +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/requires.txt +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop.egg-info/top_level.txt +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop_exp/liquid_persist_ab.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/liquid_loop_exp/liquid_recall_ab.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/setup.cfg +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_attention_gain.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_auth_guard.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_cli_version.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_consensus_expansion.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_context_compress.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_entropy.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_layer1_persistence.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_lifecycle.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_liquid_reweight.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_nucleate_dual_track.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_peek_seal.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_procedural_memory.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_rar.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_recall_filter.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_replay_pressure.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_seal_persistence.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_self_eval.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_self_evolve.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_selfspin.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_semantica_borrow.py +0 -0
- {liquid_loop-1.8.4 → liquid_loop-2.0.0}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: liquid-loop
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation
|
|
5
5
|
Author: fishbook0001
|
|
6
6
|
Maintainer: fishbook0001
|
|
@@ -43,6 +43,8 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
> **Self-Organizing Cognitive Memory for AI Agents** — Zero LLM dependency, pure Python implementation of the Liquid Loop theory.
|
|
45
45
|
|
|
46
|
+
> **当前包版本:`2.0.0`**(2026-08-24 统一版号里程碑,详见 [CHANGELOG.md](CHANGELOG.md)。数据 schema `0.4.0` 与 workspace state `0.5.1` 为记忆层数据格式版本,独立于包发布版本)。
|
|
47
|
+
|
|
46
48
|
[](https://pypi.org/project/liquid-loop/)
|
|
47
49
|
[](https://pypi.org/project/liquid-loop/)
|
|
48
50
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -120,9 +122,9 @@ RED (entropy ≥ 0.6) — 需清理
|
|
|
120
122
|
|
|
121
123
|
---
|
|
122
124
|
|
|
123
|
-
##
|
|
125
|
+
## 核心机制:反证轨 + 时间动力学(液态循环核心)
|
|
124
126
|
|
|
125
|
-
|
|
127
|
+
液环从"静态结晶"升级为**自调节记忆动力学**:记忆不是对象,而是过程。以下机制均随 **v2.0.0** 发布(历史演进中曾标 v0.8 / v0.9)。
|
|
126
128
|
|
|
127
129
|
### 反证轨(Contradiction Track)
|
|
128
130
|
|
|
@@ -364,11 +366,12 @@ pytest -v
|
|
|
364
366
|
## 路线图
|
|
365
367
|
|
|
366
368
|
- [ ] 多 Agent 液环耦合(`liquid_loop.mesh` 已移除,见 commit ac7260e)
|
|
367
|
-
- [x] **[
|
|
368
|
-
- [x] **[
|
|
369
|
-
- [x] **[
|
|
370
|
-
- [x] **[
|
|
371
|
-
- [x] **[
|
|
369
|
+
- [x] **[已发布] 反证轨(Evidence Graph)**:Evidence 分 support / contradiction,一致增稳、冲突降稳,驱动 memory stability score(不再"一致即真")
|
|
370
|
+
- [x] **[已发布] 显式时间动力学**:`M(t+1) = M(t) + reinforcement − decay − contradiction_penalty`,让记忆成为"过程"而非"对象"(真正的液态循环)
|
|
371
|
+
- [x] **[已发布] 三实验全 PASS**:E2 错误记忆恢复 → E3 多 agent 冲突 → E1 长期漂移(见上节)
|
|
372
|
+
- [x] **[已发布] 冲突检测 O(g²)→O(d²)**:`_detect_conflicts` 按 content 去重后只对 distinct 内容求两两重叠(d≤g),overlap_cache 复用;语义更纯净(度量不同论点分歧),大规模高频写入性能提升(非正确性变更)
|
|
373
|
+
- [x] **[已发布] 液态算法正式落地**:时间动力学 / 反证轨 / 双轨成核经 E1/E2/E3 三实验背书,作为稳定机制随 **v2.0.0** 发布
|
|
374
|
+
- [x] **[v2.0.0] 统一版号里程碑**:`pyproject.toml` / `__init__.__version__` / 投喂客户端 `LIQUIDLOOP_CLIENT_VERSION` 全部对齐 `2.0.0`;数据 schema `0.4.0` 与 workspace state `0.5.1` 保持独立(记忆层格式版本,禁区不动)
|
|
372
375
|
- [ ] LoCoMo / LongMemEval 基准对比
|
|
373
376
|
- [ ] 边缘端部署优化(<50KB)
|
|
374
377
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
> **Self-Organizing Cognitive Memory for AI Agents** — Zero LLM dependency, pure Python implementation of the Liquid Loop theory.
|
|
6
6
|
|
|
7
|
+
> **当前包版本:`2.0.0`**(2026-08-24 统一版号里程碑,详见 [CHANGELOG.md](CHANGELOG.md)。数据 schema `0.4.0` 与 workspace state `0.5.1` 为记忆层数据格式版本,独立于包发布版本)。
|
|
8
|
+
|
|
7
9
|
[](https://pypi.org/project/liquid-loop/)
|
|
8
10
|
[](https://pypi.org/project/liquid-loop/)
|
|
9
11
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -81,9 +83,9 @@ RED (entropy ≥ 0.6) — 需清理
|
|
|
81
83
|
|
|
82
84
|
---
|
|
83
85
|
|
|
84
|
-
##
|
|
86
|
+
## 核心机制:反证轨 + 时间动力学(液态循环核心)
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
液环从"静态结晶"升级为**自调节记忆动力学**:记忆不是对象,而是过程。以下机制均随 **v2.0.0** 发布(历史演进中曾标 v0.8 / v0.9)。
|
|
87
89
|
|
|
88
90
|
### 反证轨(Contradiction Track)
|
|
89
91
|
|
|
@@ -325,11 +327,12 @@ pytest -v
|
|
|
325
327
|
## 路线图
|
|
326
328
|
|
|
327
329
|
- [ ] 多 Agent 液环耦合(`liquid_loop.mesh` 已移除,见 commit ac7260e)
|
|
328
|
-
- [x] **[
|
|
329
|
-
- [x] **[
|
|
330
|
-
- [x] **[
|
|
331
|
-
- [x] **[
|
|
332
|
-
- [x] **[
|
|
330
|
+
- [x] **[已发布] 反证轨(Evidence Graph)**:Evidence 分 support / contradiction,一致增稳、冲突降稳,驱动 memory stability score(不再"一致即真")
|
|
331
|
+
- [x] **[已发布] 显式时间动力学**:`M(t+1) = M(t) + reinforcement − decay − contradiction_penalty`,让记忆成为"过程"而非"对象"(真正的液态循环)
|
|
332
|
+
- [x] **[已发布] 三实验全 PASS**:E2 错误记忆恢复 → E3 多 agent 冲突 → E1 长期漂移(见上节)
|
|
333
|
+
- [x] **[已发布] 冲突检测 O(g²)→O(d²)**:`_detect_conflicts` 按 content 去重后只对 distinct 内容求两两重叠(d≤g),overlap_cache 复用;语义更纯净(度量不同论点分歧),大规模高频写入性能提升(非正确性变更)
|
|
334
|
+
- [x] **[已发布] 液态算法正式落地**:时间动力学 / 反证轨 / 双轨成核经 E1/E2/E3 三实验背书,作为稳定机制随 **v2.0.0** 发布
|
|
335
|
+
- [x] **[v2.0.0] 统一版号里程碑**:`pyproject.toml` / `__init__.__version__` / 投喂客户端 `LIQUIDLOOP_CLIENT_VERSION` 全部对齐 `2.0.0`;数据 schema `0.4.0` 与 workspace state `0.5.1` 保持独立(记忆层格式版本,禁区不动)
|
|
333
336
|
- [ ] LoCoMo / LongMemEval 基准对比
|
|
334
337
|
- [ ] 边缘端部署优化(<50KB)
|
|
335
338
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Liquid Loop — Workspace Cognitive Runtime
|
|
2
|
-
__version__ = "
|
|
1
|
+
"""Liquid Loop — Workspace Cognitive Runtime v2.0.0 (禁向量·活态液态神经网络:成核门槛/反证轨/老化回收/原理优先成核/因果演化/软取代supersede+治理查询state_at/impact/duplicates+程序性记忆层procedural_memory+PerceptionGate因果共生门控)"""
|
|
2
|
+
__version__ = "2.0.0"
|
|
3
3
|
|
|
4
4
|
from .workspace import (
|
|
5
5
|
WorkspaceState, Anchor, Evidence, Memory, Conflict,
|
|
@@ -15,3 +15,10 @@ from .recall_filter import recall_content_filter, content_aware_filter, cosine_d
|
|
|
15
15
|
from .procedural_memory import (
|
|
16
16
|
ProceduralMemory, ProceduralRegistry, procmem_recall, procmem_admit,
|
|
17
17
|
)
|
|
18
|
+
# 蒸馏落地的公共原语(零侵入,供 cli / server / agent loop 接入)
|
|
19
|
+
from .guard import should_escalate, confirm_gate, CapabilityMenu
|
|
20
|
+
from .session import SessionState, mark_abort, recover, save_session, load_session
|
|
21
|
+
from .recall_filter import adaptive_recall
|
|
22
|
+
from .rar import RARIndex, build_or_cache # 公共检索 API(原 workspace 局部 import)
|
|
23
|
+
# TTL 回收入口:evict_expired 是 WorkspaceState 实例方法(见 workspace.py:254),
|
|
24
|
+
# 已随 WorkspaceState 一并导出;cli.prune 经 state.evict_expired() 调用。
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import hashlib
|
|
5
|
+
|
|
6
|
+
from .textutil import (
|
|
7
|
+
now, uid, _derive_lifecycle_thresholds, _get_version,
|
|
8
|
+
_tokenize, _keyword_overlap, _judge_answer,
|
|
9
|
+
_dissolve_votes_path, _load_dissolve_votes, _save_dissolve_votes,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
class AuditChain:
|
|
13
|
+
"""轻量级审计链:每次变更追加 SHA256 链式哈希"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, audit_path: str):
|
|
16
|
+
self._path = audit_path
|
|
17
|
+
self._chain: list[str] = []
|
|
18
|
+
self._load()
|
|
19
|
+
|
|
20
|
+
def _load(self):
|
|
21
|
+
if os.path.exists(self._path):
|
|
22
|
+
with open(self._path, "r") as f:
|
|
23
|
+
for line in f:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if line:
|
|
26
|
+
parts = line.split("|")
|
|
27
|
+
if len(parts) >= 2:
|
|
28
|
+
self._chain.append(parts[1])
|
|
29
|
+
|
|
30
|
+
def _maybe_rotate(self) -> None:
|
|
31
|
+
"""08-18 工程化修复:按大小轮转,防 audit.log 无限增长(轮转为断链点,工程取舍)。"""
|
|
32
|
+
try:
|
|
33
|
+
max_bytes = int(os.environ.get("LL_AUDIT_MAX_BYTES", "8388608")) # 默认 8MB
|
|
34
|
+
if os.path.exists(self._path) and os.path.getsize(self._path) > max_bytes:
|
|
35
|
+
os.replace(self._path, self._path + ".1")
|
|
36
|
+
self._chain = []
|
|
37
|
+
except (OSError, ValueError):
|
|
38
|
+
pass # 轮转失败不影响写
|
|
39
|
+
|
|
40
|
+
def append(self, event_type: str, data: str) -> str:
|
|
41
|
+
self._maybe_rotate()
|
|
42
|
+
prev = self._chain[-1] if self._chain else "genesis"
|
|
43
|
+
chain_hash = hashlib.sha256(f"{event_type}:{data}:{prev}".encode()).hexdigest()[:16]
|
|
44
|
+
self._chain.append(chain_hash)
|
|
45
|
+
ts = now()
|
|
46
|
+
with open(self._path, "a") as f:
|
|
47
|
+
f.write(f"{ts}|{chain_hash}|{event_type}|{data}|{prev}\n")
|
|
48
|
+
return chain_hash
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def root(self) -> str:
|
|
52
|
+
return self._chain[-1] if self._chain else "genesis"
|
|
53
|
+
|
|
54
|
+
def verify(self, entry: str) -> bool:
|
|
55
|
+
"""验证某条完整日志行是否匹配链中记录"""
|
|
56
|
+
if "|" not in entry:
|
|
57
|
+
return False
|
|
58
|
+
parts = entry.strip().split("|")
|
|
59
|
+
if len(parts) < 5:
|
|
60
|
+
return False
|
|
61
|
+
_, stored_hash, event_type, data, prev = parts[:5]
|
|
62
|
+
expected = hashlib.sha256(f"{event_type}:{data}:{prev}".encode()).hexdigest()[:16]
|
|
63
|
+
return expected == stored_hash
|
|
@@ -526,6 +526,18 @@ def version():
|
|
|
526
526
|
click.echo(f"liquid-loop {__version__}")
|
|
527
527
|
|
|
528
528
|
|
|
529
|
+
@main.command()
|
|
530
|
+
def prune():
|
|
531
|
+
"""回收过期临时记忆(蒸馏 #202·TTL 生命周期)"""
|
|
532
|
+
s = _load()
|
|
533
|
+
evicted = s.evict_expired()
|
|
534
|
+
if evicted:
|
|
535
|
+
_save(s)
|
|
536
|
+
click.echo(f"✓ 回收 {evicted} 条过期临时记忆")
|
|
537
|
+
else:
|
|
538
|
+
click.echo("无过期临时记忆可回收。")
|
|
539
|
+
|
|
540
|
+
|
|
529
541
|
if __name__ == "__main__":
|
|
530
542
|
main()
|
|
531
543
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .textutil import (
|
|
4
|
+
now, uid, _derive_lifecycle_thresholds, _get_version,
|
|
5
|
+
_tokenize, _keyword_overlap, _judge_answer,
|
|
6
|
+
_dissolve_votes_path, _load_dissolve_votes, _save_dissolve_votes,
|
|
7
|
+
)
|
|
8
|
+
from .entropy import calculate
|
|
9
|
+
|
|
10
|
+
class CPERegularizer:
|
|
11
|
+
"""CPE 正则化引擎:在证据添加前做能力保留裁决"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, state: WorkspaceState):
|
|
14
|
+
self.state = state
|
|
15
|
+
|
|
16
|
+
def evaluate_new_evidence(self, anchor: Anchor, new_content: str) -> Dict[str, Any]:
|
|
17
|
+
"""评估新证据对既有锚点体系的能力侵蚀风险(CPE §3 Regularized Self-Evolution Objective)
|
|
18
|
+
|
|
19
|
+
返回:
|
|
20
|
+
action: "PASS" | "BLOCK" | "FLAG"
|
|
21
|
+
score: 0.0(安全) ~ 1.0(高风险)
|
|
22
|
+
reasons: 原因列表
|
|
23
|
+
"""
|
|
24
|
+
if not anchor.evidence_ids:
|
|
25
|
+
return {"action": "PASS", "score": 0.0, "reasons": ["锚点无现有证据,无覆盖风险"]}
|
|
26
|
+
|
|
27
|
+
evs = [e for e in self.state.evidences if e.anchor_id == anchor.id]
|
|
28
|
+
if not evs:
|
|
29
|
+
return {"action": "PASS", "score": 0.0, "reasons": ["无对应证据,安全"]}
|
|
30
|
+
|
|
31
|
+
# ── 1. 回顾性检查(Retrospective Protection):新证据是否与旧证据方向一致 ──
|
|
32
|
+
old_contents = [e.content for e in evs if e.content]
|
|
33
|
+
overlaps = [_keyword_overlap(new_content, old) for old in old_contents]
|
|
34
|
+
max_overlap = max(overlaps) if overlaps else 0.0
|
|
35
|
+
avg_overlap = sum(overlaps) / len(overlaps) if overlaps else 0.0
|
|
36
|
+
|
|
37
|
+
# ── 2. 漂移检查(Drift Constraint):新证据是否偏离锚点定义 ──
|
|
38
|
+
drift_score = 1.0 - _keyword_overlap(new_content, anchor.description or anchor.name)
|
|
39
|
+
|
|
40
|
+
# ── 4. 重复检测:新证据与已有证据相似度 > 0.7 → 近似重复,建议合并 ──
|
|
41
|
+
duplicate_of = None
|
|
42
|
+
for old_content in old_contents:
|
|
43
|
+
if _keyword_overlap(new_content, old_content) > 0.7:
|
|
44
|
+
duplicate_of = old_content[:60]
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
if duplicate_of:
|
|
48
|
+
return {
|
|
49
|
+
"action": "MERGE",
|
|
50
|
+
"score": 0.0,
|
|
51
|
+
"reasons": [f"与已有证据近似重复: '{duplicate_of}...'"],
|
|
52
|
+
"details": {
|
|
53
|
+
"max_overlap": round(max_overlap, 3),
|
|
54
|
+
"avg_overlap": round(avg_overlap, 3),
|
|
55
|
+
"drift_score": round(drift_score, 3),
|
|
56
|
+
"existing_evidence_count": len(evs),
|
|
57
|
+
"duplicate_of": duplicate_of,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# ── 综合风险评分 ──
|
|
62
|
+
risk_score = 0.0
|
|
63
|
+
reasons = []
|
|
64
|
+
|
|
65
|
+
# 低重叠 → 高回顾性衰退风险
|
|
66
|
+
if max_overlap < 0.1:
|
|
67
|
+
risk_score += 0.5
|
|
68
|
+
reasons.append(f"回顾性风险: 与现有证据最大重叠{max_overlap:.2f},可能侵蚀")
|
|
69
|
+
elif max_overlap < 0.3:
|
|
70
|
+
risk_score += 0.25
|
|
71
|
+
reasons.append(f"回顾性风险: 中等重叠{max_overlap:.2f},建议验证")
|
|
72
|
+
|
|
73
|
+
if drift_score > 0.7:
|
|
74
|
+
risk_score += 0.3
|
|
75
|
+
reasons.append(f"漂移风险: 与锚点方向偏离概率{drift_score:.2f}")
|
|
76
|
+
elif drift_score > 0.4:
|
|
77
|
+
risk_score += 0.1
|
|
78
|
+
reasons.append(f"漂移风险: 轻微偏离{drift_score:.2f}")
|
|
79
|
+
|
|
80
|
+
# 证据量越多 → 保护应该越强(CPE的"旧能力权重递增"思想)
|
|
81
|
+
protection_weight = min(len(evs) / 10, 1.0)
|
|
82
|
+
risk_score = risk_score * (1.0 + protection_weight) # 证据越多风险越敏感
|
|
83
|
+
risk_score = min(risk_score, 1.0)
|
|
84
|
+
|
|
85
|
+
# ── 判定 ──
|
|
86
|
+
if risk_score > 0.7:
|
|
87
|
+
action = "BLOCK"
|
|
88
|
+
elif risk_score > 0.4:
|
|
89
|
+
action = "FLAG"
|
|
90
|
+
else:
|
|
91
|
+
action = "PASS"
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"action": action,
|
|
95
|
+
"score": round(risk_score, 3),
|
|
96
|
+
"reasons": reasons,
|
|
97
|
+
"details": {
|
|
98
|
+
"max_overlap": round(max_overlap, 3),
|
|
99
|
+
"avg_overlap": round(avg_overlap, 3),
|
|
100
|
+
"drift_score": round(drift_score, 3),
|
|
101
|
+
"protection_weight": round(protection_weight, 3),
|
|
102
|
+
"existing_evidence_count": len(evs),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def scan_erosion(self) -> List[Dict[str, Any]]:
|
|
107
|
+
"""扫描全工作区,检测能力侵蚀信号(CPE §2.2 Capability Erosion)
|
|
108
|
+
|
|
109
|
+
对标 CPE 三大表现:
|
|
110
|
+
- 回顾性衰退: value_score 连续两次衰减
|
|
111
|
+
- 策略漂移: 锚点近期的 stability 波动超过阈值
|
|
112
|
+
- 泛化崩塌: 同锚点证据之间的一致性持续下降
|
|
113
|
+
"""
|
|
114
|
+
warnings = []
|
|
115
|
+
for a in self.state.anchors:
|
|
116
|
+
evs = sorted(
|
|
117
|
+
[e for e in self.state.evidences if e.anchor_id == a.id],
|
|
118
|
+
key=lambda x: x.timestamp
|
|
119
|
+
)
|
|
120
|
+
if len(evs) < 2:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# 回顾性衰退:最新 vs 次新 value_score
|
|
124
|
+
a.decay_value(evidence_count=len(evs))
|
|
125
|
+
new_score = a.value_score
|
|
126
|
+
a.decay_value(evidence_count=len(evs) - 1)
|
|
127
|
+
old_score = a.value_score
|
|
128
|
+
if old_score > new_score and (old_score - new_score) > 0.1:
|
|
129
|
+
warnings.append({
|
|
130
|
+
"type": "retrospective_decay",
|
|
131
|
+
"anchor": a.name,
|
|
132
|
+
"severity": "medium",
|
|
133
|
+
"detail": f"value_score {old_score:.2f}→{new_score:.2f} (Δ={old_score - new_score:.2f})",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# 策略漂移:stability 突变
|
|
137
|
+
old_strength = a.recalc_strength(len(evs) - 1)
|
|
138
|
+
new_strength = a.recalc_strength(len(evs))
|
|
139
|
+
drift = abs(new_strength - old_strength)
|
|
140
|
+
if drift > 0.15:
|
|
141
|
+
warnings.append({
|
|
142
|
+
"type": "behavioral_drift",
|
|
143
|
+
"anchor": a.name,
|
|
144
|
+
"severity": "medium",
|
|
145
|
+
"detail": f"stability {old_strength:.2f}→{new_strength:.2f} (Δ={drift:.2f})",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
# 泛化崩塌:证据间平均重叠度
|
|
149
|
+
content_pairs = []
|
|
150
|
+
for i in range(len(evs)):
|
|
151
|
+
for j in range(i + 1, len(evs)):
|
|
152
|
+
if evs[i].content and evs[j].content:
|
|
153
|
+
content_pairs.append(
|
|
154
|
+
_keyword_overlap(evs[i].content, evs[j].content)
|
|
155
|
+
)
|
|
156
|
+
if content_pairs:
|
|
157
|
+
avg_consistency = sum(content_pairs) / len(content_pairs)
|
|
158
|
+
if avg_consistency < 0.2 and len(evs) >= 3:
|
|
159
|
+
warnings.append({
|
|
160
|
+
"type": "generalization_erosion",
|
|
161
|
+
"anchor": a.name,
|
|
162
|
+
"severity": "high",
|
|
163
|
+
"detail": f"证据间平均重叠度{avg_consistency:.2f} (<0.2, {len(evs)}条证据)",
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
self.state.cpe_erosion_warnings = warnings
|
|
167
|
+
return warnings
|
|
168
|
+
|
|
169
|
+
def coalesce(self, anchor_name: str, threshold: float = 0.7) -> Dict[str, Any]:
|
|
170
|
+
"""模糊去重合并:同锚点下相似度 > threshold 的证据自动合并
|
|
171
|
+
|
|
172
|
+
合并策略:
|
|
173
|
+
1. 遍历同锚点所有证据,两两计算 _keyword_overlap
|
|
174
|
+
2. 重叠度 > threshold → 保留较长的那条,标记较短的那条废弃
|
|
175
|
+
3. 返回合并统计
|
|
176
|
+
|
|
177
|
+
这是 CPE 泛化防线的主动修复动作——检测到 erosion 后调用。
|
|
178
|
+
"""
|
|
179
|
+
anchor = next((a for a in self.state.anchors if a.name == anchor_name), None)
|
|
180
|
+
if not anchor:
|
|
181
|
+
return {"status": "error", "message": f"锚点 '{anchor_name}' 不存在"}
|
|
182
|
+
|
|
183
|
+
evs = [e for e in self.state.evidences if e.anchor_id == anchor.id]
|
|
184
|
+
if len(evs) < 2:
|
|
185
|
+
return {"status": "skip", "message": "证据不足2条,无需合并", "removed": 0}
|
|
186
|
+
|
|
187
|
+
# 按时间戳排序(旧→新)
|
|
188
|
+
evs_sorted = sorted(evs, key=lambda x: x.timestamp)
|
|
189
|
+
to_remove: set[str] = set()
|
|
190
|
+
merged = 0
|
|
191
|
+
|
|
192
|
+
for i in range(len(evs_sorted)):
|
|
193
|
+
if evs_sorted[i].id in to_remove:
|
|
194
|
+
continue
|
|
195
|
+
for j in range(i + 1, len(evs_sorted)):
|
|
196
|
+
if evs_sorted[j].id in to_remove:
|
|
197
|
+
continue
|
|
198
|
+
overlap = _keyword_overlap(evs_sorted[i].content, evs_sorted[j].content)
|
|
199
|
+
if overlap > threshold:
|
|
200
|
+
# 保留较长的,移除较短的
|
|
201
|
+
if len(evs_sorted[i].content) >= len(evs_sorted[j].content):
|
|
202
|
+
to_remove.add(evs_sorted[j].id)
|
|
203
|
+
else:
|
|
204
|
+
to_remove.add(evs_sorted[i].id)
|
|
205
|
+
break
|
|
206
|
+
merged += 1
|
|
207
|
+
|
|
208
|
+
self.state.evidences = [e for e in self.state.evidences if e.id not in to_remove]
|
|
209
|
+
self.state.cpe_regularization_count += 1
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"status": "ok",
|
|
213
|
+
"anchor": anchor_name,
|
|
214
|
+
"threshold": threshold,
|
|
215
|
+
"removed": len(to_remove),
|
|
216
|
+
"merged_pairs": merged,
|
|
217
|
+
"remaining": len(evs) - len(to_remove),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def regularize(self, anchor_name: str, content: str, force: bool = False) -> Dict[str, Any]:
|
|
221
|
+
"""对单条证据执行 CPE 正则化检查(对外接口)
|
|
222
|
+
|
|
223
|
+
force=True 时跳过 BLOCK,仅做 FLAG 标记
|
|
224
|
+
"""
|
|
225
|
+
s = self.state
|
|
226
|
+
anchor = next((a for a in s.anchors if a.name == anchor_name), None)
|
|
227
|
+
if not anchor:
|
|
228
|
+
return {"action": "PASS", "reason": "锚点不存在,不拦截"}
|
|
229
|
+
|
|
230
|
+
result = self.evaluate_new_evidence(anchor, content)
|
|
231
|
+
action = result["action"]
|
|
232
|
+
if force and action == "BLOCK":
|
|
233
|
+
action = "FLAG"
|
|
234
|
+
|
|
235
|
+
if action == "PASS":
|
|
236
|
+
# 已通过的证据ID会在外部添加后追加到 regularized_evidences
|
|
237
|
+
pass
|
|
238
|
+
elif action == "BLOCK":
|
|
239
|
+
s.blocked_evidences.append(f"{anchor_name}:{content[:40]}")
|
|
240
|
+
s.cpe_regularization_count += 1
|
|
241
|
+
elif action == "FLAG":
|
|
242
|
+
s.blocked_evidences.append(f"FLAG:{anchor_name}:{content[:40]}")
|
|
243
|
+
s.cpe_regularization_count += 1
|
|
244
|
+
|
|
245
|
+
# 保存扫描结果
|
|
246
|
+
self.scan_erosion()
|
|
247
|
+
return result
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from datetime import datetime, timezone, timedelta
|
|
2
|
-
from
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .workspace import WorkspaceState
|
|
3
8
|
|
|
4
9
|
|
|
5
10
|
from contextlib import contextmanager
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""蒸馏 #197 + #198 守卫原语:能力菜单 + 升级刹车 + 确认闸。
|
|
2
|
+
|
|
3
|
+
#197 MCP 安全边界:能力描述/标签只是提示语,不是沙箱;隔离靠部署层。
|
|
4
|
+
→ CapabilityMenu:声明带名字/参数/风险等级的能力;高危调用前插确认闸。
|
|
5
|
+
#198 Agent 停下问人:升级条件 = 低置信 ∨ 不可逆 ∨ 超预算;审批=方向盘。
|
|
6
|
+
→ should_escalate(confidence, irreversible, over_budget) + confirm_gate。
|
|
7
|
+
|
|
8
|
+
液环 cli 已有 --force 确认雏形(cpe_check);本模块提供通用原语,供破坏性操作接入,
|
|
9
|
+
让「动作前问人」成为可配置策略而非散落各处的硬编码。零依赖:仅标准库。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
RISK_LEVELS = ("low", "medium", "high")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_content(content: str) -> str | None:
|
|
17
|
+
"""内容质量 ValidationRule(08-18 审计补盲):挡无意义/垃圾写入。
|
|
18
|
+
|
|
19
|
+
返回拒绝原因字符串;通过返回 None。
|
|
20
|
+
规则(纯规则零依赖,只挡"无意义",不评"价值"):
|
|
21
|
+
1) 空内容;
|
|
22
|
+
2) 无字母/汉字 且 过短(<8) 的纯占位/符号噪音(如 "1"、"!"、"!!!");
|
|
23
|
+
3) 字符多样性过低(熵代理)的重复噪音(如 "aaaaaa"、"111111")。
|
|
24
|
+
单点维护:server ll_remember 与 workspace.add_evidence 共用本函数。
|
|
25
|
+
"""
|
|
26
|
+
c = (content or "").strip()
|
|
27
|
+
if not c:
|
|
28
|
+
return "content 为空: 疑似无意义写入"
|
|
29
|
+
has_text = any(not ch.isdigit() and ch.isalnum() for ch in c)
|
|
30
|
+
if not has_text and len(c) < 8:
|
|
31
|
+
return "content 无文字内容且过短: 疑似占位噪音"
|
|
32
|
+
if len(set(c)) / len(c) < 0.2:
|
|
33
|
+
return "content 字符多样性过低(熵代理): 疑似重复噪音"
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def should_escalate(confidence: float = 1.0, irreversible: bool = False,
|
|
38
|
+
over_budget: bool = False) -> bool:
|
|
39
|
+
"""升级刹车:命中任一红线即交回人。低置信 ∨ 不可逆 ∨ 超预算。
|
|
40
|
+
|
|
41
|
+
全自动不是高级,敢于在关键时刻说"我需要你"才是设计出来的安全网。
|
|
42
|
+
"""
|
|
43
|
+
return (confidence < 0.5) or bool(irreversible) or bool(over_budget)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CapabilityMenu:
|
|
47
|
+
"""能力菜单:声明带风险等级的能力,隔离靠部署层而非标签。"""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self._caps: dict = {}
|
|
51
|
+
|
|
52
|
+
def declare(self, name: str, risk: str = "low", description: str = "",
|
|
53
|
+
params: list | None = None) -> None:
|
|
54
|
+
if risk not in RISK_LEVELS:
|
|
55
|
+
raise ValueError(f"risk 必须是 {RISK_LEVELS},收到 {risk!r}")
|
|
56
|
+
self._caps[name] = {
|
|
57
|
+
"risk": risk,
|
|
58
|
+
"description": description,
|
|
59
|
+
"params": params or [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def risk_of(self, name: str) -> str | None:
|
|
63
|
+
cap = self._caps.get(name)
|
|
64
|
+
return cap["risk"] if cap else None
|
|
65
|
+
|
|
66
|
+
def requires_confirm(self, name: str) -> bool:
|
|
67
|
+
"""high 风险能力调用前必须确认(对齐液环确认闸门)。"""
|
|
68
|
+
return self.risk_of(name) == "high"
|
|
69
|
+
|
|
70
|
+
def as_menu(self) -> dict:
|
|
71
|
+
"""供协议层展示的菜单(提示语,非沙箱)。"""
|
|
72
|
+
return dict(self._caps)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def confirm_gate(action: str, risk: str = "high", auto_approve: bool = False) -> bool:
|
|
76
|
+
"""确认闸:high 风险且未 auto 确认 → 返回 False(拦截,交回人)。
|
|
77
|
+
|
|
78
|
+
auto_approve=True 用于 dry-run / 测试;真实高危操作应默认拦截。
|
|
79
|
+
返回 True=放行,False=拦截(需人工确认)。
|
|
80
|
+
"""
|
|
81
|
+
if risk not in RISK_LEVELS:
|
|
82
|
+
risk = "high" # 未知风险按最高处置
|
|
83
|
+
if risk == "high" and not auto_approve:
|
|
84
|
+
return False
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PerceptionGate:
|
|
89
|
+
"""门控-液环因果共生环的胶水层(“单终端机器人”概念落地件)。
|
|
90
|
+
|
|
91
|
+
设计定位(克制原则:精选 4 模块,不重新膨胀):
|
|
92
|
+
- 输入候选感知动作 + 预算余量 + 灵敏度,输出 allow / degrade / block + 理由。
|
|
93
|
+
- 内部消费:
|
|
94
|
+
1) guard.should_escalate —— 不可逆变 + 低置信 → 交回人(block)。
|
|
95
|
+
2) 因果边(workspace.causal:enables/causes/contradicts)→ 「因果核心永饿死」
|
|
96
|
+
豁免:命中因果核心的动作即便超预算也放行,保证液环主链路不被闸饿死。
|
|
97
|
+
3) adaptive_recall 的负载双模(load<0.5 冗余验证 / ≥0.5 分工扩容)→
|
|
98
|
+
超预算时按当前认知负载决定 degrade(省算力)还是 allow(精度优先)。
|
|
99
|
+
4) build_or_cache(rar 本地索引缓存)→ 本地算力溶解「重建索引」成本,
|
|
100
|
+
使高载降级 skip 无后顾之忧(详见测试 test_perception_gate.py)。
|
|
101
|
+
|
|
102
|
+
零依赖:真实因果边 / 负载来自 liquid-loop 的 workspace / recall_filter / rar,
|
|
103
|
+
由 adapter 注入(causal_core_predicate / load_probe),不在本模块 import 重型依赖。
|
|
104
|
+
这样门控成为「可配置策略」,而非把整套液环塞进门里的膨胀方案。
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, budget: float = 1.0, sensitivity: str = "normal",
|
|
108
|
+
causal_core_predicate=None, load_probe=None):
|
|
109
|
+
self.budget = float(budget)
|
|
110
|
+
self.sensitivity = sensitivity
|
|
111
|
+
self._spent = 0.0
|
|
112
|
+
# 默认谓词:无注入时一律非因果核心(保守,闸正常生效)
|
|
113
|
+
self.causal_core_predicate = causal_core_predicate or (lambda action: False)
|
|
114
|
+
# 默认负载探测器:无注入时返回 0.0(低载,精度优先路径)
|
|
115
|
+
self.load_probe = load_probe or (lambda: 0.0)
|
|
116
|
+
|
|
117
|
+
def decide(self, action, cost: float = 0.0, confidence: float = 1.0,
|
|
118
|
+
irreversible: bool = False) -> dict:
|
|
119
|
+
"""对一条候选感知动作做门控裁决。
|
|
120
|
+
|
|
121
|
+
返回 dict: {"decision": allow|degrade|block, "reason": str, ...}
|
|
122
|
+
- block : 需人工确认(交回人),不消耗预算。
|
|
123
|
+
- allow : 放行(因果核心豁免 / 正常 / 低载精度优先)。
|
|
124
|
+
- degrade : 超预算 + 高载 → 降采样 / 跳过,省算力;不消耗预算。
|
|
125
|
+
"""
|
|
126
|
+
# 1) 不可逆变 + 低置信 → 交回人(should_escalate 红线)
|
|
127
|
+
if should_escalate(confidence=confidence, irreversible=irreversible):
|
|
128
|
+
return {
|
|
129
|
+
"decision": "block",
|
|
130
|
+
"reason": "irreversible+low_confidence→需人工确认",
|
|
131
|
+
"escalate": True,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# 2) 因果核心 → 永放行(never starve causal core)
|
|
135
|
+
if self.causal_core_predicate(action):
|
|
136
|
+
return {
|
|
137
|
+
"decision": "allow",
|
|
138
|
+
"reason": "因果核心动作永放行(保护液环主链路)",
|
|
139
|
+
"causal_core": True,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# 3) 预算检查
|
|
143
|
+
remaining = self.budget - self._spent
|
|
144
|
+
if cost > remaining:
|
|
145
|
+
load = float(self.load_probe())
|
|
146
|
+
if load >= 0.5:
|
|
147
|
+
# 高载:降级 skip / 降采样,省算力
|
|
148
|
+
# (rar.build_or_cache 已本地化索引重建成本,skip 无后顾之忧)
|
|
149
|
+
return {
|
|
150
|
+
"decision": "degrade",
|
|
151
|
+
"reason": f"超预算+高载({load:.2f})→降采样/跳过省算力",
|
|
152
|
+
"load": load,
|
|
153
|
+
"over_budget": True,
|
|
154
|
+
}
|
|
155
|
+
# 低载:冗余验证,精度优先(仍消耗预算)
|
|
156
|
+
self._spent += cost
|
|
157
|
+
return {
|
|
158
|
+
"decision": "allow",
|
|
159
|
+
"reason": f"超预算+低载({load:.2f})→冗余验证精度优先",
|
|
160
|
+
"load": load,
|
|
161
|
+
"over_budget": True,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# 4) 正常放行
|
|
165
|
+
self._spent += cost
|
|
166
|
+
return {
|
|
167
|
+
"decision": "allow",
|
|
168
|
+
"reason": "正常放行",
|
|
169
|
+
"load": float(self.load_probe()),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def remaining(self) -> float:
|
|
173
|
+
"""剩余预算。"""
|
|
174
|
+
return self.budget - self._spent
|
|
@@ -318,6 +318,8 @@ class RARIndex:
|
|
|
318
318
|
for e in state.evidences:
|
|
319
319
|
if agent_id and e.agent_id != agent_id:
|
|
320
320
|
continue
|
|
321
|
+
if getattr(e, "superseded_by", False) or getattr(e, "archived", False):
|
|
322
|
+
continue # 08-18 对齐 ll_recall:被取代/归档证据退出召回候选
|
|
321
323
|
cands.append({
|
|
322
324
|
"id": e.id, "type": "evidence",
|
|
323
325
|
"category": _category_of(state, e.anchor_id),
|