@bolloon/bolloon-agent 0.3.48 → 0.3.50

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.
@@ -81,11 +81,15 @@ export class DenyPipeline {
81
81
  * 构建 permission-mode checker.
82
82
  * default 模式: 禁用 shell_exec / git_commit / git_push 等危险工具
83
83
  * bypassPermissions: 放行所有
84
+ *
85
+ * 2026-08-10: write_file/edit_file/delete_file 移出 default 禁列表 — 它们已有
86
+ * checkWritePath 写入白名单兜底 (self-improve-policy.json), 正常任务 (如写 HTML 发布
87
+ * IPFS 网站) 不该被 permission 层拦截; 实测日志: "write_file 被权限拦了" → LLM 只能
88
+ * 绕道, 任务无法推进.
84
89
  */
85
90
  static permissionChecker() {
86
91
  const DEFAULT_DENY_TOOLS = new Set([
87
92
  'shell_exec', 'git_commit', 'git_push', 'git_branch',
88
- 'delete_file', 'write_file', 'edit_file',
89
93
  ]);
90
94
  return (ctx) => {
91
95
  if (ctx.permissionMode === 'bypassPermissions') {
@@ -21,24 +21,21 @@ export const DEFAULT_MAX_REVIEWS = 2;
21
21
  /**
22
22
  * 判定: LLM 想 <final gen> 时, 该续跑一次 review 还是真正结束.
23
23
  *
24
- * 结束条件 (以下任一 finish):
25
- * - 没有明确用户需求 (无深挖对象, 不硬挖)
26
- * - 已触发 maxReviews review 续跑 (# 不无限续)
27
- * 否则 续跑一次 (提示 LLM 对齐需求深挖).
28
- *
29
- * 注意: 续跑不是无限. 达上限即结束, 保证"不过度深挖".
24
+ * 2026-08-10 重构 (用户纠正: 不要硬编码词表, 循环要智能, 自动触发后续):
25
+ * - 旧逻辑: intent → 直接 finish. 但"发布一个 ipfs 网站..." 被 classifyIntent 误判
26
+ * chitchat intentHint 1 次循环直接 <final gen> (任务没做就结束).
27
+ * - 新逻辑: **final 前总是让 LLM 自查** (是否完成用户需求/是否还有后续步骤) 结束权
28
+ * 完全交给 LLM 判断, 规则只做上限兜底 (maxReviews). 达上限才放行, 保证不过度深挖.
29
+ * - 效果: 任务场景 LLM 自查发现"还没发布" → 自动继续调工具 (自动触发后续);
30
+ * 闲聊场景 LLM 快速确认完成, 2 次自查后放行.
30
31
  */
31
32
  export function decideAfterReview(state, maxReviews = DEFAULT_MAX_REVIEWS) {
32
- const intent = (state.userIntent || '').trim();
33
- if (!intent) {
34
- return { kind: 'finish', reason: 'no-user-intent' };
35
- }
36
33
  if (state.reviewsDone >= maxReviews) {
37
34
  return { kind: 'finish', reason: `max-reviews-${maxReviews}` };
38
35
  }
39
36
  return {
40
37
  kind: 'continue-review',
41
- reason: `${state.reviewsDone + 1}/${maxReviews} 目标对齐`,
38
+ reason: `${state.reviewsDone + 1}/${maxReviews} 完成度自查`,
42
39
  hint: buildReviewHint(state, maxReviews),
43
40
  };
44
41
  }
@@ -60,13 +57,13 @@ export function buildReviewHint(state, maxReviews = DEFAULT_MAX_REVIEWS) {
60
57
  })
61
58
  .join('\n');
62
59
  }
63
- return (`[目标对齐 review ${state.reviewsDone + 1}/${maxReviews}]` +
60
+ return (`[完成度自查 ${state.reviewsDone + 1}/${maxReviews}]` +
64
61
  `\n用户需求: ${intent.slice(0, 300)}` +
65
62
  actionLines +
66
63
  `\n已完成工具: ${tools}` +
67
- `\n请对照需求逐条自查: 上面每一条动作是否真正完成了对应的子目标? 还有未完成/可深挖的子目标 → 继续调用工具推进 (注意: 同一动作已完成就不要重复执行, 直接基于已有结果推进下一步); ` +
68
- `若已满足用户原始需求 → 直接输出 <final gen> 结束.` +
69
- `\n[重要] 如果你要结束, 请先逐条对照「用户需求」确认每一项都已完成, 不要因为做了一部分就潦草收尾.`);
64
+ `\n请对照需求逐条自查: 用户需求是否每一项都真正完成了? 如果还有未完成的子目标或自然衔接的后续步骤 → 继续调用工具推进 (已完成动作不要重复, 直接基于已有结果做下一步); ` +
65
+ `如果用户需求已全部满足 → 直接输出 <final gen> 结束.` +
66
+ `\n[重要] 不要因为做了一部分就提前结束 结束前逐条对照「用户需求」确认每一项都完成.`);
70
67
  }
71
68
  /** 布尔门: 是否该继续 review (测试/消融快速断言) */
72
69
  export function shouldReviewAgain(state, maxReviews = DEFAULT_MAX_REVIEWS) {
@@ -28,6 +28,12 @@ import { initDocumentReceiver } from './p2p-document-tools.js';
28
28
  import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
29
29
  import { SkillRegistry } from '@bolloon/constraint-runtime';
30
30
  import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
31
+ /** 2026-08-10: unreported 逃生门判定 — LLM 反复不把工具结果写进回复时, 超过上限强制收尾 (防死循环) */
32
+ export function decideUnreported(unreported, retries, max) {
33
+ if (unreported <= 0)
34
+ return 'none';
35
+ return retries < max ? 'retry' : 'force-final';
36
+ }
31
37
  // 拆分后的子模块 — 重新导出保 backward compat
32
38
  export { TOOL_DEFINITIONS, } from './pi-sdk-types.js';
33
39
  export { PiSessionManager } from './pi-sdk-session-manager.js';
@@ -203,6 +209,8 @@ export class PiAgentSession {
203
209
  currentAgentId = '';
204
210
  /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
205
211
  currentIntent = 'chitchat';
212
+ /** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
213
+ currentUserInput = '';
206
214
  currentIntentHint = '';
207
215
  /**
208
216
  * 算 judgment 注入门: 失败静默, 不阻塞主对话
@@ -667,6 +675,7 @@ export class PiAgentSession {
667
675
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
668
676
  this.currentIntent = classifyIntent(input);
669
677
  this.currentIntentHint = intentHint(this.currentIntent);
678
+ this.currentUserInput = input;
670
679
  }
671
680
  catch (err) {
672
681
  console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
@@ -760,6 +769,7 @@ export class PiAgentSession {
760
769
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
761
770
  this.currentIntent = classifyIntent(userText);
762
771
  this.currentIntentHint = intentHint(this.currentIntent);
772
+ this.currentUserInput = userText;
763
773
  if (this.currentIntent !== 'chitchat') {
764
774
  onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
765
775
  }
@@ -1025,6 +1035,7 @@ export class PiAgentSession {
1025
1035
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1026
1036
  this.currentIntent = classifyIntent(input);
1027
1037
  this.currentIntentHint = intentHint(this.currentIntent);
1038
+ this.currentUserInput = input;
1028
1039
  }
1029
1040
  catch (err) {
1030
1041
  console.warn('[PiAgent] classifyIntent in pivot failed:', err);
@@ -1124,6 +1135,11 @@ ${this.getToolDefinitions()}
1124
1135
  const MAX_TOOL_CALLS_PER_LOOP = 25; // 单轮循环总工具调用上限 → 注入 hint
1125
1136
  let totalToolCallsThisLoop = 0;
1126
1137
  const lastNTools = []; // 最近 MAX_IDEMPOTENT_TOOL 次工具名, 检测重复
1138
+ // 2026-08-10: unreported 循环逃生门 — LLM 反复不把工具结果写进回复时, 3 次后强制 final (不死板)
1139
+ const MAX_UNREPORTED_RETRIES = 3;
1140
+ let unreportedRetries = 0;
1141
+ // 2026-08-10: 工具失败时的终端逃生引导 (shell_exec 白名单命令可诊断环境/推进任务)
1142
+ const SHELL_ESCAPE_HINT = ' [逃生] 若工具无法响应/报错, 可用 shell_exec 跑终端命令诊断 (白名单: ls/cat/head/tail/pwd/git status/npm run test 等), 或调整参数换一种方式完成; 不要重复调用同一失败工具.';
1127
1143
  // 2026-08-08: final 前 review 续跑 — 目标对齐 + 需求深挖 (见 loop-review.ts)
1128
1144
  // 不潦草收尾: LLM 想 <final gen> 时先跑 1-2 次 review, 达成用户需求才放行.
1129
1145
  // 上限=2 次 (用户要求"运行一两次"), 结束后按用户需求为准.
@@ -1671,7 +1687,7 @@ ${toolDefs}
1671
1687
  // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
1672
1688
  const obs = buildObservation(toolCall.name, toolCall.args, { success: false, error: result.error });
1673
1689
  const ref = buildReflection(toolCall.name, result.error, totalErrors, lastFailedToolCount);
1674
- this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
1690
+ this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
1675
1691
  if (onStream)
1676
1692
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
1677
1693
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
@@ -1695,7 +1711,7 @@ ${toolDefs}
1695
1711
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
1696
1712
  const obs = buildObservation(toolCall.name, toolCall.args, errorResult);
1697
1713
  const ref = buildReflection(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
1698
- this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) });
1714
+ this.messageHistory.push({ role: 'system', content: formatObservationWithReflection(obs, ref) + SHELL_ESCAPE_HINT });
1699
1715
  if (onStream)
1700
1716
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
1701
1717
  console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
@@ -1728,18 +1744,33 @@ ${toolDefs}
1728
1744
  console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
1729
1745
  this.successfulToolResults = [];
1730
1746
  }
1731
- if (this.successfulToolResults.length > 0 && iteration < this.MAX_REACT_ITERATIONS) {
1747
+ // 2026-08-10: 逃生门 decideUnreported: 未达上限 再提示一次; 超限 → 清空积压强制 final (防死循环)
1748
+ const unreportedDecision = decideUnreported(this.successfulToolResults.length, unreportedRetries, MAX_UNREPORTED_RETRIES);
1749
+ if (unreportedDecision === 'retry' && iteration < this.MAX_REACT_ITERATIONS) {
1750
+ unreportedRetries++;
1732
1751
  const unreported = this.successfulToolResults.length;
1733
- console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报, push hint 让其继续`);
1752
+ console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
1734
1753
  this.messageHistory.push({
1735
1754
  role: 'system',
1736
1755
  content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
1737
1756
  });
1738
1757
  if (onStream) {
1739
- onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结`, tool: 'system' });
1758
+ onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES})`, tool: 'system' });
1740
1759
  }
1741
1760
  continue;
1742
1761
  }
1762
+ else if (unreportedDecision === 'force-final') {
1763
+ // 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
1764
+ console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
1765
+ this.successfulToolResults = [];
1766
+ this.messageHistory.push({
1767
+ role: 'system',
1768
+ content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
1769
+ });
1770
+ if (onStream) {
1771
+ onStream({ type: 'status', content: `🔄 工具结果汇报超限, 强制收尾`, tool: 'system' });
1772
+ }
1773
+ }
1743
1774
  lastQualityScore = this.estimateResponseQuality(reply);
1744
1775
  // 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
1745
1776
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
@@ -1753,7 +1784,9 @@ ${toolDefs}
1753
1784
  // 达成用户需求才放行真正结束. 达上限或无需深挖则以用户需求为准结束.
1754
1785
  const reviewDecision = decideAfterReview({
1755
1786
  reviewsDone: loopReviewCount,
1756
- userIntent: this.currentIntentHint,
1787
+ // 2026-08-10: 传用户原始输入 (不是派生 intentHint) — LLM 对照原文自查完成度,
1788
+ // 未完成 → 自动继续调工具 (自动触发后续步骤)
1789
+ userIntent: this.currentUserInput,
1757
1790
  completedTools: Array.from(loopReviewCompletedTools),
1758
1791
  actionLog: loopActionLog,
1759
1792
  }, DEFAULT_MAX_REVIEWS);
package/dist/index.js CHANGED
@@ -538,6 +538,22 @@ async function startCLI(comm) {
538
538
  catch { /* config-store 失败静默, 用 env 结果 */ }
539
539
  cliAgentName = agentIdentity?.name || 'bolloon';
540
540
  cliStartTime = Date.now();
541
+ // 2026-08-10: 启动后台拉起本地 Kubo (IPFS) — fire-and-forget, 失败静默 (ipfs 工具内会再尝试).
542
+ // 背景: 实测日志 ipfs_add 失败 "发送上传请求失败: http://127.0.0.1:5001/api/v0/add" —
543
+ // Kubo daemon 没起, 而 CLI 启动路径 (startCLI) 之前从不调 checkKuboSetup (只有 Web server 调).
544
+ // BOLLOON_SKIP_KUBO=1 可禁用 (pty 测试用临时 HOME 时避免拉起指向临时 repo 的 daemon 污染 5001)
545
+ if (process.env.BOLLOON_SKIP_KUBO !== '1') {
546
+ void (async () => {
547
+ try {
548
+ const sdk = await import('@diap/sdk');
549
+ const checkKuboSetup = sdk.checkKuboSetup;
550
+ if (typeof checkKuboSetup === 'function') {
551
+ await checkKuboSetup(true, true);
552
+ }
553
+ }
554
+ catch { /* Kubo 拉起失败静默 — ipfs 工具内 ensureKuboReady 会再尝试 */ }
555
+ })();
556
+ }
541
557
  // 恢复上次 active channel (session 恢复: CLI 与 Web 共用 active-channel.json)
542
558
  try {
543
559
  const { getIdentityStore } = await import('./agents/agent-identity-store.js');
@@ -572,16 +588,21 @@ async function startCLI(comm) {
572
588
  onStart: () => inkSetTransient(`${C_DIM}(`・ω・´) 自动整理经验中...${RESET}`),
573
589
  onEnd: (r) => {
574
590
  inkSetTransient(null); // 结束后去除显示效果 (显示为空)
591
+ // 2026-08-10: 整理结果统一进 bolloon 艺术字框 (renderMessageBox 圆角框, 与反思框同款)
592
+ const boxLines = [];
575
593
  if (r && r.leftovers.length > 0) {
576
- appendLine(`${C_DIM}🧹 发现 ${r.leftovers.length} 个遗留 skills: ${r.leftovers.slice(0, 5).map(l => l.name).join(', ')}${RESET}`);
594
+ boxLines.push(`🧹 遗留 skills (${r.leftovers.length}): ${r.leftovers.slice(0, 8).map(l => l.name).join(', ')}${r.leftovers.length > 8 ? ' ...' : ''}`);
577
595
  }
578
596
  if (r && r.evolved.length > 0) {
579
- appendLine(`${C_OK}✨ 经验进化: ${r.evolved.join(', ')}${RESET}`);
597
+ boxLines.push(`✨ 经验进化: ${r.evolved.join(', ')}`);
580
598
  }
581
- // 2026-08-10: 知识层整理汇总 (Context OS/社交/智能体/judgeness/项目/画像/日志/目标)
599
+ // 知识层整理汇总 (Context OS/社交/智能体/judgeness/项目/画像/日志/目标)
582
600
  const kSections = (r?.knowledge?.sections || []).filter(s => s.handled > 0 || s.error);
583
601
  if (kSections.length > 0) {
584
- appendLine(`${C_DIM}🧠 知识整理: ${kSections.map(s => s.error ? `${s.label}✗` : `${s.label}✓`).join(' ')}${RESET}`);
602
+ boxLines.push(`🧠 知识整理: ${kSections.map(s => s.error ? `${s.label}✗` : `${s.label}✓`).join(' ')}`);
603
+ }
604
+ if (boxLines.length > 0) {
605
+ appendLine(renderMessageBox({ title: '自动整理完成', body: boxLines.join('\n'), color: C_ACCENT, maxLines: 10 }));
585
606
  }
586
607
  },
587
608
  onError: () => inkSetTransient(null),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",