@bolloon/bolloon-agent 0.1.40 → 0.1.42

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 (38) hide show
  1. package/.comm/README.md +21 -0
  2. package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +7 -0
  3. package/CLAUDE.md +3 -0
  4. package/dist/agents/intent-classifier.js +99 -0
  5. package/dist/agents/pi-sdk.js +1364 -58
  6. package/dist/agents/pre-tool-validator.js +2 -1
  7. package/dist/agents/shell-guard.js +9 -3
  8. package/dist/agents/shell-tool.js +28 -13
  9. package/dist/agents/task-state.js +224 -0
  10. package/dist/agents/workflow-pivot-loop.js +30 -0
  11. package/dist/bollharness-integration/gate-state-machine.js +13 -0
  12. package/dist/bollharness-integration/integration.js +71 -0
  13. package/dist/bollharness-integration/skill-adapter.js +52 -127
  14. package/dist/bootstrap/context-hierarchy.js +6 -4
  15. package/dist/cli/interface.js +28 -0
  16. package/dist/documents/reader.js +14 -0
  17. package/dist/git-transport/chat-render.js +155 -0
  18. package/dist/git-transport/chat-repo.js +370 -0
  19. package/dist/git-transport/chat-types.js +23 -0
  20. package/dist/git-transport/chat-watch.js +102 -0
  21. package/dist/index.js +471 -25
  22. package/dist/llm/pi-ai.js +103 -27
  23. package/dist/network/local-inbox-bus.js +73 -0
  24. package/dist/network/p2p-direct.js +3 -4
  25. package/dist/pi-ecosystem-judgment/adaptive-scan.js +6 -2
  26. package/dist/pi-ecosystem-judgment/causal-judge.js +3 -3
  27. package/dist/pi-ecosystem-judgment/index.js +1 -1
  28. package/dist/security/tool-gate.js +11 -0
  29. package/dist/web/client.js +2876 -3770
  30. package/dist/web/components/p2p/index.js +226 -264
  31. package/dist/web/server.js +83 -15
  32. package/dist/web/ui/message-renderer.js +326 -442
  33. package/dist/web/ui/step-timeline.js +255 -351
  34. package/package.json +2 -2
  35. package/scripts/lefthook-helper.sh +69 -0
  36. package/dist/web/components/p2p/P2PModal.js +0 -188
  37. package/dist/web/components/p2p/p2p-modal.js +0 -657
  38. package/dist/web/components/p2p/p2p-tools.js +0 -248
package/dist/llm/pi-ai.js CHANGED
@@ -20,21 +20,78 @@ export function getSystemPrependProvider() {
20
20
  export class PiAIModel {
21
21
  config;
22
22
  provider;
23
+ /** 单次 LLM HTTP 请求硬上限 (ms), 防止上游卡住挂死整个 loop. 可通过 BOLLOON_LLM_TIMEOUT 覆盖. */
24
+ requestTimeoutMs;
23
25
  constructor(config) {
24
26
  this.config = config;
25
27
  this.provider = config.provider;
28
+ const envTimeout = Number(process.env.BOLLOON_LLM_TIMEOUT);
29
+ this.requestTimeoutMs = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120_000;
26
30
  }
27
- async chat(message, context, signal) {
28
- const systemPrompt = await this.buildSystemPromptAsync(context);
29
- const messages = [
30
- { role: 'system', content: systemPrompt },
31
- { role: 'user', content: message }
32
- ];
31
+ /**
32
+ * 把外部 signal 和内部 timeout 合并: 任一触发都 abort.
33
+ * - 外部 signal 优先 (用户主动 abort)
34
+ * - 否则套 120s timeout
35
+ * - 任一不合法 (非 AbortSignal 实例) 时退到无 signal
36
+ */
37
+ combinedSignal(external) {
38
+ const valid = external instanceof AbortSignal ? external : undefined;
39
+ // Node 18+ 支持 AbortSignal.timeout, 旧版本兜底用 setTimeout 构造
40
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
41
+ const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
42
+ if (!valid)
43
+ return timeoutSignal;
44
+ // 合并两个 signal
45
+ const ctrl = new AbortController();
46
+ const onAbort = () => ctrl.abort();
47
+ valid.addEventListener('abort', onAbort, { once: true });
48
+ timeoutSignal.addEventListener('abort', onAbort, { once: true });
49
+ if (valid.aborted || timeoutSignal.aborted)
50
+ ctrl.abort();
51
+ return ctrl.signal;
52
+ }
53
+ return valid;
54
+ }
55
+ /**
56
+ * 与 LLM 对话.
57
+ *
58
+ * 支持三种调用形式:
59
+ * - 旧: chat(message: string, context?: string, signal?)
60
+ * 单一 user message + 附加 system context. 简单场景用.
61
+ * - 新: chat(messages: ChatMessage[], context?: string, signal?)
62
+ * 完整 messages 数组, 含 user/assistant/tool/system role.
63
+ * 工具调用场景必用 — 否则 LLM 看不到工具结果.
64
+ * - pivot loop 兼容: chat(context: string, systemPrompt: string, signal?)
65
+ * 第二参超过 2K 时视为 system prompt 覆盖, 避免把 46K context 当 user message 发送.
66
+ *
67
+ * 2026-06-17 (M3.5 调试): buildContext() 之前把所有 history 序列化成单字符串,
68
+ * LLM 看不到 tool 调用的真实结果,导致 CLI loop 卡死.
69
+ * 现在 messages 数组版本保留 role 语义, LLM 能正确看到工具返回.
70
+ */
71
+ async chat(messageOrMessages, contextOrSystemPrompt, signal) {
72
+ const systemPrompt = await this.buildSystemPromptAsync(contextOrSystemPrompt);
73
+ let messages;
74
+ if (Array.isArray(messageOrMessages)) {
75
+ messages = [{ role: 'system', content: systemPrompt }, ...messageOrMessages];
76
+ }
77
+ else if (contextOrSystemPrompt && contextOrSystemPrompt.length > 2000) {
78
+ const baseSystem = await this.buildSystemPromptAsync(undefined);
79
+ messages = [
80
+ { role: 'system', content: baseSystem + '\n\n' + contextOrSystemPrompt },
81
+ { role: 'user', content: messageOrMessages }
82
+ ];
83
+ }
84
+ else {
85
+ messages = [
86
+ { role: 'system', content: systemPrompt },
87
+ { role: 'user', content: messageOrMessages }
88
+ ];
89
+ }
33
90
  try {
34
91
  const response = await this.generateText({
35
92
  messages,
36
93
  temperature: 0.8,
37
- maxTokens: 8192, // 2026-06-15: 提到 8192, 避免长回答 (含表格/代码块) 被截断
94
+ maxTokens: 16384, // 2026-06-17: 提到 16384 agent 注入 16K+ system prompt + 8K tool defs 时, 8K 撞上限返回空 content (见 memory: bolloon-llm-empty-large-prompt)
38
95
  signal,
39
96
  });
40
97
  return { reply: response };
@@ -215,23 +272,42 @@ export class PiAIModel {
215
272
  temperature,
216
273
  max_tokens: maxTokens
217
274
  };
218
- const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {
219
- method: 'POST',
220
- headers: {
221
- 'Content-Type': 'application/json',
222
- 'Authorization': `Bearer ${apiKey}`
223
- },
224
- body: JSON.stringify(requestBody),
225
- // 2026-06-15: signal 字段防御 Node 22+ undici 强类型 AbortSignal, 非 AbortSignal 实例
226
- // (e.g. 误传的 { maxTokens: 1 } 对象) 会 throw "Expected signal to be AbortSignal".
227
- // 若不是 AbortSignal, 退到无 signal 调用, fetch 自然支持 timeout 由外层控制.
228
- signal: signal instanceof AbortSignal ? signal : undefined,
229
- });
230
- if (!response.ok) {
231
- throw new Error(`OpenAI API error: ${response.status}`);
275
+ // 2026-06-19: 一次命中优化 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
276
+ // 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
277
+ // 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
278
+ // 2026-06-19: 一次命中优化 — 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
279
+ // 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
280
+ // 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
281
+ let lastFinishReason = '';
282
+ for (let attempt = 0; attempt < 3; attempt++) {
283
+ const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Content-Type': 'application/json',
287
+ 'Authorization': `Bearer ${apiKey}`
288
+ },
289
+ body: JSON.stringify(requestBody),
290
+ signal: this.combinedSignal(signal),
291
+ });
292
+ if (!response.ok) {
293
+ throw new Error(`OpenAI API error: ${response.status}`);
294
+ }
295
+ const data = await response.json();
296
+ const choice = data.choices?.[0];
297
+ const content = choice?.message?.content || '';
298
+ lastFinishReason = choice?.finish_reason || '';
299
+ if (content) {
300
+ if (lastFinishReason === 'length') {
301
+ console.warn(`[pi-ai] hit max_tokens ceiling (model=${this.mapModel()}, max_tokens=${maxTokens}) — caller should trim prompt or raise cap`);
302
+ }
303
+ return content;
304
+ }
305
+ // 空 content: 200 但 content="" → minimax 上游偶发, 退避后重试
306
+ console.warn(`[pi-ai] attempt ${attempt + 1}/3: 空 content (finish_reason=${lastFinishReason}), 退避 1.5s 重试`);
307
+ await new Promise(resolve => setTimeout(resolve, 1500));
232
308
  }
233
- const data = await response.json();
234
- return data.choices?.[0]?.message?.content || '';
309
+ console.warn(`[pi-ai] 3 次重试都返回空 content (finish_reason=${lastFinishReason})`);
310
+ return ''; // 返回空让上层看到 [AI 服务调用失败]
235
311
  }
236
312
  async callAnthropic(messages, temperature, maxTokens, signal) {
237
313
  const apiKey = this.getApiKey();
@@ -255,7 +331,7 @@ export class PiAIModel {
255
331
  temperature,
256
332
  max_tokens: maxTokens
257
333
  }),
258
- signal,
334
+ signal: this.combinedSignal(signal),
259
335
  });
260
336
  if (!response.ok) {
261
337
  throw new Error(`Anthropic API error: ${response.status}`);
@@ -275,7 +351,7 @@ export class PiAIModel {
275
351
  temperature,
276
352
  stream: false
277
353
  }),
278
- signal,
354
+ signal: this.combinedSignal(signal),
279
355
  });
280
356
  if (!response.ok) {
281
357
  throw new Error(`Ollama API error: ${response.status}`);
@@ -302,7 +378,7 @@ export class PiAIModel {
302
378
  temperature,
303
379
  max_tokens: maxTokens
304
380
  }),
305
- signal,
381
+ signal: this.combinedSignal(signal),
306
382
  });
307
383
  if (!response.ok) {
308
384
  throw new Error(`OpenRouter API error: ${response.status}`);
@@ -335,7 +411,7 @@ export class PiAIModel {
335
411
  maxOutputTokens: maxTokens
336
412
  }
337
413
  }),
338
- signal,
414
+ signal: this.combinedSignal(signal),
339
415
  });
340
416
  if (!response.ok) {
341
417
  throw new Error(`Gemini API error: ${response.status}`);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * local-inbox-bus.ts — 进程内 Agent 通信总线 (2026-06-19)
3
+ *
4
+ * 解决 bolloon "和另一个智能体发消息" 的本地场景:
5
+ * - 同进程内多个 PiAgentSession 实例 (例如 nodeA / nodeB 测试) 互发消息
6
+ * - 走 in-process EventEmitter, 不依赖 P2P 网络/Hyperswarm
7
+ * - 远端通信仍走 p2pNetwork.sendMessage + onMessage
8
+ *
9
+ * 用法:
10
+ * const bus = LocalInboxBus.getInstance();
11
+ * bus.subscribe('nodeA', (msg) => console.log('nodeA got:', msg));
12
+ * bus.deliver('nodeB', { from: 'nodeA', type: 'agent-message', payload: 'hi' });
13
+ *
14
+ * 角色标识: 默认从 process.env.BOLLOON_ROLE 读, 也可显式传
15
+ */
16
+ import { EventEmitter } from 'events';
17
+ class LocalInboxBus {
18
+ static _instance = null;
19
+ emitter;
20
+ /** 记录已注册的角色, deliver 时找不到会返回 false (避免 silent drop) */
21
+ registeredRoles = new Set();
22
+ constructor() {
23
+ this.emitter = new EventEmitter();
24
+ // 防止 listener 太多导致 memory leak
25
+ this.emitter.setMaxListeners(1000);
26
+ }
27
+ static getInstance() {
28
+ if (!LocalInboxBus._instance) {
29
+ LocalInboxBus._instance = new LocalInboxBus();
30
+ }
31
+ return LocalInboxBus._instance;
32
+ }
33
+ /**
34
+ * 订阅角色 inbox — 投递到该角色的消息会回调 fn
35
+ * 返回 unsubscribe 函数
36
+ */
37
+ subscribe(role, fn) {
38
+ this.registeredRoles.add(role);
39
+ this.emitter.on(`role:${role}`, fn);
40
+ return () => {
41
+ this.emitter.off(`role:${role}`, fn);
42
+ // 如果没人订阅, 移除 role (避免泄漏)
43
+ if (this.emitter.listenerCount(`role:${role}`) === 0) {
44
+ this.registeredRoles.delete(role);
45
+ }
46
+ };
47
+ }
48
+ /**
49
+ * 投递消息到指定角色
50
+ * 返回 true=已投递, false=角色未注册
51
+ */
52
+ deliver(role, msg) {
53
+ if (!this.registeredRoles.has(role)) {
54
+ return false;
55
+ }
56
+ this.emitter.emit(`role:${role}`, msg);
57
+ return true;
58
+ }
59
+ /**
60
+ * 列出当前进程所有已注册的角色
61
+ */
62
+ listRoles() {
63
+ return Array.from(this.registeredRoles);
64
+ }
65
+ /**
66
+ * 检查某角色是否已订阅
67
+ */
68
+ hasRole(role) {
69
+ return this.registeredRoles.has(role);
70
+ }
71
+ }
72
+ export { LocalInboxBus };
73
+ export default LocalInboxBus;
@@ -18,6 +18,8 @@ import { EventEmitter } from 'events';
18
18
  // @ts-ignore — b4a 没官方 .d.ts
19
19
  import b4a from 'b4a';
20
20
  import { loadOrCreateKeyPair, writebackPublicKey } from './p2p-secret.js';
21
+ // 静态 import input-scanner (ESM, 替代 require 避免 "require is not defined" 报错)
22
+ import * as inputScanner from '../security/input-scanner.js';
21
23
  export class P2PDirect extends EventEmitter {
22
24
  swarm = null;
23
25
  name;
@@ -67,10 +69,7 @@ export class P2PDirect extends EventEmitter {
67
69
  // P-Action 3: untrusted-input scanner (默认 silence-on-fail + log-only)
68
70
  // 包装层 跨机器 @-mention 代发 (已 WORKING) 不能 block-on-suspicion
69
71
  try {
70
- // 动态 import 避免循环依赖 + 启动时加载
71
- // (lazy require to keep cold-start fast)
72
- // eslint-disable-next-line @typescript-eslint/no-var-requires
73
- const { scanInput, writeScanAudit, shouldHardBlock } = require('../security/input-scanner.js');
72
+ const { scanInput, writeScanAudit, shouldHardBlock } = inputScanner;
74
73
  const result = scanInput(buf, { source: 'p2p' });
75
74
  if (shouldHardBlock(result)) {
76
75
  console.warn(`[P2PDirect:${this.name}] 阻断恶意消息 from ${remotePubKeyHex.substring(0, 12)}... (verdict=${result.verdict}, threats=${result.threats.length})`);
@@ -21,11 +21,15 @@ import * as path from 'path';
21
21
  import { loadAllJudgments } from './human-value-store.js';
22
22
  // 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
23
23
  // 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
24
+ // 跨平台: 优先 process.env.HOME (测试可控), 然后 os.homedir() (Windows 上读 USERPROFILE).
25
+ // 用 path.join 而不是字符串拼接, 保证 Windows 上用 \ 分隔.
24
26
  function getUsageLogPath() {
25
- return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/usage.jsonl';
27
+ const home = process.env.HOME || os.homedir() || '/tmp';
28
+ return path.join(home, '.bolloon', 'human-values', 'usage.jsonl');
26
29
  }
27
30
  function getEvolutionLogPath() {
28
- return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
31
+ const home = process.env.HOME || os.homedir() || '/tmp';
32
+ return path.join(home, '.bolloon', 'human-values', 'evolution.jsonl');
29
33
  }
30
34
  /**
31
35
  * P-Action 1: next-action hint (4 仓库都强调的错误信息 = 修复指令 思想)
@@ -22,9 +22,9 @@ import * as os from 'os';
22
22
  import * as path from 'path';
23
23
  import { getModel } from '../llm/pi-ai.js';
24
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';
25
+ const EVOLUTION_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'evolution.jsonl');
26
+ const COUNTERFACTUAL_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'counterfactual-audit.jsonl');
27
+ const USAGE_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'usage.jsonl');
28
28
  async function readUsageLog() {
29
29
  try {
30
30
  const content = await fs.readFile(USAGE_LOG, 'utf-8');
@@ -266,7 +266,7 @@ export async function extractFromFragment(fragmentPath) {
266
266
  type: 'rule',
267
267
  content: ruleContent,
268
268
  source: 'human',
269
- confidence: 0.8,
269
+ confidence: 0.9,
270
270
  context,
271
271
  createdAt: new Date().toISOString(),
272
272
  updatedAt: new Date().toISOString(),
@@ -21,6 +21,17 @@ const TOOL_WHITELIST = new Set([
21
21
  'read', 'write', 'edit_file', 'list_files',
22
22
  'list_sessions', 'get_session_state', 'list_messages',
23
23
  'send_to_channel', 'create_channel',
24
+ // M2.1 (2026-06-17): 新增的 10 个 agent 工具 (跟 pi-sdk registerTools 同步)
25
+ 'write_file', 'git_diff', 'git_commit', 'git_push', 'git_branch',
26
+ 'create_task', 'update_task', 'get_task', 'list_tasks',
27
+ 'use_skill', 'self_improve',
28
+ // 2026-06-19: M4 新增 (跟 pi-sdk.ts registerTools 同步)
29
+ 'read_file', 'delete_file', 'mkdir', 'move_file',
30
+ 'grep_files', 'glob_files',
31
+ 'git_log', 'git_show', 'git_stash',
32
+ 'vitest_run', 'tsc_check',
33
+ // 2026-06-19: Agent Mesh 通信工具 (跟 pi-sdk.ts registerTools 同步)
34
+ 'check_inbox', 'send_to_peer', 'p2p_broadcast', 'send_to_local_agent', 'list_local_agents', 'agent_call',
24
35
  // MCP 注册的工具
25
36
  'mcp_tool',
26
37
  ]);