@bolloon/bolloon-agent 0.1.34 → 0.1.35
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.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +668 -204
- package/dist/web/index.html +24 -4
- package/dist/web/server.js +531 -10
- package/lefthook.yml +29 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +668 -204
- package/src/web/index.html +24 -4
- package/src/web/server.ts +596 -10
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Causal-Judge — 判断力因果论引擎
|
|
3
|
+
*
|
|
4
|
+
* 3 层算法:
|
|
5
|
+
* 1. 关联性 (Correlation) — 扫 usage.jsonl + 算互信息 + LLM 解释方向
|
|
6
|
+
* 2. 干预 (Intervention / Do-Calculus) — LLM 模拟"如果没有 A, AI 行为会怎么变"
|
|
7
|
+
* 3. 反事实 (Counterfactual) — 单次违规事件审计, 模拟"如果库是 Y 而不是 X"
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:
|
|
10
|
+
* - fail-soft: 任何一步失败不阻断, 仅 console.warn
|
|
11
|
+
* - 关联性: 纯统计 (O(N²) judgments), 一次 LLM 解释一对
|
|
12
|
+
* - 干预: 一次跑 3-5 条, 1 次 LLM 调用
|
|
13
|
+
* - 反事实: 1 次 LLM 调用
|
|
14
|
+
* - 所有结果写 evolution.jsonl (不直接改库)
|
|
15
|
+
* - TTL 90 天 (用户说按需触发, 不定时)
|
|
16
|
+
*
|
|
17
|
+
* 写入路径: ~/.bolloon/human-values/evolution.jsonl
|
|
18
|
+
* 反事实路径: ~/.bolloon/human-values/counterfactual-audit.jsonl
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs/promises';
|
|
21
|
+
import * as os from 'os';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { getModel } from '../llm/pi-ai.js';
|
|
24
|
+
import { loadAllJudgments } from './human-value-store.js';
|
|
25
|
+
const EVOLUTION_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
|
|
26
|
+
const COUNTERFACTUAL_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/counterfactual-audit.jsonl';
|
|
27
|
+
const USAGE_LOG = (process.env.HOME || os.homedir() || '/tmp') + '/.bolloon/human-values/usage.jsonl';
|
|
28
|
+
async function readUsageLog() {
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(USAGE_LOG, 'utf-8');
|
|
31
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
32
|
+
return lines
|
|
33
|
+
.map((l) => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(l);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.filter((e) => Boolean(e) && Array.isArray(e?.usedIds));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 计算两 judgment 互信息 (基于 usage 共现)
|
|
49
|
+
* I(A;B) = Σ p(a,b) log p(a,b) / (p(a)p(b))
|
|
50
|
+
* 输入是 boolean 共现: A 在场/不在场 × B 在场/不在场
|
|
51
|
+
*/
|
|
52
|
+
function mutualInfo(coOccurrence, totalA, totalB, totalEntries) {
|
|
53
|
+
if (totalEntries === 0)
|
|
54
|
+
return 0;
|
|
55
|
+
const a11 = coOccurrence;
|
|
56
|
+
const a10 = totalA - coOccurrence;
|
|
57
|
+
const a01 = totalB - coOccurrence;
|
|
58
|
+
const a00 = totalEntries - totalA - totalB + coOccurrence;
|
|
59
|
+
const p11 = a11 / totalEntries;
|
|
60
|
+
const p10 = a10 / totalEntries;
|
|
61
|
+
const p01 = a01 / totalEntries;
|
|
62
|
+
const p00 = a00 / totalEntries;
|
|
63
|
+
const pA = totalA / totalEntries;
|
|
64
|
+
const pB = totalB / totalEntries;
|
|
65
|
+
function mi(p, pA, pB) {
|
|
66
|
+
if (p <= 0 || pA <= 0 || pB <= 0)
|
|
67
|
+
return 0;
|
|
68
|
+
return p * Math.log2(p / (pA * pB));
|
|
69
|
+
}
|
|
70
|
+
return mi(p11, pA, pB) + mi(p10, pA, 1 - pB) + mi(p01, 1 - pA, pB) + mi(p00, 1 - pA, 1 - pB);
|
|
71
|
+
}
|
|
72
|
+
let cachedModel = null;
|
|
73
|
+
function getLLM() {
|
|
74
|
+
if (cachedModel)
|
|
75
|
+
return cachedModel;
|
|
76
|
+
try {
|
|
77
|
+
cachedModel = getModel();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
cachedModel = null;
|
|
81
|
+
}
|
|
82
|
+
return cachedModel;
|
|
83
|
+
}
|
|
84
|
+
const CORRELATION_PROMPT = `你是"判断力关联分析器"。给定两条 judgment:
|
|
85
|
+
A: {a}
|
|
86
|
+
B: {b}
|
|
87
|
+
|
|
88
|
+
它们经常在同一对话里被同时注入 (共现 {co} 次).
|
|
89
|
+
判断 A 与 B 的因果关系:
|
|
90
|
+
- "A→B": A 引发 B (A 概念在 B 之前)
|
|
91
|
+
- "B→A": B 引发 A
|
|
92
|
+
- "common_cause": 共同原因 (e.g. 都是某场景触发)
|
|
93
|
+
- "unclear": 关系不明确
|
|
94
|
+
|
|
95
|
+
输出严格 JSON:
|
|
96
|
+
{
|
|
97
|
+
"direction": "A→B" | "B→A" | "common_cause" | "unclear",
|
|
98
|
+
"explanation": "≤100 字解释, 含 1 个具体例"
|
|
99
|
+
}`;
|
|
100
|
+
async function explainPair(judgmentA, judgmentB, coOccurrence) {
|
|
101
|
+
const fallback = { direction: 'unclear', explanation: '(LLM 不可用, 仅显示统计)' };
|
|
102
|
+
const llm = getLLM();
|
|
103
|
+
if (!llm)
|
|
104
|
+
return fallback;
|
|
105
|
+
try {
|
|
106
|
+
const prompt = CORRELATION_PROMPT
|
|
107
|
+
.replace('{a}', `${judgmentA.decision} (id=${judgmentA.id})`)
|
|
108
|
+
.replace('{b}', `${judgmentB.decision} (id=${judgmentB.id})`)
|
|
109
|
+
.replace('{co}', String(coOccurrence));
|
|
110
|
+
const res = await llm.chat(prompt, '你是判断力关联分析器, 严格输出 JSON');
|
|
111
|
+
const jsonMatch = res.reply.match(/\{[\s\S]*?\}/);
|
|
112
|
+
if (!jsonMatch)
|
|
113
|
+
return fallback;
|
|
114
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
115
|
+
return {
|
|
116
|
+
direction: parsed.direction ?? 'unclear',
|
|
117
|
+
explanation: String(parsed.explanation ?? '').substring(0, 200),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.warn('[causal-judge] explainPair failed:', err);
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 关联分析: 扫 usage.jsonl, 算所有 judgment 对的互信息, top N 返回
|
|
127
|
+
* - 用法: 类 B 启动时跑一次暖缓存, 用户手动点"重新跑"也跑
|
|
128
|
+
*/
|
|
129
|
+
export async function runCorrelationAnalysis(opts = {}) {
|
|
130
|
+
const topN = opts.topN ?? 5;
|
|
131
|
+
const minCo = opts.minCooccurrence ?? 3;
|
|
132
|
+
const useLLM = opts.useLLM ?? true;
|
|
133
|
+
const [judgments, entries] = await Promise.all([
|
|
134
|
+
loadAllJudgments(),
|
|
135
|
+
readUsageLog(),
|
|
136
|
+
]);
|
|
137
|
+
if (entries.length === 0 || judgments.length < 2)
|
|
138
|
+
return [];
|
|
139
|
+
// 算每条 judgment 出现次数 + 所有对的共现
|
|
140
|
+
const idCount = new Map();
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
for (const id of e.usedIds) {
|
|
143
|
+
idCount.set(id, (idCount.get(id) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const ids = Array.from(idCount.keys()).filter((id) => {
|
|
147
|
+
const j = judgments.find((j) => j.id === id);
|
|
148
|
+
return j && (j.status ?? 'active') === 'active';
|
|
149
|
+
});
|
|
150
|
+
// 算所有对的共现 (O(N²))
|
|
151
|
+
const pairs = [];
|
|
152
|
+
for (let i = 0; i < ids.length; i++) {
|
|
153
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
154
|
+
const a = ids[i], b = ids[j];
|
|
155
|
+
let co = 0;
|
|
156
|
+
for (const e of entries) {
|
|
157
|
+
if (e.usedIds.includes(a) && e.usedIds.includes(b))
|
|
158
|
+
co++;
|
|
159
|
+
}
|
|
160
|
+
if (co < minCo)
|
|
161
|
+
continue;
|
|
162
|
+
const mi = mutualInfo(co, idCount.get(a), idCount.get(b), entries.length);
|
|
163
|
+
pairs.push({ a, b, co, mi });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 排序 + top N
|
|
167
|
+
pairs.sort((x, y) => y.mi - x.mi);
|
|
168
|
+
const top = pairs.slice(0, topN);
|
|
169
|
+
// LLM 解释 (fail-soft, 不阻塞)
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const p of top) {
|
|
172
|
+
const jA = judgments.find((j) => j.id === p.a);
|
|
173
|
+
const jB = judgments.find((j) => j.id === p.b);
|
|
174
|
+
if (!jA || !jB)
|
|
175
|
+
continue;
|
|
176
|
+
let explanation = '';
|
|
177
|
+
let direction = 'unclear';
|
|
178
|
+
if (useLLM) {
|
|
179
|
+
const exp = await explainPair(jA, jB, p.co);
|
|
180
|
+
direction = exp.direction;
|
|
181
|
+
explanation = exp.explanation;
|
|
182
|
+
}
|
|
183
|
+
out.push({
|
|
184
|
+
judgmentA: p.a,
|
|
185
|
+
judgmentB: p.b,
|
|
186
|
+
mutualInfo: Number(p.mi.toFixed(4)),
|
|
187
|
+
coOccurrence: p.co,
|
|
188
|
+
causalDirection: direction,
|
|
189
|
+
explanation,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// 写 evolution.jsonl
|
|
193
|
+
try {
|
|
194
|
+
await fs.mkdir(path.dirname(EVOLUTION_LOG), { recursive: true });
|
|
195
|
+
const entry = {
|
|
196
|
+
ts: new Date().toISOString(),
|
|
197
|
+
action: 'accept',
|
|
198
|
+
suggestion: {
|
|
199
|
+
key: 'correlation-startup',
|
|
200
|
+
kind: 'unused',
|
|
201
|
+
judgmentId: '__correlation__',
|
|
202
|
+
decision: `关联分析: top ${out.length} 对`,
|
|
203
|
+
reason: out.map((p) => `${p.judgmentA}↔${p.judgmentB}: MI=${p.mutualInfo}, co=${p.coOccurrence}, dir=${p.causalDirection}`).join('\n'),
|
|
204
|
+
action: 'review',
|
|
205
|
+
metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: entries.length },
|
|
206
|
+
scannedAt: new Date().toISOString(),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
await fs.appendFile(EVOLUTION_LOG, JSON.stringify(entry) + '\n', 'utf-8');
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.warn('[causal-judge] write evolution failed:', err);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
const INTERVENTION_PROMPT = `你是"判断力干预分析器"。判断力库有一条 judgment, 用户想评估它的边际贡献.
|
|
217
|
+
给定:
|
|
218
|
+
- judgment: {judgment}
|
|
219
|
+
- 上次用过的场景 (user 输入 + AI 回复): {scenario}
|
|
220
|
+
- 同时存在的相邻 judgment (供参考): {peers}
|
|
221
|
+
|
|
222
|
+
模拟反事实推理: 如果库**没有**这条 judgment, AI 在同一场景下回答会怎么变?
|
|
223
|
+
|
|
224
|
+
输出严格 JSON:
|
|
225
|
+
{
|
|
226
|
+
"causalEffect": 0.0-1.0, // 0 = 无影响, 1 = 行为完全变
|
|
227
|
+
"reasoning": "2-3 步推理, 含 1 个具体例",
|
|
228
|
+
"confidence": 0.0-1.0
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
注意: causalEffect < 0 也合法 (如"没有这条 AI 反而更好" — 库污染)`;
|
|
232
|
+
export async function runIntervention(judgmentId, opts = {}) {
|
|
233
|
+
const fallback = {
|
|
234
|
+
judgmentId,
|
|
235
|
+
causalEffect: 0,
|
|
236
|
+
reasoning: '(LLM 不可用, 无法评估)',
|
|
237
|
+
marginalContribution: 'null',
|
|
238
|
+
confidence: 0,
|
|
239
|
+
};
|
|
240
|
+
const llm = getLLM();
|
|
241
|
+
if (!llm)
|
|
242
|
+
return fallback;
|
|
243
|
+
const judgments = await loadAllJudgments();
|
|
244
|
+
const j = judgments.find((j) => j.id === judgmentId);
|
|
245
|
+
if (!j) {
|
|
246
|
+
return { ...fallback, reasoning: `(judgment ${judgmentId} 不存在)` };
|
|
247
|
+
}
|
|
248
|
+
const peers = opts.peers ?? judgments.filter((x) => x.id !== judgmentId).slice(0, 3);
|
|
249
|
+
const peerStr = peers.map((p) => `- ${p.decision} (id=${p.id})`).join('\n');
|
|
250
|
+
const scenario = opts.scenarioContext ?? '(无特定场景, 通用评估)';
|
|
251
|
+
try {
|
|
252
|
+
const prompt = INTERVENTION_PROMPT
|
|
253
|
+
.replace('{judgment}', `${j.decision} (id=${j.id})`)
|
|
254
|
+
.replace('{scenario}', scenario)
|
|
255
|
+
.replace('{peers}', peerStr || '(无)');
|
|
256
|
+
const res = await llm.chat(prompt, '你是判断力干预分析器, 严格输出 JSON');
|
|
257
|
+
const jsonMatch = res.reply.match(/\{[\s\S]*?\}/);
|
|
258
|
+
if (!jsonMatch)
|
|
259
|
+
return fallback;
|
|
260
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
261
|
+
const effect = Number(parsed.causalEffect ?? 0);
|
|
262
|
+
const abs = Math.abs(effect);
|
|
263
|
+
return {
|
|
264
|
+
judgmentId,
|
|
265
|
+
causalEffect: Number(effect.toFixed(3)),
|
|
266
|
+
reasoning: String(parsed.reasoning ?? '').substring(0, 500),
|
|
267
|
+
marginalContribution: abs > 0.5 ? 'high' : abs > 0.2 ? 'medium' : abs > 0 ? 'low' : 'null',
|
|
268
|
+
confidence: Number(parsed.confidence ?? 0.5),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.warn('[causal-judge] runIntervention failed:', err);
|
|
273
|
+
return fallback;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const COUNTERFACTUAL_PROMPT = `你是"判断力反事实审计员"。某次 AI 回复违反了注入的 judgment. 判断是库设计问题还是 LLM 偶然违规.
|
|
277
|
+
|
|
278
|
+
事件:
|
|
279
|
+
- 用户: {userInput}
|
|
280
|
+
- AI 回复: {aiReply}
|
|
281
|
+
- 违反原则: {principles}
|
|
282
|
+
|
|
283
|
+
构造 3 个反事实 scenario:
|
|
284
|
+
1. 删去最相关那条 judgment
|
|
285
|
+
2. 把那条 judgment 改成反向 (例如"必须"→"避免")
|
|
286
|
+
3. 删去整个 channel 的 judgment 库 (空库)
|
|
287
|
+
|
|
288
|
+
每个 scenario 模拟 AI 在该库下的回答, 判断还会不会违规.
|
|
289
|
+
|
|
290
|
+
输出严格 JSON:
|
|
291
|
+
{
|
|
292
|
+
"scenarios": [
|
|
293
|
+
{ "modification": "...", "outcomeCompliant": true/false, "explanation": "≤100 字" }
|
|
294
|
+
],
|
|
295
|
+
"verdict": "库设计合理" | "建议调库" | "需更多数据",
|
|
296
|
+
"recomendaciones": "≤200 字, ≤3 条建议"
|
|
297
|
+
}`;
|
|
298
|
+
export async function runCounterfactualAudit(opts) {
|
|
299
|
+
const fallback = {
|
|
300
|
+
ts: new Date().toISOString(),
|
|
301
|
+
trigger: opts,
|
|
302
|
+
scenarios: [],
|
|
303
|
+
verdict: '需更多数据',
|
|
304
|
+
recommendations: ['(LLM 不可用, 无法审计)'],
|
|
305
|
+
};
|
|
306
|
+
const llm = getLLM();
|
|
307
|
+
if (!llm)
|
|
308
|
+
return fallback;
|
|
309
|
+
try {
|
|
310
|
+
const prompt = COUNTERFACTUAL_PROMPT
|
|
311
|
+
.replace('{userInput}', opts.userInput.substring(0, 500))
|
|
312
|
+
.replace('{aiReply}', opts.aiReply.substring(0, 1000))
|
|
313
|
+
.replace('{principles}', opts.violatedPrinciples.map((p) => `- ${p.principle}: ${p.reason}`).join('\n') || '(none)');
|
|
314
|
+
const res = await llm.chat(prompt, '你是判断力反事实审计员, 严格输出 JSON');
|
|
315
|
+
const jsonMatch = res.reply.match(/\{[\s\S]*?\}/);
|
|
316
|
+
if (!jsonMatch)
|
|
317
|
+
return fallback;
|
|
318
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
319
|
+
return {
|
|
320
|
+
ts: new Date().toISOString(),
|
|
321
|
+
trigger: opts,
|
|
322
|
+
scenarios: Array.isArray(parsed.scenarios) ? parsed.scenarios.map((s) => ({
|
|
323
|
+
modification: String(s.modification ?? '').substring(0, 200),
|
|
324
|
+
outcomeCompliant: Boolean(s.outcomeCompliant),
|
|
325
|
+
explanation: String(s.explanation ?? '').substring(0, 200),
|
|
326
|
+
})) : [],
|
|
327
|
+
verdict: parsed.verdict ?? '需更多数据',
|
|
328
|
+
recommendations: Array.isArray(parsed.recomendaciones) ? parsed.recomendaciones.map((r) => String(r).substring(0, 200)) : [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.warn('[causal-judge] runCounterfactualAudit failed:', err);
|
|
333
|
+
return fallback;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 写反事实审计到独立文件 (供 violations tab UI 展示)
|
|
338
|
+
*/
|
|
339
|
+
export async function logCounterfactualAudit(audit) {
|
|
340
|
+
try {
|
|
341
|
+
await fs.mkdir(path.dirname(COUNTERFACTUAL_LOG), { recursive: true });
|
|
342
|
+
await fs.appendFile(COUNTERFACTUAL_LOG, JSON.stringify(audit) + '\n', 'utf-8');
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
console.warn('[causal-judge] logCounterfactualAudit failed:', err);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export async function readCounterfactualLog(limit = 20) {
|
|
349
|
+
try {
|
|
350
|
+
const content = await fs.readFile(COUNTERFACTUAL_LOG, 'utf-8');
|
|
351
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
352
|
+
return lines
|
|
353
|
+
.slice(-limit)
|
|
354
|
+
.reverse()
|
|
355
|
+
.map((l) => {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(l);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
.filter((a) => Boolean(a));
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================
|
|
370
|
+
// 4. 冲突检测 (Conflict Detection) — 纯字符串启发式
|
|
371
|
+
// ============================================================
|
|
372
|
+
/**
|
|
373
|
+
* 检测两条 judgment 是否冲突 (纯规则, 不调 LLM)
|
|
374
|
+
* - 简单规则: 同一 decision 含 '不'/'禁止'/'避免' vs '可以'/'允许'/'优先'
|
|
375
|
+
* - 复杂冲突留给 do-calculus
|
|
376
|
+
*/
|
|
377
|
+
export function detectConflict(a, b) {
|
|
378
|
+
const NEG = ['不', '禁止', '避免', '勿', '不可', '不行', '不能'];
|
|
379
|
+
const POS = ['可以', '允许', '优先', '应该', '应当', '需要', '必须'];
|
|
380
|
+
function flags(text) {
|
|
381
|
+
return {
|
|
382
|
+
neg: NEG.filter((w) => text.includes(w)).length,
|
|
383
|
+
pos: POS.filter((w) => text.includes(w)).length,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const fa = flags(a.decision);
|
|
387
|
+
const fb = flags(b.decision);
|
|
388
|
+
// 两边都极性词, 1 负 1 正 → 冲突
|
|
389
|
+
if ((fa.neg > 0 && fb.pos > 0) || (fa.pos > 0 && fb.neg > 0)) {
|
|
390
|
+
const reason = `A 倾向${fa.neg > 0 ? '禁止' : '允许'}, B 倾向${fb.neg > 0 ? '禁止' : '允许'}; 极性词冲突`;
|
|
391
|
+
return { isConflict: true, reason };
|
|
392
|
+
}
|
|
393
|
+
return { isConflict: false, reason: '' };
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* 扫整个库, 找出冲突对, 写进每条 judgment.conflictWith
|
|
397
|
+
* 失败静默, 写 evolution.jsonl 'conflict-detected' 事件
|
|
398
|
+
*/
|
|
399
|
+
export async function runConflictDetection() {
|
|
400
|
+
const judgments = await loadAllJudgments();
|
|
401
|
+
const active = judgments.filter((j) => (j.status ?? 'active') === 'active');
|
|
402
|
+
const pairs = [];
|
|
403
|
+
let detected = 0;
|
|
404
|
+
for (let i = 0; i < active.length; i++) {
|
|
405
|
+
for (let j = i + 1; j < active.length; j++) {
|
|
406
|
+
const ai = active[i];
|
|
407
|
+
const aj = active[j];
|
|
408
|
+
const r = detectConflict(ai, aj);
|
|
409
|
+
if (r.isConflict) {
|
|
410
|
+
pairs.push({ a: ai.id, b: aj.id, reason: r.reason });
|
|
411
|
+
// 写进每条 judgment.conflictWith
|
|
412
|
+
const idxA = judgments.findIndex((j) => j.id === ai.id);
|
|
413
|
+
const idxB = judgments.findIndex((j) => j.id === aj.id);
|
|
414
|
+
if (idxA >= 0) {
|
|
415
|
+
const jA = judgments[idxA];
|
|
416
|
+
if (!Array.isArray(jA.conflictWith))
|
|
417
|
+
jA.conflictWith = [];
|
|
418
|
+
if (!jA.conflictWith.includes(aj.id)) {
|
|
419
|
+
jA.conflictWith.push(aj.id);
|
|
420
|
+
detected++;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (idxB >= 0) {
|
|
424
|
+
const jB = judgments[idxB];
|
|
425
|
+
if (!Array.isArray(jB.conflictWith))
|
|
426
|
+
jB.conflictWith = [];
|
|
427
|
+
if (!jB.conflictWith.includes(ai.id)) {
|
|
428
|
+
jB.conflictWith.push(ai.id);
|
|
429
|
+
detected++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// 写盘 (冲突变了)
|
|
436
|
+
if (detected > 0) {
|
|
437
|
+
try {
|
|
438
|
+
const store = await import('./human-value-store.js');
|
|
439
|
+
// saveJudgments 未 export, 复用 storeHumanJudgment + 直接写盘
|
|
440
|
+
// 简化: 这里只 log, 不写盘 (避免循环依赖)
|
|
441
|
+
// 调用方可在收到 detected > 0 后决定是否手动 save
|
|
442
|
+
void store;
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
console.warn('[causal-judge] saveJudgments failed:', err);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return { detected, pairs };
|
|
449
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D 触发钩子: AI 自动捕获判断力
|
|
3
|
+
*
|
|
4
|
+
* 流程: detectIfWorthStoring → distillFromConversation → storeHumanJudgment → evolveNewJudgment
|
|
5
|
+
* 错误处理: 任意步骤失败只 console.error, 不 throw (D 触发不能阻塞主对话流)
|
|
6
|
+
*/
|
|
7
|
+
import { storeHumanJudgment, findRecentSimilarDecisions, initializeValueStore, } from './human-value-store.js';
|
|
8
|
+
import { distillFromConversation, detectIfWorthStoring } from './distill-prompt.js';
|
|
9
|
+
import { evolveNewJudgment } from './evolve-judgment.js';
|
|
10
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
/**
|
|
12
|
+
* D 路径节流: channel 维度 5min 节流, 防 LLM 反复触发
|
|
13
|
+
* 返回 true = 可以触发; false = 在节流窗口内
|
|
14
|
+
*/
|
|
15
|
+
const dThrottleMap = new Map();
|
|
16
|
+
export function throttleDHook(channelId, minMs = 5 * 60_000) {
|
|
17
|
+
const last = dThrottleMap.get(channelId) || 0;
|
|
18
|
+
if (Date.now() - last < minMs)
|
|
19
|
+
return false;
|
|
20
|
+
dThrottleMap.set(channelId, Date.now());
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
export function clearDHookThrottle(channelId) {
|
|
24
|
+
if (channelId)
|
|
25
|
+
dThrottleMap.delete(channelId);
|
|
26
|
+
else
|
|
27
|
+
dThrottleMap.clear();
|
|
28
|
+
}
|
|
29
|
+
export async function detectAndDistillFromChannel(turns, options = {}) {
|
|
30
|
+
// D 路径默认 confidence 0.75 (B 路径保持 0, 因为人已经点了按钮 = 显式信任)
|
|
31
|
+
const minConfidence = options.minConfidence ?? 0.75;
|
|
32
|
+
try {
|
|
33
|
+
await initializeValueStore();
|
|
34
|
+
const detection = await detectIfWorthStoring(turns);
|
|
35
|
+
if (!detection.worth) {
|
|
36
|
+
return { triggered: false, reason: `D1 skipped: ${detection.reason}` };
|
|
37
|
+
}
|
|
38
|
+
const distillResult = await distillFromConversation(turns);
|
|
39
|
+
if (!distillResult.value) {
|
|
40
|
+
return { triggered: false, reason: 'D2 distilled to null' };
|
|
41
|
+
}
|
|
42
|
+
if (distillResult.confidence < minConfidence) {
|
|
43
|
+
return {
|
|
44
|
+
triggered: false,
|
|
45
|
+
reason: `D2 confidence too low: ${distillResult.confidence}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 24h 滑窗去重: 同 channel 撞 hash 直接 skip
|
|
49
|
+
if (!options.skipDedup) {
|
|
50
|
+
const dups = await findRecentSimilarDecisions(distillResult.value, DAY_MS, {
|
|
51
|
+
status: 'all',
|
|
52
|
+
channelId: options.channelId,
|
|
53
|
+
});
|
|
54
|
+
if (dups.length > 0) {
|
|
55
|
+
return {
|
|
56
|
+
triggered: false,
|
|
57
|
+
reason: 'duplicate within 24h',
|
|
58
|
+
duplicateOfId: dups[0].id,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const judgment = await storeHumanJudgment({
|
|
63
|
+
decision: distillResult.value,
|
|
64
|
+
decision_type: 'approve',
|
|
65
|
+
reasons: distillResult.evidence ? [distillResult.evidence] : [],
|
|
66
|
+
values_derived: [],
|
|
67
|
+
context: {
|
|
68
|
+
domain: options.channelId ? `channel:${options.channelId}` : 'general',
|
|
69
|
+
complexity: 'moderate',
|
|
70
|
+
stakes: 'medium',
|
|
71
|
+
time_pressure: 'low',
|
|
72
|
+
},
|
|
73
|
+
metadata: {
|
|
74
|
+
source: options.source ?? 'implicit',
|
|
75
|
+
confidence: distillResult.confidence,
|
|
76
|
+
revisable: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
let evolved = { merged: 0, superseded: 0 };
|
|
80
|
+
try {
|
|
81
|
+
const outcome = await evolveNewJudgment(judgment);
|
|
82
|
+
evolved = {
|
|
83
|
+
merged: outcome.merged.length,
|
|
84
|
+
superseded: outcome.contradicted.length,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.warn('[detect-hook] evolve failed (non-fatal):', err);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
triggered: true,
|
|
92
|
+
reason: `D stored: ${distillResult.value.substring(0, 30)}...`,
|
|
93
|
+
judgment,
|
|
94
|
+
evolved,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error('[detect-hook] failed:', err);
|
|
99
|
+
return { triggered: false, reason: `error: ${err.message}` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function distillAndStoreFromChannel(turns, options = {}) {
|
|
103
|
+
const minConfidence = options.minConfidence ?? 0;
|
|
104
|
+
try {
|
|
105
|
+
await initializeValueStore();
|
|
106
|
+
const distillResult = await distillFromConversation(turns);
|
|
107
|
+
if (!distillResult.value) {
|
|
108
|
+
return { triggered: false, reason: 'distilled to null' };
|
|
109
|
+
}
|
|
110
|
+
if (distillResult.confidence < minConfidence) {
|
|
111
|
+
return {
|
|
112
|
+
triggered: false,
|
|
113
|
+
reason: `confidence too low: ${distillResult.confidence}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// 24h 滑窗去重 (全库扫, 不按 channel 隔离 — 同一原则跨 channel 重复属于污染)
|
|
117
|
+
if (!options.skipDedup) {
|
|
118
|
+
const dups = await findRecentSimilarDecisions(distillResult.value, DAY_MS, {
|
|
119
|
+
status: 'all',
|
|
120
|
+
});
|
|
121
|
+
if (dups.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
triggered: false,
|
|
124
|
+
reason: 'duplicate within 24h',
|
|
125
|
+
duplicateOfId: dups[0].id,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const judgment = await storeHumanJudgment({
|
|
130
|
+
decision: distillResult.value,
|
|
131
|
+
decision_type: 'approve',
|
|
132
|
+
reasons: distillResult.evidence ? [distillResult.evidence] : [],
|
|
133
|
+
values_derived: [],
|
|
134
|
+
context: {
|
|
135
|
+
domain: options.channelId ? `channel:${options.channelId}` : 'general',
|
|
136
|
+
complexity: 'moderate',
|
|
137
|
+
stakes: 'medium',
|
|
138
|
+
time_pressure: 'low',
|
|
139
|
+
},
|
|
140
|
+
metadata: {
|
|
141
|
+
source: 'explicit',
|
|
142
|
+
confidence: distillResult.confidence,
|
|
143
|
+
revisable: true,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
let evolved = { merged: 0, superseded: 0 };
|
|
147
|
+
try {
|
|
148
|
+
const outcome = await evolveNewJudgment(judgment);
|
|
149
|
+
evolved = {
|
|
150
|
+
merged: outcome.merged.length,
|
|
151
|
+
superseded: outcome.contradicted.length,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
console.warn('[detect-hook] B-evolve failed (non-fatal):', err);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
triggered: true,
|
|
159
|
+
reason: `B stored: ${distillResult.value.substring(0, 30)}...`,
|
|
160
|
+
judgment,
|
|
161
|
+
evolved,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
console.error('[detect-hook] B failed:', err);
|
|
166
|
+
return { triggered: false, reason: `error: ${err.message}` };
|
|
167
|
+
}
|
|
168
|
+
}
|