@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.
- package/dist/agents/deny-pipeline.js +5 -1
- package/dist/agents/loop-review.js +12 -15
- package/dist/agents/pi-sdk.js +39 -6
- package/dist/index.js +25 -4
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 (`[
|
|
60
|
+
return (`[完成度自查 ${state.reviewsDone + 1}/${maxReviews}]` +
|
|
64
61
|
`\n用户需求: ${intent.slice(0, 300)}` +
|
|
65
62
|
actionLines +
|
|
66
63
|
`\n已完成工具: ${tools}` +
|
|
67
|
-
`\n请对照需求逐条自查:
|
|
68
|
-
|
|
69
|
-
`\n[重要]
|
|
64
|
+
`\n请对照需求逐条自查: 用户需求是否每一项都真正完成了? 如果还有未完成的子目标或自然衔接的后续步骤 → 继续调用工具推进 (已完成动作不要重复, 直接基于已有结果做下一步); ` +
|
|
65
|
+
`如果用户需求已全部满足 → 直接输出 <final gen> 结束.` +
|
|
66
|
+
`\n[重要] 不要因为做了一部分就提前结束 — 结束前逐条对照「用户需求」确认每一项都完成.`);
|
|
70
67
|
}
|
|
71
68
|
/** 布尔门: 是否该继续 review (测试/消融快速断言) */
|
|
72
69
|
export function shouldReviewAgain(state, maxReviews = DEFAULT_MAX_REVIEWS) {
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
+
boxLines.push(`✨ 经验进化: ${r.evolved.join(', ')}`);
|
|
580
598
|
}
|
|
581
|
-
//
|
|
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
|
-
|
|
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),
|