@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.
Files changed (60) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/Bolloon.md +103 -0
  4. package/dist/agents/pi-sdk.js +264 -12
  5. package/dist/bootstrap/bootstrap.js +114 -0
  6. package/dist/bootstrap/context-collector.js +296 -0
  7. package/dist/bootstrap/lifecycle-hooks.js +109 -0
  8. package/dist/bootstrap/project-context.js +151 -0
  9. package/dist/index.js +11 -0
  10. package/dist/llm/pi-ai.js +31 -21
  11. package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
  12. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  13. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  14. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  15. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  16. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  17. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  18. package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
  19. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  20. package/dist/security/builtin-guards.js +124 -0
  21. package/dist/security/context-router-tool.js +106 -0
  22. package/dist/security/react-harness.js +143 -0
  23. package/dist/security/tool-gate.js +235 -0
  24. package/dist/utils/auto-evolve-policy.js +117 -0
  25. package/dist/utils/clamp.js +7 -0
  26. package/dist/utils/double.js +6 -0
  27. package/dist/web/client.js +668 -204
  28. package/dist/web/index.html +24 -4
  29. package/dist/web/server.js +531 -10
  30. package/lefthook.yml +29 -0
  31. package/package.json +3 -2
  32. package/scripts/auto-evolve-loop.ts +376 -0
  33. package/scripts/auto-evolve-oneshot.sh +155 -0
  34. package/scripts/auto-evolve-snapshot.sh +136 -0
  35. package/scripts/detect-schema-changes.sh +48 -0
  36. package/scripts/diff-reviewer.ts +159 -0
  37. package/scripts/weekly-report.ts +364 -0
  38. package/src/agents/pi-sdk.ts +293 -15
  39. package/src/bootstrap/bootstrap.ts +132 -0
  40. package/src/bootstrap/context-collector.ts +342 -0
  41. package/src/bootstrap/lifecycle-hooks.ts +176 -0
  42. package/src/bootstrap/project-context.ts +163 -0
  43. package/src/index.ts +11 -0
  44. package/src/llm/pi-ai.ts +33 -22
  45. package/src/security/builtin-guards.ts +162 -0
  46. package/src/security/context-router-tool.ts +122 -0
  47. package/src/security/react-harness.ts +177 -0
  48. package/src/security/tool-gate.ts +294 -0
  49. package/src/utils/auto-evolve-policy.ts +138 -0
  50. package/src/utils/clamp.ts +5 -0
  51. package/src/web/client.js +668 -204
  52. package/src/web/index.html +24 -4
  53. package/src/web/server.ts +596 -10
  54. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  55. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  56. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  57. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  58. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  59. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  60. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Distill & Evolve Prompts
3
+ * - 蒸馏: 从对话片段提取 1 条 30-80 字的判断力
4
+ * - 演化对齐: 判定新判断力与已有判断力的关系 (merge/contradict/unrelated)
5
+ */
6
+ import { getModel } from '../llm/pi-ai.js';
7
+ const DISTILL_SYSTEM_PROMPT = `你是一个"判断力蒸馏器"。从给定的对话片段中,提取出**一条**可以长期复用的判断力原则。
8
+
9
+ 要求:
10
+ - 长度 30-80 字(中文字符),软目标 50 字
11
+ - 用陈述句,不要"我觉得/我想要"这种主观前缀
12
+ - 要有可操作性,能被未来的自己/AI 引用
13
+ - 如果对话里没有值得提炼的判断力,返回 {"value": null}
14
+
15
+ 输出格式(严格 JSON,无其他文字):
16
+ {
17
+ "value": "<30-80 字的判断力>" | null,
18
+ "category": "rule" | "preference" | "trajectory" | "reward",
19
+ "confidence": 0.0-1.0,
20
+ "evidence": "<原文中支持这句话的关键句,不超过 30 字>"
21
+ }`;
22
+ const EVOLVE_SYSTEM_PROMPT = `你是"判断力演化对齐器"。对比一条新判断力与已有判断力,判定关系。
23
+
24
+ 新判断力:
25
+ {new_value}
26
+
27
+ 已有判断力列表(最多 10 条):
28
+ {existing_list}
29
+
30
+ 对每条已有判断力,判定:
31
+ - "merge": 新判断力是这条的更新版/具体版/纠正版 → 旧条应被 superseded
32
+ - "contradict": 新判断力与这条方向相反(例如"保守"vs"激进") → 旧条应被 superseded
33
+ - "unrelated": 两者主题不同,各自保留
34
+
35
+ 输出严格 JSON:
36
+ {
37
+ "relations": [
38
+ {"id": "...", "relation": "merge" | "contradict" | "unrelated", "reason": "≤20 字"}
39
+ ]
40
+ }`;
41
+ let cachedModel = null;
42
+ function getDistillModel() {
43
+ if (cachedModel)
44
+ return cachedModel;
45
+ try {
46
+ cachedModel = getModel();
47
+ }
48
+ catch {
49
+ cachedModel = null;
50
+ }
51
+ return cachedModel;
52
+ }
53
+ export async function detectIfWorthStoring(turns) {
54
+ const fallback = { worth: false, reason: 'llm-unavailable' };
55
+ const model = getDistillModel();
56
+ if (!model)
57
+ return fallback;
58
+ const conversation = formatTurns(turns);
59
+ const prompt = `请判断以下对话是否包含"值得长期保留的判断力"(用户的偏好/规则/原则/价值观)。
60
+ 如果包含,返回 {"worth": true, "reason": "≤20 字理由"}
61
+ 如果不包含,返回 {"worth": false, "reason": "≤20 字理由"}
62
+
63
+ 对话:
64
+ ${conversation}
65
+
66
+ 输出严格 JSON:`;
67
+ try {
68
+ const res = await withRetry(() => model.chat(prompt, '判断力检测专家'));
69
+ return parseDetectResponse(res.reply, fallback);
70
+ }
71
+ catch (err) {
72
+ console.warn('[distill-prompt] detectIfWorthStoring failed:', err);
73
+ return fallback;
74
+ }
75
+ }
76
+ export async function distillFromConversation(turns) {
77
+ const fallback = {
78
+ value: null,
79
+ category: 'preference',
80
+ confidence: 0,
81
+ evidence: null,
82
+ };
83
+ const model = getDistillModel();
84
+ if (!model)
85
+ return fallback;
86
+ const conversation = formatTurns(turns);
87
+ const userPrompt = `对话上下文(最多 10 轮):
88
+ ${conversation}
89
+
90
+ 输出:`;
91
+ try {
92
+ const res = await withRetry(() => model.chat(userPrompt, DISTILL_SYSTEM_PROMPT));
93
+ return parseDistillResponse(res.reply, fallback);
94
+ }
95
+ catch (err) {
96
+ console.warn('[distill-prompt] distillFromConversation failed:', err);
97
+ return fallback;
98
+ }
99
+ }
100
+ export async function evolveWithLLM(newValue, existing) {
101
+ const fallback = { relations: [] };
102
+ const model = getDistillModel();
103
+ if (!model)
104
+ return fallback;
105
+ if (existing.length === 0)
106
+ return fallback;
107
+ const existingList = existing
108
+ .map((e, i) => `${i + 1}. [id="${e.id}"] ${e.value}`)
109
+ .join('\n');
110
+ const systemPrompt = EVOLVE_SYSTEM_PROMPT
111
+ .replace('{new_value}', newValue)
112
+ .replace('{existing_list}', existingList);
113
+ const userPrompt = '请输出 JSON:';
114
+ try {
115
+ const res = await withRetry(() => model.chat(userPrompt, systemPrompt));
116
+ return parseEvolveResponse(res.reply, existing, fallback);
117
+ }
118
+ catch (err) {
119
+ console.warn('[distill-prompt] evolveWithLLM failed:', err);
120
+ return fallback;
121
+ }
122
+ }
123
+ function formatTurns(turns) {
124
+ return turns
125
+ .slice(-10)
126
+ .map((t) => {
127
+ const role = t.role === 'human' ? '用户' : 'AI';
128
+ return `${role}: ${t.content}`;
129
+ })
130
+ .join('\n');
131
+ }
132
+ async function withRetry(fn, maxRetries = 2) {
133
+ let lastErr;
134
+ for (let i = 0; i <= maxRetries; i++) {
135
+ try {
136
+ return await fn();
137
+ }
138
+ catch (err) {
139
+ lastErr = err;
140
+ if (i < maxRetries) {
141
+ await new Promise((r) => setTimeout(r, 500 * Math.pow(2, i)));
142
+ }
143
+ }
144
+ }
145
+ throw lastErr;
146
+ }
147
+ function extractJson(text) {
148
+ const codeBlock = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
149
+ if (codeBlock) {
150
+ try {
151
+ return JSON.parse(codeBlock[1]);
152
+ }
153
+ catch { /* fall through */ }
154
+ }
155
+ const first = text.indexOf('{');
156
+ const last = text.lastIndexOf('}');
157
+ if (first >= 0 && last > first) {
158
+ try {
159
+ return JSON.parse(text.substring(first, last + 1));
160
+ }
161
+ catch { /* fall through */ }
162
+ }
163
+ return null;
164
+ }
165
+ function parseDistillResponse(reply, fallback) {
166
+ const json = extractJson(reply);
167
+ if (!json || typeof json !== 'object')
168
+ return fallback;
169
+ const obj = json;
170
+ let value = typeof obj.value === 'string' ? obj.value.trim() : null;
171
+ if (value === null || value === '')
172
+ return { ...fallback, value: null };
173
+ if (value.length > 80)
174
+ value = value.substring(0, 80);
175
+ // < 20 字直接拒 (5 个中文字以下基本是噪声)
176
+ if (value.length < 20)
177
+ return { ...fallback, value: null };
178
+ // 20-30 字区间: 尝试补句号, 仍 < 20 才拒
179
+ if (value.length < 30 && !/[。.!!??]$/.test(value)) {
180
+ value = value + '。';
181
+ if (value.length < 20)
182
+ return { ...fallback, value: null };
183
+ }
184
+ const category = ['rule', 'preference', 'trajectory', 'reward'].includes(String(obj.category))
185
+ ? obj.category
186
+ : 'preference';
187
+ const confidence = Math.max(0, Math.min(1, Number(obj.confidence) || 0.7));
188
+ const evidence = typeof obj.evidence === 'string' && obj.evidence.trim()
189
+ ? obj.evidence.trim().substring(0, 30)
190
+ : null;
191
+ return { value, category, confidence, evidence };
192
+ }
193
+ function parseDetectResponse(reply, fallback) {
194
+ const json = extractJson(reply);
195
+ if (!json || typeof json !== 'object')
196
+ return fallback;
197
+ const obj = json;
198
+ const worth = Boolean(obj.worth);
199
+ const reason = typeof obj.reason === 'string' ? obj.reason.substring(0, 20) : '';
200
+ return { worth, reason };
201
+ }
202
+ function parseEvolveResponse(reply, existing, fallback) {
203
+ const json = extractJson(reply);
204
+ if (!json || typeof json !== 'object')
205
+ return fallback;
206
+ const obj = json;
207
+ const relations = obj.relations;
208
+ if (!Array.isArray(relations))
209
+ return fallback;
210
+ const existingIds = new Set(existing.map((e) => e.id));
211
+ const validRelations = [];
212
+ for (const r of relations) {
213
+ if (!r || typeof r !== 'object')
214
+ continue;
215
+ const item = r;
216
+ const id = typeof item.id === 'string' ? item.id : '';
217
+ if (!existingIds.has(id))
218
+ continue;
219
+ const relation = item.relation;
220
+ if (relation !== 'merge' && relation !== 'contradict' && relation !== 'unrelated')
221
+ continue;
222
+ const reason = typeof item.reason === 'string' ? item.reason.substring(0, 20) : '';
223
+ validRelations.push({ id, relation, reason });
224
+ }
225
+ return { relations: validRelations };
226
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * 演化对齐 (Evolution Alignment)
3
+ *
4
+ * 算法流程:
5
+ * 1. 取所有 status='active' 的判断力
6
+ * 2. 算 Jaccard 字符级相似度:
7
+ * - > 0.85: 直接 merge (不调 LLM)
8
+ * - < 0.3: 视为 unrelated (不调 LLM)
9
+ * - 0.3-0.85: 收集进 LLM 候选,按相似度 Top 10 送 LLM
10
+ * 3. LLM 判定 merge/contradict/unrelated
11
+ * 4. 应用结果: 旧条 status='superseded', supersededBy=newJudgment.id, evolutionReason, evolvedAt
12
+ *
13
+ * 防抖: 同 channel 1 分钟内重复调用直接 return
14
+ */
15
+ import { batchUpdateJudgments, listJudgmentsByStatus, } from './human-value-store.js';
16
+ import { evolveWithLLM } from './distill-prompt.js';
17
+ const debounceMap = new Map();
18
+ const DEBOUNCE_MS = 60_000;
19
+ export async function evolveNewJudgment(newJudgment, options = {}) {
20
+ const maxLLMCompare = options.maxLLMCompare ?? 10;
21
+ const highTh = options.jaccardHighThreshold ?? 0.85;
22
+ const lowTh = options.jaccardLowThreshold ?? 0.3;
23
+ const channelId = newJudgment.context?.channelId;
24
+ if (!options.skipDebounce && channelId) {
25
+ const last = debounceMap.get(channelId) || 0;
26
+ if (Date.now() - last < DEBOUNCE_MS) {
27
+ return {
28
+ newJudgment,
29
+ merged: [],
30
+ contradicted: [],
31
+ unrelated: 0,
32
+ llmCompared: 0,
33
+ };
34
+ }
35
+ debounceMap.set(channelId, Date.now());
36
+ }
37
+ const allActive = await listJudgmentsByStatus('active');
38
+ const candidates = allActive.filter((j) => j.id !== newJudgment.id);
39
+ const newValue = newJudgment.decision;
40
+ const merged = [];
41
+ const llmCandidates = [];
42
+ let unrelatedCount = 0;
43
+ for (const old of candidates) {
44
+ const j = jaccardSimilarity(newValue, old.decision);
45
+ if (j > highTh) {
46
+ merged.push(old);
47
+ }
48
+ else if (j >= lowTh) {
49
+ llmCandidates.push({ id: old.id, value: old.decision, jaccard: j });
50
+ }
51
+ else {
52
+ unrelatedCount++;
53
+ }
54
+ }
55
+ llmCandidates.sort((a, b) => b.jaccard - a.jaccard);
56
+ const llmBatch = llmCandidates.slice(0, maxLLMCompare);
57
+ let contradicted = [];
58
+ if (llmBatch.length > 0) {
59
+ try {
60
+ const llmResult = await evolveWithLLM(newValue, llmBatch.map((c) => ({ id: c.id, value: c.value })));
61
+ const contradictedIds = new Set();
62
+ for (const rel of llmResult.relations) {
63
+ if (rel.relation === 'contradict') {
64
+ contradictedIds.add(rel.id);
65
+ }
66
+ if (rel.relation === 'merge' || rel.relation === 'contradict') {
67
+ const old = candidates.find((c) => c.id === rel.id);
68
+ if (old && !merged.find((m) => m.id === rel.id)) {
69
+ merged.push(old);
70
+ }
71
+ }
72
+ }
73
+ const now = new Date().toISOString();
74
+ const toSupersede = [];
75
+ for (const old of merged) {
76
+ const reason = contradictedIds.has(old.id) ? 'contradicted' : 'merged';
77
+ toSupersede.push({
78
+ ...old,
79
+ status: 'superseded',
80
+ supersededBy: newJudgment.id,
81
+ evolutionReason: reason,
82
+ evolvedAt: now,
83
+ });
84
+ }
85
+ contradicted = candidates.filter((c) => contradictedIds.has(c.id));
86
+ if (toSupersede.length > 0) {
87
+ await batchUpdateJudgments(toSupersede.map((j) => ({
88
+ id: j.id,
89
+ patch: {
90
+ status: 'superseded',
91
+ supersededBy: newJudgment.id,
92
+ evolutionReason: j.evolutionReason,
93
+ evolvedAt: j.evolvedAt,
94
+ },
95
+ })));
96
+ }
97
+ }
98
+ catch (err) {
99
+ console.warn('[evolve-judgment] LLM evolve failed, keep high-jaccard merges only:', err);
100
+ const now = new Date().toISOString();
101
+ if (merged.length > 0) {
102
+ await batchUpdateJudgments(merged.map((j) => ({
103
+ id: j.id,
104
+ patch: {
105
+ status: 'superseded',
106
+ supersededBy: newJudgment.id,
107
+ evolutionReason: 'merged',
108
+ evolvedAt: now,
109
+ },
110
+ })));
111
+ }
112
+ }
113
+ }
114
+ else if (merged.length > 0) {
115
+ const now = new Date().toISOString();
116
+ await batchUpdateJudgments(merged.map((j) => ({
117
+ id: j.id,
118
+ patch: {
119
+ status: 'superseded',
120
+ supersededBy: newJudgment.id,
121
+ evolutionReason: 'merged',
122
+ evolvedAt: now,
123
+ },
124
+ })));
125
+ }
126
+ return {
127
+ newJudgment,
128
+ merged,
129
+ contradicted,
130
+ unrelated: unrelatedCount,
131
+ llmCompared: llmBatch.length,
132
+ };
133
+ }
134
+ export function jaccardSimilarity(a, b) {
135
+ if (!a || !b)
136
+ return 0;
137
+ const normalize = (s) => s
138
+ .toLowerCase()
139
+ .replace(/[\s,.,。、!?!?""''()()::;;\-—_]/g, '')
140
+ .trim();
141
+ const textA = normalize(a);
142
+ const textB = normalize(b);
143
+ if (textA.length === 0 || textB.length === 0)
144
+ return 0;
145
+ // 短句 (< 8 字符): 字符级 bigram, 防止"先校验再写"vs"写入前先校验"全 0
146
+ // 长句: 单字 set, 避免 bigram 集合爆炸
147
+ const grams = (s) => {
148
+ if (s.length < 8) {
149
+ const out = new Set();
150
+ for (let i = 0; i < s.length - 1; i++)
151
+ out.add(s.slice(i, i + 2));
152
+ for (const c of s)
153
+ out.add(c);
154
+ return out;
155
+ }
156
+ return new Set(s);
157
+ };
158
+ const setA = grams(textA);
159
+ const setB = grams(textB);
160
+ let inter = 0;
161
+ for (const c of setA) {
162
+ if (setB.has(c))
163
+ inter++;
164
+ }
165
+ const union = setA.size + setB.size - inter;
166
+ return union === 0 ? 0 : inter / union;
167
+ }
168
+ export function clearEvolveDebounce() {
169
+ debounceMap.clear();
170
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 判断力自动入库 + AI 演化 统一入口
3
+ *
4
+ * 把新模块 (distill-prompt, evolve-judgment, detect-hook) 集中导出
5
+ * 给 server.ts 和前端调用方一个稳定的入口
6
+ *
7
+ * 旧的 human-value-store.ts (JSON 存储) 和 index.ts (YAML 存储) 不动
8
+ */
9
+ export { detectIfWorthStoring, distillFromConversation, evolveWithLLM, } from './distill-prompt.js';
10
+ export { evolveNewJudgment, jaccardSimilarity, clearEvolveDebounce, } from './evolve-judgment.js';
11
+ export { detectAndDistillFromChannel, distillAndStoreFromChannel, throttleDHook, clearDHookThrottle, } from './detect-hook.js';
12
+ export { injectJudgmentGate, recordJudgmentUsage, getRecentUsage, chatWithJudgmentGate, DEFAULT_INJECTION_CONFIG, } from './injection-gate.js';
13
+ export { checkCompliance, monitorAfterReply, logViolation, getRecentViolations, } from './monitor-gate.js';
14
+ export { runAdaptiveScan, getCachedScan, clearAdaptiveScanCache, logEvolution, readEvolutionLog, } from './adaptive-scan.js';
15
+ // Causal-judge 引擎 (阶段 2)
16
+ export { runCorrelationAnalysis, runIntervention, runCounterfactualAudit, logCounterfactualAudit, readCounterfactualLog, runConflictDetection, detectConflict, } from './causal-judge.js';
17
+ // Bootstrap 入口 (re-export 给 index.ts / server.ts 用)
18
+ export { bootstrapBolloon, } from '../bootstrap/bootstrap.js';
19
+ export { onSessionStart, onStop, onPreToolUse, clearSessionStartCache, } from '../bootstrap/lifecycle-hooks.js';
20
+ export { collectBolloonContext, getCachedBolloonContext, clearBolloonContextCache, } from '../bootstrap/context-collector.js';
21
+ export { formatContextForSystemPrompt, } from '../bootstrap/project-context.js';