@bolloon/bolloon-agent 0.2.3 → 0.2.4
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/bin/bolloon-cli.cjs +15 -4
- package/dist/agents/pi-sdk.js +11 -5
- package/dist/agents/react-loop.js +34 -0
- package/dist/web/server.js +16 -1
- package/package.json +1 -1
package/bin/bolloon-cli.cjs
CHANGED
|
@@ -29,11 +29,22 @@ function getMainEntry() {
|
|
|
29
29
|
const binPath = require.main.filename; // /tmp/node_modules/.bin/bolloon
|
|
30
30
|
const binDir = path.dirname(binPath); // /tmp/node_modules/.bin
|
|
31
31
|
const packageDir = path.dirname(binDir); // /tmp/node_modules/@bolloon/bolloon-agent
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// 优先用 package.json 'main' 字段 (v0.2.3+ 指向 dist/cli-entry.js).
|
|
33
|
+
// 真实 npm 安装用户拿到的是编译后的 dist, 不该再依赖 tsx.
|
|
34
|
+
const pkgJsonPath = path.join(packageDir, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
38
|
+
if (pkg.main) {
|
|
39
|
+
const mainPath = path.join(packageDir, pkg.main);
|
|
40
|
+
if (fs.existsSync(mainPath)) return mainPath;
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
35
43
|
}
|
|
36
|
-
// fallback
|
|
44
|
+
// fallback: 兼容老版本 (dist/index.js 是之前默认)
|
|
45
|
+
const distIndex = path.join(packageDir, "dist", "index.js");
|
|
46
|
+
if (fs.existsSync(distIndex)) return distIndex;
|
|
47
|
+
// 最后才 fallback 到 src (开发环境)
|
|
37
48
|
return path.join(packageDir, "src", "index.ts");
|
|
38
49
|
}
|
|
39
50
|
|
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -34,6 +34,7 @@ import { ReactHarness } from '../security/react-harness.js';
|
|
|
34
34
|
import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
|
|
35
35
|
import { sessionStore as defaultSessionStore } from './session-store.js';
|
|
36
36
|
import { ToolRegistry } from './tool-registry.js';
|
|
37
|
+
import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
|
|
37
38
|
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
38
39
|
const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
|
|
39
40
|
export class PiSessionManager {
|
|
@@ -2187,10 +2188,12 @@ ${this.getToolDefinitions()}
|
|
|
2187
2188
|
while (iteration < this.MAX_REACT_ITERATIONS) {
|
|
2188
2189
|
iteration++;
|
|
2189
2190
|
// 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
|
|
2190
|
-
|
|
2191
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
|
|
2192
|
+
const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
|
|
2193
|
+
if (maxIterDecision.shouldExit) {
|
|
2191
2194
|
console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
|
|
2192
2195
|
onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
|
|
2193
|
-
finalResponse = finalResponse ||
|
|
2196
|
+
finalResponse = finalResponse || maxIterDecision.finalAnswer;
|
|
2194
2197
|
break;
|
|
2195
2198
|
}
|
|
2196
2199
|
// 停止条件 2: signal.aborted (显式 abort / 用户中断)
|
|
@@ -2217,9 +2220,10 @@ ${this.getToolDefinitions()}
|
|
|
2217
2220
|
}
|
|
2218
2221
|
// 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
|
|
2219
2222
|
// compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
|
|
2223
|
+
// 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
|
|
2220
2224
|
const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
|
|
2221
2225
|
const estimatedTokensBefore = this.estimateHistoryTokens();
|
|
2222
|
-
if (estimatedTokensBefore
|
|
2226
|
+
if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
|
|
2223
2227
|
const tokensBeforeCompact = estimatedTokensBefore;
|
|
2224
2228
|
console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
|
|
2225
2229
|
onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
|
|
@@ -2232,11 +2236,13 @@ ${this.getToolDefinitions()}
|
|
|
2232
2236
|
}
|
|
2233
2237
|
}
|
|
2234
2238
|
// 停止条件 3: context overflow (compact 后还超, 强制终止)
|
|
2239
|
+
// 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
|
|
2235
2240
|
const estimatedTokens = this.estimateHistoryTokens();
|
|
2236
|
-
|
|
2241
|
+
const overflowDecision = decideContextOverflow(estimatedTokens, this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD);
|
|
2242
|
+
if (overflowDecision.shouldExit) {
|
|
2237
2243
|
console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
|
|
2238
2244
|
onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
|
|
2239
|
-
finalResponse = finalResponse ||
|
|
2245
|
+
finalResponse = finalResponse || overflowDecision.finalAnswer;
|
|
2240
2246
|
break;
|
|
2241
2247
|
}
|
|
2242
2248
|
// 调试日志:显示每次循环开始
|
|
@@ -78,6 +78,40 @@ export function extractFinalText(reply, marker = '<final gen>') {
|
|
|
78
78
|
export function shouldForceExit(totalErrors, maxTotalErrors) {
|
|
79
79
|
return totalErrors >= maxTotalErrors;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* 2026-07-01 (v0.2.4 子任务 1): 迭代上限判定 — 纯函数.
|
|
83
|
+
* 之前 runReActLoop 在循环顶部硬编码 `if (iteration >= MAX_REACT_ITERATIONS)`,
|
|
84
|
+
* 抽出后 claude code 可独立单测 + 替代.
|
|
85
|
+
*/
|
|
86
|
+
export function decideMaxIterations(iteration, maxIterations) {
|
|
87
|
+
return {
|
|
88
|
+
shouldExit: iteration >= maxIterations,
|
|
89
|
+
finalAnswer: iteration >= maxIterations
|
|
90
|
+
? '(本轮 ReAct 循环达到最大步数, 强制结束)'
|
|
91
|
+
: '',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 2026-07-01 (v0.2.4 子任务 1): context overflow 判定 — 纯函数.
|
|
96
|
+
* 之前 runReActLoop 紧接 decideMaxIterations 之后硬编码 token 阈值检查.
|
|
97
|
+
* 抽出后行为一致 + 可测.
|
|
98
|
+
*/
|
|
99
|
+
export function decideContextOverflow(estimatedTokens, threshold) {
|
|
100
|
+
return {
|
|
101
|
+
shouldExit: estimatedTokens > threshold,
|
|
102
|
+
finalAnswer: estimatedTokens > threshold
|
|
103
|
+
? `(本轮 ReAct 循环因上下文溢出终止)`
|
|
104
|
+
: '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 2026-07-01 (v0.2.4 子任务 1): compact 触发判定 — 纯函数.
|
|
109
|
+
* 之前 runReActLoop 入口处: estimatedTokens > compactThreshold → 跑 maybeAutoCompact.
|
|
110
|
+
* 抽出纯函数后, claude code 能 dry-run "token X 时该 compact 吗".
|
|
111
|
+
*/
|
|
112
|
+
export function shouldCompactBeforeIteration(estimatedTokens, compactThreshold) {
|
|
113
|
+
return estimatedTokens > compactThreshold;
|
|
114
|
+
}
|
|
81
115
|
/**
|
|
82
116
|
* 同一工具连续失败 N 次, 提示 LLM 不要再用同一个工具 (force final).
|
|
83
117
|
*/
|
package/dist/web/server.js
CHANGED
|
@@ -1480,8 +1480,11 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1480
1480
|
runState.running = true;
|
|
1481
1481
|
runState.abortController = new AbortController();
|
|
1482
1482
|
broadcastQueueUpdate(channelId);
|
|
1483
|
+
// 2026-07-01 (v0.2.5): hoist sessionKey 到 try 外, 让 finally 块的 saveCurrentSession 能用
|
|
1484
|
+
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
1485
|
+
let agent = null;
|
|
1483
1486
|
try {
|
|
1484
|
-
|
|
1487
|
+
agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
1485
1488
|
let fullResponse = '';
|
|
1486
1489
|
// P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
|
|
1487
1490
|
let usedJudgmentIds = [];
|
|
@@ -1816,6 +1819,18 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1816
1819
|
runState.running = false;
|
|
1817
1820
|
runState.abortController = null;
|
|
1818
1821
|
broadcastQueueUpdate(channelId);
|
|
1822
|
+
// 2026-07-01 (v0.2.5): 持久化当前 messageHistory — 让 web 用户跨刷新保留对话.
|
|
1823
|
+
// saveCurrentSession 失败静默, 不阻塞 channel 状态清理.
|
|
1824
|
+
// saveCurrentSession 内部走 SessionStore (默认 ~/.bolloon/sessions/cache/<sessionKey>.json).
|
|
1825
|
+
// agent 可能在 try 抛错后仍为 null (getAgentForChannel 失败), null-guard 跳过 save.
|
|
1826
|
+
if (agent) {
|
|
1827
|
+
try {
|
|
1828
|
+
await agent.saveCurrentSession(sessionKey);
|
|
1829
|
+
}
|
|
1830
|
+
catch (saveErr) {
|
|
1831
|
+
console.warn(`[web] saveCurrentSession failed (non-fatal): ${saveErr?.message?.slice(0, 100)}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1819
1834
|
}
|
|
1820
1835
|
});
|
|
1821
1836
|
// ---------- 频道元数据后台修复队列 ----------
|