@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.
@@ -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
- const distIndex = path.join(packageDir, "dist", "index.js");
33
- if (fs.existsSync(distIndex)) {
34
- return distIndex;
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 to src (for development)
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
 
@@ -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
- if (iteration >= this.MAX_REACT_ITERATIONS) {
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 || '(本轮 ReAct 循环达到最大步数, 强制结束)';
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 > compactThreshold) {
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
- if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
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 || '(本轮 ReAct 循环因上下文溢出终止)';
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
  */
@@ -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
- const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
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
  // ---------- 频道元数据后台修复队列 ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",