liquid-loop 1.8.2__tar.gz → 1.8.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.
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/PKG-INFO +1 -1
- liquid_loop-1.8.4/liquid_loop/__init__.py +17 -0
- liquid_loop-1.8.4/liquid_loop/context_compress.py +214 -0
- liquid_loop-1.8.4/liquid_loop/procedural_memory.py +361 -0
- liquid_loop-1.8.4/liquid_loop/recall_filter.py +126 -0
- liquid_loop-1.8.4/liquid_loop/self_eval.py +164 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/workspace.py +11 -2
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/PKG-INFO +1 -1
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/SOURCES.txt +8 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/pyproject.toml +1 -1
- liquid_loop-1.8.4/tests/test_context_compress.py +134 -0
- liquid_loop-1.8.4/tests/test_procedural_memory.py +208 -0
- liquid_loop-1.8.4/tests/test_recall_filter.py +79 -0
- liquid_loop-1.8.4/tests/test_self_eval.py +77 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_semantica_borrow.py +23 -0
- liquid_loop-1.8.2/liquid_loop/__init__.py +0 -11
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/LICENSE +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/README.md +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/__main__.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/cli.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/cognitive_budget.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/entropy.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/liquid_reweight.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/rar.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/selfspin.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop/storage.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/dependency_links.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/entry_points.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/requires.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop.egg-info/top_level.txt +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop_exp/liquid_persist_ab.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/liquid_loop_exp/liquid_recall_ab.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/setup.cfg +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_attention_gain.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_auth_guard.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_cli_version.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_consensus_expansion.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_entropy.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_layer1_persistence.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_lifecycle.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_liquid_reweight.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_nucleate_dual_track.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_peek_seal.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_rar.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_replay_pressure.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_seal_persistence.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_self_evolve.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_selfspin.py +0 -0
- {liquid_loop-1.8.2 → liquid_loop-1.8.4}/tests/test_workspace.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Liquid Loop — Workspace Cognitive Runtime v1.8.4 (禁向量·活态液态神经网络:成核门槛/反证轨/老化回收/原理优先成核/因果演化/软取代supersede+治理查询state_at/impact/duplicates+程序性记忆层procedural_memory)"""
|
|
2
|
+
__version__ = "1.8.4"
|
|
3
|
+
|
|
4
|
+
from .workspace import (
|
|
5
|
+
WorkspaceState, Anchor, Evidence, Memory, Conflict,
|
|
6
|
+
AuditChain, CPERegularizer, SelfRefineEngine,
|
|
7
|
+
)
|
|
8
|
+
from .storage import load, save, locked_state
|
|
9
|
+
from .entropy import calculate, calculate_detail, calculate as calculate_entropy
|
|
10
|
+
from .selfspin import LiquidSelfSpin
|
|
11
|
+
from .liquid_reweight import LiquidReweight
|
|
12
|
+
from .context_compress import ExtractiveCondenser, compress_context, CondenseReport, structured_note
|
|
13
|
+
from .self_eval import consensus_self_check, diverg_beta, majority_vote, u_opsd_step
|
|
14
|
+
from .recall_filter import recall_content_filter, content_aware_filter, cosine_dict
|
|
15
|
+
from .procedural_memory import (
|
|
16
|
+
ProceduralMemory, ProceduralRegistry, procmem_recall, procmem_admit,
|
|
17
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""长会话/长上下文提取式压缩(蒸馏自 Octomind condense.rs,落地液环)
|
|
2
|
+
|
|
3
|
+
把外部 runtime 的「提取式上下文压缩」蒸馏为液环一等公民组件:
|
|
4
|
+
- 超阈值触发 → 选行保留(非生成式摘要)
|
|
5
|
+
- 单条 < MIN_CANDIDATE_TOKENS 绝不触碰
|
|
6
|
+
- 压后 ≥ 压前则保留原文(no-gain 保护)
|
|
7
|
+
- 任何异常保留原文(fail-open)
|
|
8
|
+
|
|
9
|
+
契合液环哲学:零向量、确定性、零丢失。
|
|
10
|
+
压缩仅裁剪噪声行,原文语义结构(决策/错误/文件路径/代码/结论)保留;
|
|
11
|
+
底层 storage 完整存档,「蒸馏」与「归档」互不干扰。
|
|
12
|
+
|
|
13
|
+
触发阈值:环境变量 LIQUID_CONTEXT_COMPRESS_TOKENS(默认 0 = 不压缩),
|
|
14
|
+
与 LIQUID_EVIDENCE_BUDGET 运维风格一致。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def estimate_tokens(text: str) -> int:
|
|
25
|
+
"""字符数 / 4 启发式(对预算触发判断足够精确,零依赖 tiktoken)。"""
|
|
26
|
+
if not text:
|
|
27
|
+
return 0
|
|
28
|
+
return max(1, len(text) // 4)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 高价值行信号:保留含错误/决策/文件路径/代码定义/符号的行
|
|
32
|
+
_HIGH_VALUE = re.compile(
|
|
33
|
+
r"(?i)("
|
|
34
|
+
r"\b(error|failed|exception|traceback|panic)\b" # 错误/异常
|
|
35
|
+
r"|\b(decision|changed|fixed|result|todo|fix|note)\b" # 决策/结论
|
|
36
|
+
r"|[\w./\-]+\.(py|rs|ts|js|go|toml|json|md|yaml|yml)" # 文件路径
|
|
37
|
+
r"|^\s*(def|fn|func|class|impl|pub|import|export)\b" # 代码定义
|
|
38
|
+
r"|=>|==|!=|::|->" # 代码符号
|
|
39
|
+
r")"
|
|
40
|
+
)
|
|
41
|
+
# 噪声行信号:空/纯符号行、闲聊开场
|
|
42
|
+
_NOISE = re.compile(
|
|
43
|
+
r"(?i)^[\s>*\-–·•]*$"
|
|
44
|
+
r"|^(thinking|let me|i'll|sure|okay|great|here's)\b"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CondenseReport:
|
|
50
|
+
"""压缩运行报告(对齐原版统计字段)。"""
|
|
51
|
+
|
|
52
|
+
triggered: bool = False
|
|
53
|
+
candidates: int = 0
|
|
54
|
+
condensed: int = 0
|
|
55
|
+
untouched: int = 0
|
|
56
|
+
saved_tokens: int = 0
|
|
57
|
+
notes: list = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExtractiveCondenser:
|
|
61
|
+
"""超阈值触发 → 选行保留(非生成式摘要)→ fail-open 不崩。
|
|
62
|
+
|
|
63
|
+
与原版 condense_round 不变量对齐:
|
|
64
|
+
- 整轮 token 总和 > threshold 才触发(防小结果免费注入大轮)
|
|
65
|
+
- 单条 < MIN_CANDIDATE_TOKENS 绝不触碰
|
|
66
|
+
- 压后 ≥ 压前则保留原文 (no-gain)
|
|
67
|
+
- 任何异常返回原样 (fail-open)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
MIN_CANDIDATE_TOKENS = 512
|
|
71
|
+
KEEP_RATIO = 0.5 # 候选结果最多保留的行比例上限
|
|
72
|
+
|
|
73
|
+
def __init__(self, tokens_threshold: int = 0):
|
|
74
|
+
# 0 = 不压缩(缺省关闭,与 LIQUID_EVIDENCE_BUDGET 风格一致)
|
|
75
|
+
self.tokens_threshold = tokens_threshold
|
|
76
|
+
|
|
77
|
+
def _score_line(self, line: str) -> float:
|
|
78
|
+
"""启发式重要性打分(替代原版 cheap-LLM 行选择,零 LLM 依赖)。"""
|
|
79
|
+
s = 0.0
|
|
80
|
+
if _HIGH_VALUE.search(line):
|
|
81
|
+
s += 3.0
|
|
82
|
+
if _NOISE.search(line):
|
|
83
|
+
s -= 2.0
|
|
84
|
+
stripped = line.strip()
|
|
85
|
+
if len(stripped) >= 20:
|
|
86
|
+
s += 1.0
|
|
87
|
+
# 近似重复行惩罚
|
|
88
|
+
if len(stripped) > 40 and stripped.count(stripped[:10]) > 2:
|
|
89
|
+
s -= 1.5
|
|
90
|
+
return s
|
|
91
|
+
|
|
92
|
+
def condense_round(self, results: list) -> tuple:
|
|
93
|
+
"""results: list of dict {"tool": str, "content": str}
|
|
94
|
+
|
|
95
|
+
返回 (压缩后 results, CondenseReport)。fail-open 时 triggered=False。
|
|
96
|
+
"""
|
|
97
|
+
rep = CondenseReport()
|
|
98
|
+
try:
|
|
99
|
+
sizes = [estimate_tokens(r.get("content", "")) for r in results]
|
|
100
|
+
if sum(sizes) <= self.tokens_threshold:
|
|
101
|
+
return results, rep # 未触发
|
|
102
|
+
rep.triggered = True
|
|
103
|
+
|
|
104
|
+
candidates = [i for i, t in enumerate(sizes)
|
|
105
|
+
if t >= self.MIN_CANDIDATE_TOKENS]
|
|
106
|
+
rep.candidates = len(candidates)
|
|
107
|
+
if not candidates:
|
|
108
|
+
return results, rep
|
|
109
|
+
|
|
110
|
+
out = list(results)
|
|
111
|
+
for idx in candidates:
|
|
112
|
+
r = results[idx]
|
|
113
|
+
original = r.get("content", "")
|
|
114
|
+
before = estimate_tokens(original)
|
|
115
|
+
lines = original.splitlines()
|
|
116
|
+
if len(lines) <= 2:
|
|
117
|
+
rep.untouched += 1
|
|
118
|
+
continue
|
|
119
|
+
scored = [(self._score_line(ln), i, ln) for i, ln in enumerate(lines)]
|
|
120
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
121
|
+
keep_n = max(2, int(len(lines) * self.KEEP_RATIO))
|
|
122
|
+
keep_idx = sorted(i for _, i, _ in scored[:keep_n])
|
|
123
|
+
new_content = "\n".join(lines[i] for i in keep_idx)
|
|
124
|
+
after = estimate_tokens(new_content)
|
|
125
|
+
if after >= before: # no-gain 保护
|
|
126
|
+
rep.untouched += 1
|
|
127
|
+
continue
|
|
128
|
+
out[idx] = {"tool": r.get("tool", ""), "content": new_content}
|
|
129
|
+
rep.condensed += 1
|
|
130
|
+
rep.saved_tokens += (before - after)
|
|
131
|
+
if rep.condensed:
|
|
132
|
+
rep.notes.append("📎 CONDENSED by supervisor")
|
|
133
|
+
return out, rep
|
|
134
|
+
except Exception as e:
|
|
135
|
+
rep.notes.append(f"fail-open: {e}")
|
|
136
|
+
return results, rep
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compress_context(
|
|
140
|
+
texts: list,
|
|
141
|
+
threshold: Optional[int] = None,
|
|
142
|
+
) -> tuple:
|
|
143
|
+
"""液环便捷入口:把多条证据/上下文作为一轮压缩。
|
|
144
|
+
|
|
145
|
+
texts: 待压缩文本列表(如 recall 返回的多条证据 content)
|
|
146
|
+
threshold: 触发阈值 token;None 时读 LIQUID_CONTEXT_COMPRESS_TOKENS(默认 0=不压)
|
|
147
|
+
返回 (压缩后文本列表, CondenseReport)
|
|
148
|
+
"""
|
|
149
|
+
if threshold is None:
|
|
150
|
+
threshold = int(os.environ.get("LIQUID_CONTEXT_COMPRESS_TOKENS", "0"))
|
|
151
|
+
if threshold <= 0:
|
|
152
|
+
return list(texts), CondenseReport() # 未启用
|
|
153
|
+
results = [{"tool": "", "content": t} for t in texts]
|
|
154
|
+
out, rep = ExtractiveCondenser(threshold).condense_round(results)
|
|
155
|
+
return [r["content"] for r in out], rep
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# RE-TRAC 同构:结构化三组分笔记(提取式分桶,非生成式)
|
|
160
|
+
# 把压缩后的证据/上下文按语义分桶为 {answer, evidence, open},
|
|
161
|
+
# 对应 RE-TRAC 的「当前最优答案 + 证据库 + 不确定项/待探索」。
|
|
162
|
+
# 零 LLM 依赖、fail-open,契合液环「提取式、确定性、零丢失」哲学。
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
_BUCKET_ANSWER = re.compile(
|
|
165
|
+
r"(?i)("
|
|
166
|
+
r"\b(result|fixed|changed|decision|conclusion|answer|resolved|done)\b"
|
|
167
|
+
r"|结论|已修复|决定|已解决"
|
|
168
|
+
r")"
|
|
169
|
+
)
|
|
170
|
+
_BUCKET_OPEN = re.compile(
|
|
171
|
+
r"(?i)("
|
|
172
|
+
r"\b(failed|exception|error|uncertain|unknown|pending|todo|wip)\b"
|
|
173
|
+
r"|待探索|待定|不确定|失败|待办|未解决"
|
|
174
|
+
r")"
|
|
175
|
+
)
|
|
176
|
+
_BUCKET_EVIDENCE = re.compile(
|
|
177
|
+
r"(?i)("
|
|
178
|
+
r"[\w./\-]+\.(py|rs|ts|js|go|toml|json|md|yaml|yml)" # 文件路径
|
|
179
|
+
r"|=>|==|!=|::|->" # 代码符号
|
|
180
|
+
r"|\b(path|evidence|data|source|依据|来源|数据)\b"
|
|
181
|
+
r")"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def structured_note(
|
|
186
|
+
texts: list,
|
|
187
|
+
threshold: Optional[int] = None,
|
|
188
|
+
) -> dict:
|
|
189
|
+
"""RE-TRAC 同构:把多条证据/上下文压成结构化三组分笔记。
|
|
190
|
+
|
|
191
|
+
返回 {answer, evidence, open} 三个文本列表。分桶为提取式
|
|
192
|
+
(按行语义正则归类),不生成新内容,fail-open(异常返回空桶)。
|
|
193
|
+
|
|
194
|
+
- answer : 当前最优结论 / 已修复 / 决策
|
|
195
|
+
- evidence : 路径 / 符号 / 数据 / 来源等支撑性内容(保底桶,不丢行)
|
|
196
|
+
- open : 失败 / 异常 / 不确定 / 待探索项
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
compressed, _ = compress_context(texts, threshold)
|
|
200
|
+
buckets = {"answer": [], "evidence": [], "open": []}
|
|
201
|
+
for t in compressed:
|
|
202
|
+
for line in t.splitlines():
|
|
203
|
+
s = line.strip()
|
|
204
|
+
if not s:
|
|
205
|
+
continue
|
|
206
|
+
if _BUCKET_OPEN.search(s):
|
|
207
|
+
buckets["open"].append(s)
|
|
208
|
+
elif _BUCKET_ANSWER.search(s):
|
|
209
|
+
buckets["answer"].append(s)
|
|
210
|
+
else:
|
|
211
|
+
buckets["evidence"].append(s) # 保底不丢
|
|
212
|
+
return buckets
|
|
213
|
+
except Exception:
|
|
214
|
+
return {"answer": [], "evidence": [], "open": []}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""程序性记忆层(distill_registry 实现 · D3 + 几轮军师调研蒸馏整合)
|
|
2
|
+
|
|
3
|
+
蒸馏这几轮调研的「必要内容」,落地为液环一等公民组件:
|
|
4
|
+
|
|
5
|
+
- PlugMem(#124 / #170 第二信源): procedural(行动处方)记忆一等公民,按任务召回
|
|
6
|
+
→ 本模块把「技能/配方」作为独立记忆类型,与声明式 Anchor/Evidence/Memory 正交
|
|
7
|
+
- Skill1(#169): 单一任务信号拆三份 → 此处落地为「选择/利用/蒸馏」三类信用:
|
|
8
|
+
选择信用 = 任务标签匹配度;利用信用 = 复用成功率;蒸馏信用 = 改进增量
|
|
9
|
+
- EvoC2F(#172): 验证门控技能进化(功能测试+契约验证+回归评估)+分阶段部署
|
|
10
|
+
(shadow→canary→active) + 不可逆操作交人工(↔ ops_gate 不可逆确认)
|
|
11
|
+
- RE-TRAC(#165, 已落 structured_note): 技能笔记用 {answer, evidence, open} 三组分
|
|
12
|
+
|
|
13
|
+
守液环铁律:
|
|
14
|
+
- 禁向量:任务路由用结构化 tag 精确/子串匹配,绝不 cosine / embedding
|
|
15
|
+
- 提取式/确定性:准入判定为确定性规则,零 LLM 依赖
|
|
16
|
+
- fail-open:任何异常返回安全默认(空列表/降级状态),不阻断主流程
|
|
17
|
+
- 零丢失:被拒/降级的技能仍保留(gate_status=rejected),可审计、可解冻
|
|
18
|
+
|
|
19
|
+
持久化:sidecar `.liquid/procedural.json`,独立 fcntl 锁,不触碰 WorkspaceState 序列化
|
|
20
|
+
(与 storage.py 同风格,但独立文件避免扩大 WorkspaceState 改动面)。
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fcntl
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field, fields
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from .context_compress import structured_note
|
|
33
|
+
from .workspace import now
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 阈值(环境变量可覆写,缺省保守)─────────────────────────────────────
|
|
37
|
+
def _env_float(name: str, default: float) -> float:
|
|
38
|
+
try:
|
|
39
|
+
v = float(os.environ.get(name, default))
|
|
40
|
+
return v
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return default
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
REGRESSION_EPS = _env_float("LIQUID_PROCMEM_REGRESSION_EPS", 0.6) # 回归门:成功率≥此值才晋级
|
|
46
|
+
MIN_USES_SHADOW = max(1, int(_env_float("LIQUID_PROCMEM_MIN_USES", 3))) # 出 shadow 最少使用次数(≥1)
|
|
47
|
+
MIN_USES_ACTIVE = MIN_USES_SHADOW * 2 # 出 canary 进 active 所需使用次数
|
|
48
|
+
|
|
49
|
+
_ACTION_VERB = re.compile(
|
|
50
|
+
r"(?i)\b(run|exec|invoke|call|use|apply|send|start|build|deploy|install|"
|
|
51
|
+
r"query|fetch|write|read|patch|create|delete|test|verify|extract)\b"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ProceduralMemory:
|
|
57
|
+
"""一条程序性记忆(行动处方)。
|
|
58
|
+
|
|
59
|
+
与声明式 Memory(结晶自≥2一致证据) 正交:这是「怎么做」而非「是什么」。
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
skill_id: str = ""
|
|
63
|
+
task_tags: list = field(default_factory=list) # 结构化任务标签(非向量);选择信用匹配键
|
|
64
|
+
invocation: str = "" # 行动处方(how-to/命令/配方)
|
|
65
|
+
evidence: str = "" # 成功证据/来源
|
|
66
|
+
note: dict = field(default_factory=lambda: {"answer": [], "evidence": [], "open": []}) # RE-TRAC 三组分
|
|
67
|
+
gate_status: str = "pending" # pending→shadow→canary→active | rejected
|
|
68
|
+
admit_score: float = 0.0 # EvoC2F 三门前综合分(0~1)
|
|
69
|
+
use_count: int = 0
|
|
70
|
+
success_count: int = 0
|
|
71
|
+
contract: dict = field(default_factory=dict) # {inputs, outputs} 契约(门2 校验依据)
|
|
72
|
+
irreversible: bool = False # 不可逆操作→交人工(↔ ops_gate)
|
|
73
|
+
requires_human: bool = False # 不可逆技能晋级到 canary 后需人工放行
|
|
74
|
+
created_at: str = field(default_factory=now)
|
|
75
|
+
last_used_at: str = ""
|
|
76
|
+
|
|
77
|
+
# ── 派生指标 ──
|
|
78
|
+
@property
|
|
79
|
+
def success_rate(self) -> float:
|
|
80
|
+
if self.use_count == 0:
|
|
81
|
+
return 0.0
|
|
82
|
+
return self.success_count / self.use_count
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def usable(self) -> bool:
|
|
86
|
+
"""是否可被任务召回(降级/待审不可召回)。"""
|
|
87
|
+
return self.gate_status in ("active", "canary")
|
|
88
|
+
|
|
89
|
+
def as_dict(self) -> dict:
|
|
90
|
+
return {
|
|
91
|
+
"skill_id": self.skill_id,
|
|
92
|
+
"task_tags": list(self.task_tags),
|
|
93
|
+
"invocation": self.invocation,
|
|
94
|
+
"evidence": self.evidence,
|
|
95
|
+
"note": self.note,
|
|
96
|
+
"gate_status": self.gate_status,
|
|
97
|
+
"admit_score": round(self.admit_score, 3),
|
|
98
|
+
"use_count": self.use_count,
|
|
99
|
+
"success_count": self.success_count,
|
|
100
|
+
"success_rate": round(self.success_rate, 3),
|
|
101
|
+
"contract": self.contract,
|
|
102
|
+
"irreversible": self.irreversible,
|
|
103
|
+
"requires_human": self.requires_human,
|
|
104
|
+
"created_at": self.created_at,
|
|
105
|
+
"last_used_at": self.last_used_at,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProceduralRegistry:
|
|
110
|
+
"""程序性记忆注册表:验证门控准入 + 分阶段部署 + 任务路由召回。
|
|
111
|
+
|
|
112
|
+
fail-open:load/save/admit/recall 任意异常均返回安全默认,不抛出。
|
|
113
|
+
独立 sidecar 持久化,不依赖 WorkspaceState。
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
STORE_NAME = "procedural.json"
|
|
117
|
+
|
|
118
|
+
def __init__(self, workspace_root: Path):
|
|
119
|
+
self.root = Path(workspace_root)
|
|
120
|
+
self.path = self.root / ".liquid" / self.STORE_NAME
|
|
121
|
+
self._items: dict = {}
|
|
122
|
+
self._load()
|
|
123
|
+
|
|
124
|
+
# ── 持久化(fcntl 排他锁,与 storage.py 同风格)─────────────────────
|
|
125
|
+
def _load(self) -> None:
|
|
126
|
+
try:
|
|
127
|
+
if self.path.exists():
|
|
128
|
+
with open(self.path, "r", encoding="utf-8") as f:
|
|
129
|
+
data = json.load(f)
|
|
130
|
+
_fields = {f.name for f in fields(ProceduralMemory)}
|
|
131
|
+
self._items = {
|
|
132
|
+
sid: ProceduralMemory(**{k: v for k, v in it.items() if k in _fields})
|
|
133
|
+
for sid, it in data.get("skills", {}).items()
|
|
134
|
+
}
|
|
135
|
+
except Exception:
|
|
136
|
+
self._items = {}
|
|
137
|
+
|
|
138
|
+
def _save(self) -> bool:
|
|
139
|
+
"""原子写 sidecar;返回是否成功(失败不阻断,但调用方可感知)。"""
|
|
140
|
+
try:
|
|
141
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
data = {"skills": {sid: it.as_dict() for sid, it in self._items.items()}}
|
|
143
|
+
tmp = self.path.with_suffix(".tmp")
|
|
144
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
145
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
146
|
+
f.flush()
|
|
147
|
+
os.fsync(f.fileno())
|
|
148
|
+
os.replace(tmp, self.path)
|
|
149
|
+
return True
|
|
150
|
+
except Exception:
|
|
151
|
+
return False # fail-open:持久化失败不阻断,但结果带 warning
|
|
152
|
+
|
|
153
|
+
def _with_lock(self, fn):
|
|
154
|
+
"""对 sidecar 加排他锁后执行 fn,保证 load→modify→save 原子。
|
|
155
|
+
|
|
156
|
+
非阻塞锁 + fail-open:锁被占用(竞争/陈旧进程)时立即返回错误,
|
|
157
|
+
绝不阻塞调用方(否则会拖垮整个 HTTP 端点)。这符合液环 fail-open 铁律。
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
lk = open(self.path.with_suffix(".lock"), "w")
|
|
162
|
+
try:
|
|
163
|
+
fcntl.flock(lk, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
164
|
+
except BlockingIOError:
|
|
165
|
+
return {"ok": False, "error": "lock busy (fail-open, 重试)"}
|
|
166
|
+
try:
|
|
167
|
+
self._load()
|
|
168
|
+
out = fn()
|
|
169
|
+
saved = self._save()
|
|
170
|
+
if isinstance(out, dict) and out.get("ok") and not saved:
|
|
171
|
+
out = {**out, "warning": "持久化失败(内存已更新,未落盘)"}
|
|
172
|
+
return out
|
|
173
|
+
finally:
|
|
174
|
+
fcntl.flock(lk, fcntl.LOCK_UN)
|
|
175
|
+
lk.close()
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return {"ok": False, "error": f"fail-open: {e}"}
|
|
178
|
+
|
|
179
|
+
# ── EvoC2F 三关准入 ──────────────────────────────────────────────
|
|
180
|
+
def _gate_functional(self, invocation: str) -> tuple:
|
|
181
|
+
"""门1 功能测试(本地代理):行动处方非空且含可行动词/足够长度。"""
|
|
182
|
+
if not invocation or len(invocation.strip()) < 8:
|
|
183
|
+
return 0.0, "invocation 过短或为空"
|
|
184
|
+
has_verb = bool(_ACTION_VERB.search(invocation))
|
|
185
|
+
return (0.7 if has_verb else 0.4), ("含可行动词" if has_verb else "缺可行动词(弱)")
|
|
186
|
+
|
|
187
|
+
def _gate_contract(self, evidence: str, contract) -> tuple:
|
|
188
|
+
"""门2 契约验证(本地代理):若提供契约,校验证据涵盖声明输出键。"""
|
|
189
|
+
if not isinstance(contract, dict) or not contract:
|
|
190
|
+
return 1.0, "无契约声明→直接通过"
|
|
191
|
+
outs = contract.get("outputs", [])
|
|
192
|
+
if not outs:
|
|
193
|
+
return 1.0, "契约无输出声明"
|
|
194
|
+
if not evidence:
|
|
195
|
+
return 0.0, "有契约但缺证据可校验"
|
|
196
|
+
hit = sum(1 for o in outs if o and o in evidence)
|
|
197
|
+
frac = hit / len(outs)
|
|
198
|
+
ok = frac >= 0.5
|
|
199
|
+
return (frac, f"证据涵盖 {hit}/{len(outs)} 输出键" + ("" if ok else "→不足"))
|
|
200
|
+
|
|
201
|
+
def _gate_regression(self, use_count: int, success_rate: float) -> tuple:
|
|
202
|
+
"""门3 回归评估(本地代理):无使用数据前不可定级,置 shadow。"""
|
|
203
|
+
if use_count == 0:
|
|
204
|
+
return 0.0, "尚无使用数据→置 shadow 观察"
|
|
205
|
+
ok = success_rate >= REGRESSION_EPS
|
|
206
|
+
return (success_rate, f"成功率 {success_rate:.2f} " + ("≥阈值" if ok else "<阈值"))
|
|
207
|
+
|
|
208
|
+
def admit(self, skill_id: str, task_tags: list, invocation: str,
|
|
209
|
+
evidence: str = "", contract: Optional[dict] = None,
|
|
210
|
+
irreversible: bool = False) -> dict:
|
|
211
|
+
"""准入一条程序性记忆,跑 EvoC2F 三关 + 分阶段部署。
|
|
212
|
+
|
|
213
|
+
返回 {ok, skill_id, gates, gate_status, admit_score}。fail-open。
|
|
214
|
+
已 active/canary 的 skill 重准入时**保留晋级进度与复用历史**(仅更新内容字段),
|
|
215
|
+
不把已验证技能降回 shadow;shadow/rejected 重准入视为新尝试。
|
|
216
|
+
"""
|
|
217
|
+
def _do():
|
|
218
|
+
if contract is not None and not isinstance(contract, dict):
|
|
219
|
+
return {"ok": False, "error": "contract 必须是 dict 对象"}
|
|
220
|
+
existing = self._items.get(skill_id)
|
|
221
|
+
f_s, f_msg = self._gate_functional(invocation)
|
|
222
|
+
c_s, c_msg = self._gate_contract(evidence, contract or {})
|
|
223
|
+
r_s, r_msg = self._gate_regression(0, 0.0)
|
|
224
|
+
# 三关加权:功能 0.4 / 契约 0.3 / 回归(初始 0) 0.3 → 初始分由功能+契约决定
|
|
225
|
+
admit_score = round(0.4 * f_s + 0.3 * c_s + 0.3 * 0.0, 3)
|
|
226
|
+
# 功能不过 → 直接 rejected(脏技能污染防护,↔ Voyager 脏技能)
|
|
227
|
+
status = "rejected" if f_s < 0.4 else "shadow"
|
|
228
|
+
# 已验证技能(active/canary)重准入 → 保留晋级进度与历史
|
|
229
|
+
preserved = bool(existing and existing.gate_status in ("active", "canary"))
|
|
230
|
+
if preserved:
|
|
231
|
+
status = existing.gate_status
|
|
232
|
+
note = structured_note([evidence]) if evidence else {"answer": [], "evidence": [], "open": []}
|
|
233
|
+
pm = ProceduralMemory(
|
|
234
|
+
skill_id=skill_id,
|
|
235
|
+
task_tags=list(task_tags or []),
|
|
236
|
+
invocation=invocation,
|
|
237
|
+
evidence=evidence,
|
|
238
|
+
note=note,
|
|
239
|
+
gate_status=status,
|
|
240
|
+
admit_score=admit_score,
|
|
241
|
+
use_count=existing.use_count if preserved else 0,
|
|
242
|
+
success_count=existing.success_count if preserved else 0,
|
|
243
|
+
contract=dict(contract or {}),
|
|
244
|
+
irreversible=bool(irreversible),
|
|
245
|
+
requires_human=bool(irreversible) or bool(existing and existing.requires_human),
|
|
246
|
+
created_at=existing.created_at if existing else now(),
|
|
247
|
+
last_used_at=existing.last_used_at if preserved else "",
|
|
248
|
+
)
|
|
249
|
+
self._items[skill_id] = pm
|
|
250
|
+
return {
|
|
251
|
+
"ok": True,
|
|
252
|
+
"skill_id": skill_id,
|
|
253
|
+
"gates": {
|
|
254
|
+
"functional": {"score": round(f_s, 3), "msg": f_msg},
|
|
255
|
+
"contract": {"score": round(c_s, 3), "msg": c_msg},
|
|
256
|
+
"regression": {"score": round(r_s, 3), "msg": r_msg},
|
|
257
|
+
},
|
|
258
|
+
"gate_status": status,
|
|
259
|
+
"admit_score": admit_score,
|
|
260
|
+
}
|
|
261
|
+
return self._with_lock(_do)
|
|
262
|
+
|
|
263
|
+
# ── 晋级/回退(EvoC2F 分阶段部署 + 回归门控)────────────────────
|
|
264
|
+
def _recompute_stage(self, pm: ProceduralMemory) -> None:
|
|
265
|
+
if pm.gate_status in ("pending", "rejected"):
|
|
266
|
+
return
|
|
267
|
+
if pm.gate_status == "shadow" and pm.use_count >= MIN_USES_SHADOW:
|
|
268
|
+
if pm.success_rate >= REGRESSION_EPS:
|
|
269
|
+
pm.gate_status = "canary" # 不可逆也到 canary(封顶,需人工放行)
|
|
270
|
+
else:
|
|
271
|
+
pm.gate_status = "rejected" # 回归不达标→拒绝(脏技能防护)
|
|
272
|
+
elif pm.gate_status == "canary" and pm.use_count >= MIN_USES_ACTIVE:
|
|
273
|
+
if pm.success_rate >= REGRESSION_EPS:
|
|
274
|
+
if not pm.irreversible:
|
|
275
|
+
pm.gate_status = "active"
|
|
276
|
+
else:
|
|
277
|
+
pm.gate_status = "rejected" # canary 后期回归→拒绝(防已放行技能劣化)
|
|
278
|
+
|
|
279
|
+
def record_use(self, skill_id: str, success: bool) -> dict:
|
|
280
|
+
"""记录一次复用结果,自动触发晋级判定。fail-open。"""
|
|
281
|
+
def _do():
|
|
282
|
+
pm = self._items.get(skill_id)
|
|
283
|
+
if pm is None:
|
|
284
|
+
return {"ok": False, "error": "unknown skill_id"}
|
|
285
|
+
pm.use_count += 1
|
|
286
|
+
if success:
|
|
287
|
+
pm.success_count += 1
|
|
288
|
+
pm.last_used_at = now()
|
|
289
|
+
self._recompute_stage(pm)
|
|
290
|
+
return {"ok": True, "skill_id": skill_id,
|
|
291
|
+
"gate_status": pm.gate_status,
|
|
292
|
+
"success_rate": round(pm.success_rate, 3),
|
|
293
|
+
"requires_human": pm.requires_human}
|
|
294
|
+
return self._with_lock(_do)
|
|
295
|
+
|
|
296
|
+
def promote(self, skill_id: str, by: str = "human") -> dict:
|
|
297
|
+
"""人工放行晋级(不可逆技能封顶 canary 后由 ops_gate 同构的人工确认)。"""
|
|
298
|
+
def _do():
|
|
299
|
+
pm = self._items.get(skill_id)
|
|
300
|
+
if pm is None:
|
|
301
|
+
return {"ok": False, "error": "unknown skill_id"}
|
|
302
|
+
if pm.irreversible and pm.gate_status == "canary":
|
|
303
|
+
pm.requires_human = False
|
|
304
|
+
pm.gate_status = "active"
|
|
305
|
+
return {"ok": True, "skill_id": skill_id,
|
|
306
|
+
"gate_status": "active", "promoted_by": by}
|
|
307
|
+
return {"ok": False, "error": f"不可晋级(irreversible={pm.irreversible},status={pm.gate_status})"}
|
|
308
|
+
return self._with_lock(_do)
|
|
309
|
+
|
|
310
|
+
# ── Skill1 任务路由召回(禁向量:结构化 tag 匹配 + 三信用排序)──
|
|
311
|
+
@staticmethod
|
|
312
|
+
def _tag_match(task_query: str, tags: list) -> float:
|
|
313
|
+
"""结构化匹配:task_query 子串命中多少 tag(非向量)。"""
|
|
314
|
+
if not task_query or not tags:
|
|
315
|
+
return 0.0
|
|
316
|
+
q = task_query.lower()
|
|
317
|
+
hit = sum(1 for t in tags if t and t.lower() in q)
|
|
318
|
+
return hit / len(tags)
|
|
319
|
+
|
|
320
|
+
def recall_for_task(self, task_query: str, top_k: int = 3) -> list:
|
|
321
|
+
"""按任务召回可复用程序性记忆。
|
|
322
|
+
|
|
323
|
+
候选=usable 技能(active/canary);排序分 = 选择信用(tag匹配)*0.5
|
|
324
|
+
+ 利用信用(成功率×时效)*0.5。禁向量,确定性。fail-open 返回 []。
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
cands = [pm for pm in self._items.values() if pm.usable]
|
|
328
|
+
scored = []
|
|
329
|
+
for pm in cands:
|
|
330
|
+
sel = self._tag_match(task_query, pm.task_tags) # 选择信用
|
|
331
|
+
recency = 1.0 if not pm.last_used_at else 0.8 # 简化时效因子
|
|
332
|
+
util = pm.success_rate * recency # 利用信用
|
|
333
|
+
score = 0.5 * sel + 0.5 * util
|
|
334
|
+
if sel <= 0 and util <= 0:
|
|
335
|
+
continue # 既不匹配也无复用记录→不召回(避免噪声)
|
|
336
|
+
scored.append((score, pm))
|
|
337
|
+
scored.sort(key=lambda x: (-x[0], -x[1].success_rate, x[1].skill_id))
|
|
338
|
+
return [pm.as_dict() | {"match_score": round(s, 3)} for s, pm in scored[:top_k]]
|
|
339
|
+
except Exception:
|
|
340
|
+
return []
|
|
341
|
+
|
|
342
|
+
def list_all(self, include_rejected: bool = False) -> list:
|
|
343
|
+
try:
|
|
344
|
+
items = self._items.values()
|
|
345
|
+
if not include_rejected:
|
|
346
|
+
items = [p for p in items if p.gate_status != "rejected"]
|
|
347
|
+
return [p.as_dict() for p in items]
|
|
348
|
+
except Exception:
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ── 便捷函数(供 server / CLI 直接调用)────────────────────────────────
|
|
353
|
+
def procmem_recall(workspace_root: Path, task_query: str, top_k: int = 3) -> list:
|
|
354
|
+
return ProceduralRegistry(workspace_root).recall_for_task(task_query, top_k)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def procmem_admit(workspace_root: Path, skill_id: str, task_tags: list,
|
|
358
|
+
invocation: str, evidence: str = "", contract: Optional[dict] = None,
|
|
359
|
+
irreversible: bool = False) -> dict:
|
|
360
|
+
return ProceduralRegistry(workspace_root).admit(
|
|
361
|
+
skill_id, task_tags, invocation, evidence, contract, irreversible)
|