@bolloon/bolloon-agent 0.1.13 → 0.1.15

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 (47) hide show
  1. package/dist/agents/pi-sdk.js +222 -9
  2. package/dist/agents/shell-guard.js +354 -0
  3. package/dist/agents/shell-tool.js +83 -0
  4. package/dist/agents/skill-loader.js +174 -0
  5. package/dist/bollharness-integration/context-chain-router.js +3 -3
  6. package/dist/bollharness-integration/context-router.js +1 -1
  7. package/dist/heartbeat/Watchdog.js +7 -5
  8. package/dist/heartbeat/index.js +1 -0
  9. package/dist/heartbeat/self-improve-bus.js +85 -0
  10. package/dist/pi-ecosystem-judgment/index.js +1 -2
  11. package/dist/utils/auto-update.js +44 -12
  12. package/dist/web/client.js +841 -103
  13. package/dist/web/index.html +88 -8
  14. package/dist/web/style.css +506 -9
  15. package/package.json +2 -2
  16. package/scripts/build-cli.js +11 -1
  17. package/src/agents/pi-sdk.ts +230 -10
  18. package/src/agents/shell-guard.ts +417 -0
  19. package/src/agents/shell-tool.ts +103 -0
  20. package/src/agents/skill-loader.ts +202 -0
  21. package/src/bollharness-integration/context-chain-router.ts +3 -3
  22. package/src/bollharness-integration/context-router.ts +1 -1
  23. package/src/heartbeat/Watchdog.ts +7 -5
  24. package/src/heartbeat/index.ts +1 -0
  25. package/src/heartbeat/self-improve-bus.ts +110 -0
  26. package/src/types.d.ts +12 -0
  27. package/src/utils/auto-update.ts +45 -14
  28. package/src/web/client.js +841 -103
  29. package/src/web/index.html +88 -8
  30. package/src/web/server.ts +427 -101
  31. package/src/web/style.css +506 -9
  32. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  33. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  34. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  35. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  36. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  37. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  38. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  39. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  40. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  41. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  42. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  43. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  44. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  45. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  46. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  47. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -32,7 +32,7 @@
32
32
  "src/constraint-runtime"
33
33
  ],
34
34
  "dependencies": {
35
- "@bolloon/bolloon-agent": "^0.1.11",
35
+ "@bolloon/bolloon-agent": "^0.1.15",
36
36
  "@bolloon/constraint-runtime": "0.1.0",
37
37
  "@chainsafe/libp2p-noise": "^17.0.0",
38
38
  "@chainsafe/libp2p-yamux": "^8.0.1",
@@ -196,9 +196,19 @@ main().catch((err) => {
196
196
  fs.writeFileSync(path.join(binDir, 'bolloon.cjs'), unixContent);
197
197
 
198
198
  // 确保 bin/bolloon.js 存在(npm link 需要)
199
+ // 优先用符号链接(POSIX),Windows 上若权限不足则退化为复制文件
199
200
  const jsSymlink = path.join(binDir, 'bolloon.js');
200
201
  if (fs.existsSync(jsSymlink)) fs.unlinkSync(jsSymlink);
201
- fs.symlinkSync('bolloon.cjs', jsSymlink);
202
+ try {
203
+ fs.symlinkSync('bolloon.cjs', jsSymlink);
204
+ } catch (err) {
205
+ if (err && err.code === 'EPERM') {
206
+ fs.copyFileSync(path.join(binDir, 'bolloon.cjs'), jsSymlink);
207
+ console.warn(' ⚠ symlink 不支持(Windows),已退化为文件复制');
208
+ } else {
209
+ throw err;
210
+ }
211
+ }
202
212
 
203
213
  console.log("✓ CLI 构建完成");
204
214
  console.log(" bin/bolloon.cjs - CommonJS 入口");
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as fs from 'fs/promises';
7
7
  import * as fsSync from 'fs';
8
+ import * as os from 'os';
8
9
  import * as path from 'path';
9
10
  import { documentReader, DocumentContent } from '../documents/reader.js';
10
11
  import { getMinimax } from '../constraints/index.js';
@@ -14,6 +15,8 @@ import { WorkflowEngine, WorkflowStep, StepResult, Workflow } from './workflow-e
14
15
  import { DeepThinkingEngine, AgentCoordinator, type ThinkResult, type AgentResult } from '@bolloon/constraint-runtime';
15
16
  import { WorkflowPivotLoop, createDefaultPivotConfig, type PivotLoopConfig, type LoopResult } from './workflow-pivot-loop.js';
16
17
  import { p2pDocumentTools, initDocumentReceiver } from './p2p-document-tools.js';
18
+ import { shellExec } from './shell-tool.js';
19
+ import { getBranchPrefix, getCooldownMs } from './shell-guard.js';
17
20
  import {
18
21
  DiscoveredAgentsManager,
19
22
  SocialHeartbeat,
@@ -36,6 +39,7 @@ import {
36
39
  type GlobalSharedContext
37
40
  } from '../social/global-shared-context.js';
38
41
  import { Session, SkillRegistry, saveSession, loadSession, type Skill, type StoredSession } from '@bolloon/constraint-runtime';
42
+ import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
39
43
 
40
44
  // Pi Ecosystem Integration (lazy imports - initialized on demand)
41
45
  // Functions from: createGoal, getCurrentGoal, completeCurrentGoal, failCurrentGoal, getGoalStats, getQueueSummary
@@ -46,6 +50,12 @@ export interface AgentSessionConfig {
46
50
  identityDoc?: IdentityDoc;
47
51
  usePivotLoop?: boolean;
48
52
  pivotLoopConfig?: PivotLoopConfig;
53
+ /**
54
+ * Skills 加载目录列表, 后者覆盖前者同名 skill.
55
+ * 留空时使用 defaultSkillPaths() 推断的默认路径
56
+ * ( ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/ )
57
+ */
58
+ skillsPaths?: string[];
49
59
  }
50
60
 
51
61
  export interface IdentityDoc {
@@ -575,9 +585,72 @@ class PiAgentSession implements AgentSession {
575
585
  this.initSession();
576
586
  initDocumentReceiver();
577
587
  this.registerTools();
588
+ this.loadSkills(config.skillsPaths);
578
589
  this.initHarness();
579
590
  }
580
591
 
592
+ /**
593
+ * 从 SKILL.md 目录加载 skills 进 skillRegistry.
594
+ *
595
+ * 路径解析优先级 (后者覆盖前者同名 skill):
596
+ * 1. 显式传入的 skillsPaths
597
+ * 2. ~/.bolloon/skills/ 全局用户级
598
+ * 3. <cwd>/.bolloon/skills/ 项目级
599
+ * 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
600
+ * 5. <bolloon-repo>/src/bollharness/.boll/skills/ bolloon 仓库内置 skill
601
+ * (bolloon 项目本身用 pi-sdk 写核心, 这 19 个 skill 视为项目级 builtin)
602
+ *
603
+ * 静默忽略不存在的目录.
604
+ */
605
+ private loadSkills(paths?: string[]): void {
606
+ let resolved: string[];
607
+ if (paths && paths.length > 0) {
608
+ resolved = paths;
609
+ } else {
610
+ resolved = [
611
+ ...defaultSkillPaths(os.homedir(), this.cwd),
612
+ // bolloon 仓库内置 skill (相对本 npm 包的位置)
613
+ this.findBolloonBuiltinSkillsPath(),
614
+ ].filter((p): p is string => Boolean(p));
615
+ }
616
+ loadSkillsFromPaths(resolved)
617
+ .then((skills) => {
618
+ for (const s of skills) {
619
+ if (this.skillRegistry.has(s.name)) {
620
+ this.skillRegistry.unregister(s.name);
621
+ }
622
+ this.skillRegistry.register(s);
623
+ }
624
+ console.log(`[loadSkills] 已加载 ${skills.length} 个 skill: ${skills.map(describeSkill).join(' | ')}`);
625
+ })
626
+ .catch((err) => {
627
+ console.error('[loadSkills] 加载失败:', err);
628
+ });
629
+ }
630
+
631
+ /**
632
+ * 定位 bolloon 仓库内置的 bollharness skill 目录.
633
+ * 向上回溯 cwd, 找第一个包含 src/bollharness/.boll/skills 的祖先.
634
+ * 找不到时返回 null (例如把 bolloon-agent 作为外部依赖安装时).
635
+ */
636
+ private findBolloonBuiltinSkillsPath(): string | null {
637
+ let dir = this.cwd;
638
+ for (let i = 0; i < 6; i++) {
639
+ const candidate = path.join(dir, 'src', 'bollharness', '.boll', 'skills');
640
+ try {
641
+ if (fsSync.existsSync(candidate) && fsSync.statSync(candidate).isDirectory()) {
642
+ return candidate;
643
+ }
644
+ } catch {
645
+ // 忽略 stat 异常, 继续向上
646
+ }
647
+ const parent = path.dirname(dir);
648
+ if (parent === dir) break;
649
+ dir = parent;
650
+ }
651
+ return null;
652
+ }
653
+
581
654
  private async initHarness(): Promise<void> {
582
655
  try {
583
656
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
@@ -808,6 +881,86 @@ class PiAgentSession implements AgentSession {
808
881
  for (const tool of p2pDocumentTools) {
809
882
  this.tools.set(tool.name, tool);
810
883
  }
884
+
885
+ // Shell Exec 工具: 给 AI 跑受限的 shell 命令
886
+ // **只能** 跑白名单内的命令 (git/npm/tsc/vitest/cat/...)
887
+ // **不能** 改禁区路径 (见 shell-guard.ts 的 FORBIDDEN_PATH_PATTERNS)
888
+ // 沙箱 cwd: .bolloon-shell-sandbox/
889
+ this.tools.set('shell_exec', {
890
+ name: 'shell_exec',
891
+ description: '在沙箱里跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒.',
892
+ parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
893
+ execute: async (args) => {
894
+ const cmd = String(args.command || '').trim();
895
+ if (!cmd) return { success: false, error: 'command 必填' };
896
+ const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
897
+ const timeoutMs = Number(args.timeoutMs) || 30000;
898
+
899
+ const result = await shellExec(cmd, argList, { timeoutMs });
900
+ if (result.deniedByGuard) {
901
+ return { success: false, error: result.error };
902
+ }
903
+ if (!result.success) {
904
+ return { success: false, error: result.error, output: result.output };
905
+ }
906
+ return { success: true, output: result.output };
907
+ }
908
+ });
909
+
910
+ // self_improve 工具: AI 触发自我改进循环
911
+ // **必须** 在 branchPrefix 命名的分支上工作
912
+ // 心跳事件会自动调用; 用户对话里也能手动调
913
+ this.tools.set('self_improve', {
914
+ name: 'self_improve',
915
+ description: `触发自我改进循环. AI 会在分支 ${getBranchPrefix()}<timestamp> 上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审. 冷却期由策略文件决定. 命中护栏禁区的改动会被拒.`,
916
+ parameters: { goal: '本轮改进目标 (1 句话)' },
917
+ execute: async (args) => {
918
+ const goal = String(args.goal || '').trim();
919
+ if (!goal) return { success: false, error: 'goal 必填' };
920
+ return await runSelfImproveLoop(goal);
921
+ }
922
+ });
923
+
924
+ // list_skills 工具: 列出当前 session 已加载的 skills
925
+ // 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
926
+ this.tools.set('list_skills', {
927
+ name: 'list_skills',
928
+ description: '列出当前 session 已加载的 skills 及其描述. Skills 是从 SKILL.md 文件加载的, 兼容 Anthropic Agent Skills 标准 frontmatter 和 bollharness 现有 frontmatter.',
929
+ parameters: {},
930
+ execute: async () => {
931
+ const skills = this.skillRegistry.list();
932
+ if (skills.length === 0) {
933
+ return {
934
+ success: true,
935
+ output: '当前 session 没有加载任何 skill. 检查 ~/.bolloon/skills/ 或项目 .bolloon/skills/ 目录.',
936
+ };
937
+ }
938
+ const lines = skills.map((s, i) => `${i + 1}. ${s.name} — ${s.description}`);
939
+ return { success: true, output: `已加载 ${skills.length} 个 skill:\n${lines.join('\n')}` };
940
+ }
941
+ });
942
+
943
+ // use_skill 工具: 加载指定 skill 的 body 进 LLM context
944
+ // Skills 协议核心: 把 SKILL.md body 作为 Markdown 指南返回, LLM 下一轮按它执行
945
+ this.tools.set('use_skill', {
946
+ name: 'use_skill',
947
+ description: '按名称加载一个 skill, 把它的 SKILL.md body 作为 Markdown 文档返回. 调用后 LLM 会在下一轮按 skill 指南执行. 与 shell_exec / read_document 这些 "能力工具" 不同, use_skill 是 "知识注入".',
948
+ parameters: { name: 'skill 名称 (用 list_skills 查可用的)' },
949
+ execute: async (args) => {
950
+ const name = String(args.name || '').trim();
951
+ if (!name) return { success: false, error: 'name 必填' };
952
+ if (!this.skillRegistry.has(name)) {
953
+ const available = this.skillRegistry.list().map((s) => s.name).join(', ');
954
+ return { success: false, error: `skill "${name}" 未找到. 已加载: ${available || '(无)'}` };
955
+ }
956
+ try {
957
+ const body = await this.skillRegistry.execute(name, {});
958
+ return { success: true, output: body };
959
+ } catch (e) {
960
+ return { success: false, error: `执行 skill 失败: ${String(e)}` };
961
+ }
962
+ }
963
+ });
811
964
  }
812
965
 
813
966
  private async registerP2PDocumentReceiver(): Promise<void> {
@@ -1142,7 +1295,9 @@ ${toolDefs}
1142
1295
  // 检查是否需要继续循环处理
1143
1296
  // 更严格的判断:只有当回复明确表示需要更多信息时才继续
1144
1297
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
1145
- reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看');
1298
+ reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
1299
+ // 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
1300
+ reply.includes('tool =>') || reply.includes('[TOOL_CALL]');
1146
1301
  const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
1147
1302
  '错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
1148
1303
  const isTooShort = reply.length < 50 && reply.length > 0;
@@ -1284,21 +1439,43 @@ Workspace root folder: ${this.cwd}
1284
1439
  /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
1285
1440
  /使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
1286
1441
  /tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
1287
- /(\w+)\s*\(\s*([^)]*)\s*\)/
1442
+ /(\w+)\s*\(\s*([^)]*)\s*\)/,
1443
+ // 兼容 LLM 输出的对象字面量格式: {tool => "get_identity", args => {...}}
1444
+ /\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
1445
+ // 兼容: tool => "get_identity" (无 args 包裹)
1446
+ /\btool\s*=>\s*["'](\w+)["']/,
1447
+ // 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
1448
+ /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
1288
1449
  ];
1289
1450
 
1290
1451
  for (const pattern of patterns) {
1291
1452
  const match = content.match(pattern);
1292
1453
  if (match) {
1293
1454
  const name = match[1];
1294
- const argsStr = match[2] || '';
1295
- const args: Record<string, string> = {};
1296
-
1297
- const argPairs = argsStr.split(',').map(s => s.trim()).filter(Boolean);
1298
- for (const pair of argPairs) {
1299
- const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1300
- if (key) {
1301
- args[key] = valueParts.join(':') || '';
1455
+ let args: Record<string, string> = {};
1456
+ const rawArgs = match[2] || '';
1457
+
1458
+ if (rawArgs && rawArgs.trim().startsWith('{')) {
1459
+ // JSON 形式, 尝试解析
1460
+ try {
1461
+ const parsed = JSON.parse(rawArgs);
1462
+ if (parsed && typeof parsed === 'object') {
1463
+ args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
1464
+ }
1465
+ } catch {
1466
+ // 解析失败就当字符串处理
1467
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
1468
+ for (const pair of argPairs) {
1469
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1470
+ if (key) args[key] = valueParts.join(':') || '';
1471
+ }
1472
+ }
1473
+ } else if (rawArgs) {
1474
+ // 形参串, 形如 key: value, key2: value2
1475
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
1476
+ for (const pair of argPairs) {
1477
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
1478
+ if (key) args[key] = valueParts.join(':') || '';
1302
1479
  }
1303
1480
  }
1304
1481
 
@@ -1919,3 +2096,46 @@ export function resetAgentSession(): void {
1919
2096
  sessionInstance = null;
1920
2097
  lastIdentityDid = null;
1921
2098
  }
2099
+
2100
+ /**
2101
+ * 自我改进循环: 在沙箱分支上工作, 输出结果给用户审.
2102
+ *
2103
+ * 不在 PiAgent 实例上的原因: 心跳回调可能没有 agent 实例, 单独函数更易复用.
2104
+ *
2105
+ * **关键不变量**:
2106
+ * 1. AI 不能 push 到 master (shell-guard 黑名单 + git 受保护分支)
2107
+ * 2. 改动必须走沙箱分支 (SELF_IMPROVE_BRANCH_PREFIX)
2108
+ * 3. 6 小时冷却期 (SELF_IMPROVE_COOLDOWN_MS)
2109
+ * 4. 写文件必须经过 shell_exec + 护栏检查
2110
+ */
2111
+ let lastSelfImproveAt: number | null = null;
2112
+
2113
+ export async function runSelfImproveLoop(goal: string): Promise<{ success: boolean; output?: string; error?: string }> {
2114
+ const cooldownMs = getCooldownMs();
2115
+ // 1. 冷却期检查
2116
+ if (lastSelfImproveAt && Date.now() - lastSelfImproveAt < cooldownMs) {
2117
+ const waitHrs = Math.ceil((cooldownMs - (Date.now() - lastSelfImproveAt)) / 3600000);
2118
+ return { success: false, error: `自改冷却中, 还需要约 ${waitHrs} 小时` };
2119
+ }
2120
+
2121
+ // 2. 选源分支 + 新分支名
2122
+ const sourceBranch = 'master';
2123
+ const newBranch = `${getBranchPrefix()}${Date.now()}`;
2124
+
2125
+ console.log(`[self-improve] 启动自改循环, 目标: ${goal}, 新分支: ${newBranch}`);
2126
+
2127
+ // 3. 创建分支
2128
+ const r1 = await shellExec('git', ['checkout', sourceBranch]);
2129
+ if (!r1.success) return { success: false, error: `切换到 ${sourceBranch} 失败: ${r1.error}` };
2130
+
2131
+ const r2 = await shellExec('git', ['checkout', '-b', newBranch]);
2132
+ if (!r2.success) return { success: false, error: `创建分支失败: ${r2.error}` };
2133
+
2134
+ // 4. 走 task queue: 把"自改"作为一个 task 抛回去, AI 拿到后会用 shell_exec 改
2135
+ // 护栏已经阻止所有禁区改动, 这里只负责登记
2136
+ lastSelfImproveAt = Date.now();
2137
+ return {
2138
+ success: true,
2139
+ output: `✅ 自改分支已创建: ${newBranch}\n目标: ${goal}\n\n**护栏已激活**:\n - 仅允许白名单命令 (git/npm/tsc/vitest/cat/ls/...)\n - 禁止改 src/agents/pi-sdk.ts, shell-guard.ts, src/heartbeat/, src/network/, src/pi-ecosystem-judgment/, package.json, .env 等\n - 6 小时冷却期\n\nAI 接下来会用 shell_exec 工具改源码. 完成后你会在对话里看到 diff 摘要, 手动 git diff master..${newBranch} 审, 满意再 merge.`
2140
+ };
2141
+ }