@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
package/dist/llm/pi-ai.js CHANGED
@@ -7,7 +7,7 @@ export class PiAIModel {
7
7
  this.config = config;
8
8
  this.provider = config.provider;
9
9
  }
10
- async chat(message, context) {
10
+ async chat(message, context, signal) {
11
11
  const systemPrompt = this.buildSystemPrompt(context);
12
12
  const messages = [
13
13
  { role: 'system', content: systemPrompt },
@@ -16,11 +16,16 @@ export class PiAIModel {
16
16
  try {
17
17
  const response = await this.generateText({
18
18
  messages,
19
- temperature: 0.8
19
+ temperature: 0.8,
20
+ signal,
20
21
  });
21
22
  return { reply: response };
22
23
  }
23
24
  catch (error) {
25
+ // abort 不当作错误, 透传一个 sentinel 让上层能识别
26
+ if (signal?.aborted || error?.name === 'AbortError') {
27
+ throw error; // 上层 try/catch 处理
28
+ }
24
29
  console.error('PiAI chat error:', error);
25
30
  return { reply: '抱歉,AI服务暂时不可用。' };
26
31
  }
@@ -64,7 +69,7 @@ export class PiAIModel {
64
69
  }
65
70
  }
66
71
  async generateText(options) {
67
- const { messages, temperature = 0.7, maxTokens = 4096 } = options;
72
+ const { messages, temperature = 0.7, maxTokens = 4096, signal } = options;
68
73
  switch (this.provider) {
69
74
  case 'openai':
70
75
  case 'minimax':
@@ -72,17 +77,17 @@ export class PiAIModel {
72
77
  case 'kimi':
73
78
  case 'glm':
74
79
  case 'qwen':
75
- return this.callOpenAI(messages, temperature, maxTokens);
80
+ return this.callOpenAI(messages, temperature, maxTokens, signal);
76
81
  case 'anthropic':
77
- return this.callAnthropic(messages, temperature, maxTokens);
82
+ return this.callAnthropic(messages, temperature, maxTokens, signal);
78
83
  case 'ollama':
79
- return this.callOllama(messages, temperature);
84
+ return this.callOllama(messages, temperature, signal);
80
85
  case 'openrouter':
81
- return this.callOpenRouter(messages, temperature, maxTokens);
86
+ return this.callOpenRouter(messages, temperature, maxTokens, signal);
82
87
  case 'gemini':
83
- return this.callGemini(messages, temperature, maxTokens);
88
+ return this.callGemini(messages, temperature, maxTokens, signal);
84
89
  case 'local':
85
- return this.callLocal(messages, temperature);
90
+ return this.callLocal(messages, temperature, signal);
86
91
  default:
87
92
  throw new Error(`Unsupported provider: ${this.provider}`);
88
93
  }
@@ -142,7 +147,7 @@ export class PiAIModel {
142
147
  };
143
148
  return modelMap[this.provider];
144
149
  }
145
- async callOpenAI(messages, temperature, maxTokens) {
150
+ async callOpenAI(messages, temperature, maxTokens, signal) {
146
151
  const apiKey = this.getApiKey();
147
152
  if (!apiKey) {
148
153
  throw new Error('OPENAI_API_KEY not set');
@@ -158,7 +163,8 @@ export class PiAIModel {
158
163
  messages,
159
164
  temperature,
160
165
  max_tokens: maxTokens
161
- })
166
+ }),
167
+ signal,
162
168
  });
163
169
  if (!response.ok) {
164
170
  throw new Error(`OpenAI API error: ${response.status}`);
@@ -166,7 +172,7 @@ export class PiAIModel {
166
172
  const data = await response.json();
167
173
  return data.choices?.[0]?.message?.content || '';
168
174
  }
169
- async callAnthropic(messages, temperature, maxTokens) {
175
+ async callAnthropic(messages, temperature, maxTokens, signal) {
170
176
  const apiKey = this.getApiKey();
171
177
  if (!apiKey) {
172
178
  throw new Error('ANTHROPIC_API_KEY not set');
@@ -187,7 +193,8 @@ export class PiAIModel {
187
193
  system: systemMessage,
188
194
  temperature,
189
195
  max_tokens: maxTokens
190
- })
196
+ }),
197
+ signal,
191
198
  });
192
199
  if (!response.ok) {
193
200
  throw new Error(`Anthropic API error: ${response.status}`);
@@ -195,7 +202,7 @@ export class PiAIModel {
195
202
  const data = await response.json();
196
203
  return data.content?.[0]?.text || '';
197
204
  }
198
- async callOllama(messages, temperature) {
205
+ async callOllama(messages, temperature, signal) {
199
206
  const response = await fetch(`${this.getBaseUrl()}/api/chat`, {
200
207
  method: 'POST',
201
208
  headers: {
@@ -206,7 +213,8 @@ export class PiAIModel {
206
213
  messages,
207
214
  temperature,
208
215
  stream: false
209
- })
216
+ }),
217
+ signal,
210
218
  });
211
219
  if (!response.ok) {
212
220
  throw new Error(`Ollama API error: ${response.status}`);
@@ -214,7 +222,7 @@ export class PiAIModel {
214
222
  const data = await response.json();
215
223
  return data.message?.content || '';
216
224
  }
217
- async callOpenRouter(messages, temperature, maxTokens) {
225
+ async callOpenRouter(messages, temperature, maxTokens, signal) {
218
226
  const apiKey = this.getApiKey();
219
227
  if (!apiKey) {
220
228
  throw new Error('OPENROUTER_API_KEY not set');
@@ -232,7 +240,8 @@ export class PiAIModel {
232
240
  messages,
233
241
  temperature,
234
242
  max_tokens: maxTokens
235
- })
243
+ }),
244
+ signal,
236
245
  });
237
246
  if (!response.ok) {
238
247
  throw new Error(`OpenRouter API error: ${response.status}`);
@@ -240,7 +249,7 @@ export class PiAIModel {
240
249
  const data = await response.json();
241
250
  return data.choices?.[0]?.message?.content || '';
242
251
  }
243
- async callGemini(messages, temperature, maxTokens) {
252
+ async callGemini(messages, temperature, maxTokens, signal) {
244
253
  const apiKey = this.getApiKey();
245
254
  if (!apiKey) {
246
255
  throw new Error('GEMINI_API_KEY not set');
@@ -264,7 +273,8 @@ export class PiAIModel {
264
273
  temperature,
265
274
  maxOutputTokens: maxTokens
266
275
  }
267
- })
276
+ }),
277
+ signal,
268
278
  });
269
279
  if (!response.ok) {
270
280
  throw new Error(`Gemini API error: ${response.status}`);
@@ -272,8 +282,8 @@ export class PiAIModel {
272
282
  const data = await response.json();
273
283
  return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
274
284
  }
275
- async callLocal(messages, temperature) {
276
- return this.callOllama(messages, temperature);
285
+ async callLocal(messages, temperature, signal) {
286
+ return this.callOllama(messages, temperature, signal);
277
287
  }
278
288
  buildSystemPrompt(context) {
279
289
  const envDetails = this.getEnvironmentDetails();
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Adaptive Scan — 类 B 自迭代入口
3
+ *
4
+ * 扫描 ~/.bolloon/human-values/usage.jsonl + judgments.json,
5
+ * 生成"自适应建议" (只读, 不改库). 用户在 UI 接受/拒绝后
6
+ * 才会真正生效, 写入 evolution.jsonl 留作审计.
7
+ *
8
+ * 设计原则 (类 B 边界):
9
+ * - 不自动改库 (避免 AI 越权)
10
+ * - 所有建议可逆 (接受/拒绝都行, 接受后写 evolution.jsonl 留痕)
11
+ * - 失败静默 (主对话不阻塞)
12
+ * - 单次扫描纯本地 + 内存计算, 无 LLM 调用 (避免 LLM 幻觉污染)
13
+ *
14
+ * 触发时机:
15
+ * - 用户主动: UI "📊 自适应" tab 点 "重新扫描"
16
+ * - 被动: 每天首次进入判断力 modal 时自动跑一次 (缓存 24h)
17
+ */
18
+ import * as fs from 'fs/promises';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { loadAllJudgments } from './human-value-store.js';
22
+ // 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
23
+ // 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
24
+ function getUsageLogPath() {
25
+ return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/usage.jsonl';
26
+ }
27
+ function getEvolutionLogPath() {
28
+ return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
29
+ }
30
+ const DAY_MS = 24 * 60 * 60 * 1000;
31
+ async function readUsageLog() {
32
+ try {
33
+ const content = await fs.readFile(getUsageLogPath(), 'utf-8');
34
+ const lines = content.trim().split('\n').filter(Boolean);
35
+ return lines
36
+ .map((l) => {
37
+ try {
38
+ return JSON.parse(l);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ })
44
+ .filter((e) => Boolean(e) && Array.isArray(e?.usedIds));
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
50
+ function countByJudgment(entries) {
51
+ const now = Date.now();
52
+ const cutoff7d = now - 7 * DAY_MS;
53
+ const cutoff30d = now - 30 * DAY_MS;
54
+ const map = new Map();
55
+ for (const e of entries) {
56
+ const ts = new Date(e.ts).getTime();
57
+ for (const id of e.usedIds) {
58
+ const cur = map.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
59
+ cur.total++;
60
+ if (ts >= cutoff7d)
61
+ cur.last7d++;
62
+ if (ts >= cutoff30d)
63
+ cur.last30d++;
64
+ if (cur.lastUseTs === null || ts > cur.lastUseTs)
65
+ cur.lastUseTs = ts;
66
+ map.set(id, cur);
67
+ }
68
+ }
69
+ return map;
70
+ }
71
+ /**
72
+ * 主扫描函数: 纯本地计算, 无 LLM
73
+ */
74
+ export async function runAdaptiveScan() {
75
+ const now = new Date();
76
+ const nowTs = now.getTime();
77
+ const [judgments, entries] = await Promise.all([
78
+ loadAllJudgments(),
79
+ readUsageLog(),
80
+ ]);
81
+ const usage = countByJudgment(entries);
82
+ const suggestions = [];
83
+ for (const j of judgments) {
84
+ if ((j.status ?? 'active') !== 'active')
85
+ continue;
86
+ const id = j.id;
87
+ const u = usage.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
88
+ const daysSinceLastUse = u.lastUseTs === null
89
+ ? Math.floor((nowTs - new Date(j.timestamp).getTime()) / DAY_MS)
90
+ : Math.floor((nowTs - u.lastUseTs) / DAY_MS);
91
+ const metrics = {
92
+ usage7d: u.last7d,
93
+ usage30d: u.last30d,
94
+ daysSinceLastUse,
95
+ totalUsage: u.total,
96
+ };
97
+ // rising: 7 天频率 > 30 天均值的 1.5 倍, 且至少用过 2 次 (避免噪声)
98
+ if (u.last30d > 0 && u.last7d >= 2) {
99
+ const dailyAvg30 = u.last30d / 30;
100
+ const dailyRate7 = u.last7d / 7;
101
+ if (dailyRate7 > dailyAvg30 * 1.5) {
102
+ suggestions.push({
103
+ key: `rising:${id}`,
104
+ kind: 'rising',
105
+ judgmentId: id,
106
+ decision: j.decision,
107
+ reason: `7 天使用率 (${dailyRate7.toFixed(2)}/d) 是 30 天均值的 ${(dailyRate7 / dailyAvg30).toFixed(1)} 倍`,
108
+ action: 'boost',
109
+ metrics,
110
+ scannedAt: now.toISOString(),
111
+ });
112
+ continue; // 不再归入 unused/stale
113
+ }
114
+ }
115
+ // stale: 90 天未使用 + 总使用 < 3 (低频 + 长期不用)
116
+ if (u.total < 3 && daysSinceLastUse >= 90) {
117
+ suggestions.push({
118
+ key: `stale:${id}`,
119
+ kind: 'stale',
120
+ judgmentId: id,
121
+ decision: j.decision,
122
+ reason: `已 ${daysSinceLastUse} 天未使用, 总使用仅 ${u.total} 次`,
123
+ action: 'deprecate',
124
+ metrics,
125
+ scannedAt: now.toISOString(),
126
+ });
127
+ continue;
128
+ }
129
+ // unused: 30 天未使用 + 总使用 < 5 (中频但近期没在用)
130
+ if (u.total < 5 && daysSinceLastUse >= 30) {
131
+ suggestions.push({
132
+ key: `unused:${id}`,
133
+ kind: 'unused',
134
+ judgmentId: id,
135
+ decision: j.decision,
136
+ reason: `已 ${daysSinceLastUse} 天未使用, 总使用 ${u.total} 次 (可能不再相关)`,
137
+ action: 'review',
138
+ metrics,
139
+ scannedAt: now.toISOString(),
140
+ });
141
+ }
142
+ }
143
+ // 按 action 重要性排序: rising > stale > unused
144
+ const order = { rising: 0, stale: 1, unused: 2, causal_conflict: 3, low_causal_power: 4 };
145
+ // 阶段 2: causal_conflict — judgment 库内自动检测冲突对
146
+ const { runConflictDetection } = await import('./causal-judge.js');
147
+ const conflictResult = await runConflictDetection();
148
+ for (const a of judgments) {
149
+ if ((a.status ?? 'active') !== 'active')
150
+ continue;
151
+ if (!Array.isArray(a.conflictWith) || a.conflictWith.length === 0)
152
+ continue;
153
+ // 列出每对冲突 (limit 每个 judgment 最多 3 对, 避免 UI 刷屏)
154
+ const conflicts = a.conflictWith.slice(0, 3);
155
+ for (const otherId of conflicts) {
156
+ const other = judgments.find((j) => j.id === otherId);
157
+ if (!other)
158
+ continue;
159
+ const c = await import('./causal-judge.js');
160
+ const det = c.detectConflict(a, other);
161
+ suggestions.push({
162
+ key: `causal_conflict:${a.id}:${other.id}`,
163
+ kind: 'causal_conflict',
164
+ judgmentId: a.id,
165
+ decision: `${a.decision} ↔ ${other.decision}`,
166
+ reason: det.isConflict ? det.reason : '库内已标冲突, 需 LLM 复核',
167
+ action: 'review',
168
+ metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
169
+ scannedAt: now.toISOString(),
170
+ });
171
+ }
172
+ }
173
+ // 静默 conflictResult.detected 计数 (已通过 import 触发了)
174
+ void conflictResult;
175
+ // 阶段 2: low_causal_power — 留空 (依赖 do-calculus 跑过后的低边际贡献记录)
176
+ // 当前不主动跑 (cost 高), 仅在 UI 手动触发后, 类 B 下次扫描时捡起来
177
+ // 此处不实现, 留作下个迭代
178
+ suggestions.sort((a, b) => order[a.kind] - order[b.kind]);
179
+ return {
180
+ scannedAt: now.toISOString(),
181
+ judgmentsTotal: judgments.length,
182
+ usageEntriesScanned: entries.length,
183
+ suggestions,
184
+ };
185
+ }
186
+ export async function logEvolution(entry) {
187
+ try {
188
+ await fs.mkdir(path.dirname(getEvolutionLogPath()), { recursive: true });
189
+ await fs.appendFile(getEvolutionLogPath(), JSON.stringify(entry) + '\n', 'utf-8');
190
+ }
191
+ catch (err) {
192
+ console.warn('[adaptive-scan] logEvolution failed:', err);
193
+ }
194
+ }
195
+ export async function readEvolutionLog(limit = 50) {
196
+ try {
197
+ const content = await fs.readFile(getEvolutionLogPath(), 'utf-8');
198
+ const lines = content.trim().split('\n').filter(Boolean);
199
+ return lines
200
+ .slice(-limit)
201
+ .reverse()
202
+ .map((l) => {
203
+ try {
204
+ return JSON.parse(l);
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ })
210
+ .filter((e) => Boolean(e));
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ }
216
+ // ============================================================
217
+ // 缓存: 避免每次打开 modal 都扫一次
218
+ // ============================================================
219
+ let lastScan = null;
220
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
221
+ export async function getCachedScan(force = false) {
222
+ if (!force && lastScan && Date.now() - lastScan.at < CACHE_TTL_MS) {
223
+ return lastScan.result;
224
+ }
225
+ const result = await runAdaptiveScan();
226
+ lastScan = { at: Date.now(), result };
227
+ return result;
228
+ }
229
+ export function clearAdaptiveScanCache() {
230
+ lastScan = null;
231
+ }