@bolloon/bolloon-agent 0.6.5 → 0.6.6

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 (169) hide show
  1. package/dist/agents/chain/chain-settlement.js +0 -1
  2. package/dist/agents/chat-segmenter.js +4 -16
  3. package/dist/agents/contacts/human-directory.js +155 -0
  4. package/dist/agents/contacts/tools.js +79 -0
  5. package/dist/agents/experience-review.js +0 -3
  6. package/dist/agents/goal-flywheel/goal-change.js +3 -4
  7. package/dist/agents/goal-resume.js +0 -4
  8. package/dist/agents/intent-classifier.js +1 -1
  9. package/dist/agents/iteration-budget.js +12 -0
  10. package/dist/agents/knowledge-organizer.js +0 -2
  11. package/dist/agents/lesson-to-skill.js +0 -1
  12. package/dist/agents/loop-review.js +2 -2
  13. package/dist/agents/memory-store.js +359 -0
  14. package/dist/agents/opportunity-web-search.js +39 -0
  15. package/dist/agents/p2p-chat-tools.js +1 -2
  16. package/dist/agents/parse-tool-call.js +9 -22
  17. package/dist/agents/pi-harness.js +1 -2
  18. package/dist/agents/pi-sdk-tools.js +180 -5
  19. package/dist/agents/pi-sdk.js +143 -9
  20. package/dist/agents/process-runner.js +28 -9
  21. package/dist/agents/react-loop.js +9 -17
  22. package/dist/agents/runner-resolver.js +4 -3
  23. package/dist/agents/skill-loader.js +22 -15
  24. package/dist/agents/skills-manager.js +1 -25
  25. package/dist/agents/workflow-pivot-loop.js +152 -61
  26. package/dist/bootstrap/context-collector.js +2 -4
  27. package/dist/bootstrap/context-hierarchy.js +13 -1
  28. package/dist/cli/ink-app.js +44 -17
  29. package/dist/cli/intent-command.js +22 -12
  30. package/dist/cli/loading-tui.js +8 -2
  31. package/dist/cli/log-gate.js +40 -8
  32. package/dist/cli/mention-data.js +1 -0
  33. package/dist/cli/reply-hygiene.js +22 -0
  34. package/dist/cli/trace-line.js +61 -2
  35. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  36. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -1
  37. package/dist/electron-build/agents/chain/chain-settlement.js +0 -1
  38. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -1
  39. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/human-directory.js +204 -0
  41. package/dist/electron-build/agents/contacts/human-directory.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/policy.js.map +1 -1
  43. package/dist/electron-build/agents/contacts/tools.js +112 -0
  44. package/dist/electron-build/agents/contacts/tools.js.map +1 -1
  45. package/dist/electron-build/agents/experience-review.js +0 -3
  46. package/dist/electron-build/agents/experience-review.js.map +1 -1
  47. package/dist/electron-build/agents/external-events.js.map +1 -1
  48. package/dist/electron-build/agents/goal-flywheel/goal-change.js +3 -4
  49. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -1
  50. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -1
  51. package/dist/electron-build/agents/goal-resume.js +0 -4
  52. package/dist/electron-build/agents/goal-resume.js.map +1 -1
  53. package/dist/electron-build/agents/intent-classifier.js +1 -1
  54. package/dist/electron-build/agents/lesson-to-skill.js +0 -1
  55. package/dist/electron-build/agents/lesson-to-skill.js.map +1 -1
  56. package/dist/electron-build/agents/loop-review.js +2 -2
  57. package/dist/electron-build/agents/loop-review.js.map +1 -1
  58. package/dist/electron-build/agents/memory-recall.js.map +1 -1
  59. package/dist/electron-build/agents/memory-store.js +404 -0
  60. package/dist/electron-build/agents/memory-store.js.map +1 -0
  61. package/dist/electron-build/agents/parse-tool-call.js +9 -22
  62. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  63. package/dist/electron-build/agents/pi-harness.js +1 -2
  64. package/dist/electron-build/agents/pi-harness.js.map +1 -1
  65. package/dist/electron-build/agents/pi-sdk-tools.js +180 -5
  66. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  67. package/dist/electron-build/agents/pi-sdk.js +143 -9
  68. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  69. package/dist/electron-build/agents/pre-tool-validator.js.map +1 -1
  70. package/dist/electron-build/agents/process-runner.js +28 -9
  71. package/dist/electron-build/agents/process-runner.js.map +1 -1
  72. package/dist/electron-build/agents/runner-resolver.js +4 -3
  73. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  74. package/dist/electron-build/agents/skill-loader.js +22 -16
  75. package/dist/electron-build/agents/skill-loader.js.map +1 -1
  76. package/dist/electron-build/agents/skill-supervisor-link.js.map +1 -1
  77. package/dist/electron-build/agents/skills-manager.js +0 -24
  78. package/dist/electron-build/agents/skills-manager.js.map +1 -1
  79. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  80. package/dist/electron-build/agents/workflow-engine.js.map +1 -1
  81. package/dist/electron-build/agents/workflow-pivot-loop.js +155 -62
  82. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  83. package/dist/electron-build/agents/x402/goal-run-bridge.js.map +1 -1
  84. package/dist/electron-build/bollharness-integration/gate-state-machine.js.map +1 -1
  85. package/dist/electron-build/bootstrap/context-collector.js +2 -4
  86. package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
  87. package/dist/electron-build/bootstrap/context-hierarchy.js +13 -1
  88. package/dist/electron-build/bootstrap/context-hierarchy.js.map +1 -1
  89. package/dist/electron-build/electron-preload.js.map +1 -1
  90. package/dist/electron-build/external-engines/delegate.js.map +1 -1
  91. package/dist/electron-build/kernel/channel-actor.js +31 -2
  92. package/dist/electron-build/kernel/channel-actor.js.map +1 -1
  93. package/dist/electron-build/kernel/model-runtime.js +8 -1
  94. package/dist/electron-build/kernel/model-runtime.js.map +1 -1
  95. package/dist/electron-build/llm/config-store.js +20 -0
  96. package/dist/electron-build/llm/config-store.js.map +1 -1
  97. package/dist/electron-build/llm/embedding-adapter.js +75 -0
  98. package/dist/electron-build/llm/embedding-adapter.js.map +1 -0
  99. package/dist/electron-build/llm/pi-ai.js +96 -35
  100. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  101. package/dist/electron-build/network/agent-network.js.map +1 -1
  102. package/dist/electron-build/orbitdb/memory-sync.js +76 -0
  103. package/dist/electron-build/orbitdb/memory-sync.js.map +1 -0
  104. package/dist/electron-build/pi-ecosystem-judgment/adaptive-scan.js.map +1 -1
  105. package/dist/electron-build/security/tool-gate.js +2 -0
  106. package/dist/electron-build/security/tool-gate.js.map +1 -1
  107. package/dist/electron-build/setup/setup-store.js +34 -6
  108. package/dist/electron-build/setup/setup-store.js.map +1 -1
  109. package/dist/electron-build/social/heartbeat.js +18 -5
  110. package/dist/electron-build/social/heartbeat.js.map +1 -1
  111. package/dist/electron-build/utils/update-manager.js.map +1 -1
  112. package/dist/index.js +250 -12
  113. package/dist/ios/bolloon-web.json +2 -2
  114. package/dist/ios/client.js +74 -35
  115. package/dist/ios/icons/icon-1400x1400.png +0 -0
  116. package/dist/ios/icons/icon-1400x1400.webp +0 -0
  117. package/dist/ios/index.html +106 -107
  118. package/dist/ios/mobile-agent.js +346 -45
  119. package/dist/ios/mobile-core.js +7303 -4590
  120. package/dist/ios/mobile-harness.js +1004 -0
  121. package/dist/ios/mobile-orbitdb.js +194 -0
  122. package/dist/ios/mobile-persona.js +80 -0
  123. package/dist/ios/mobile-privacy.js +2 -3
  124. package/dist/ios/mobile.css +286 -21
  125. package/dist/ios/mobile.html +106 -107
  126. package/dist/ios/mobile.js +300 -132
  127. package/dist/ios/routes-world.js +4 -1
  128. package/dist/ios/server.js +3 -1
  129. package/dist/ios/style.css +13 -5
  130. package/dist/ios/ui/message-renderer.js +4 -18
  131. package/dist/ios/ui/step-timeline.js +14 -1
  132. package/dist/kernel/channel-actor.js +30 -2
  133. package/dist/kernel/gate-scan.js +1 -1
  134. package/dist/kernel/index.js +33 -0
  135. package/dist/kernel/model-runtime.js +8 -1
  136. package/dist/kernel/plan.js +3 -3
  137. package/dist/kernel/roster.js +3 -3
  138. package/dist/llm/config-store.js +20 -0
  139. package/dist/llm/embedding-adapter.js +69 -0
  140. package/dist/llm/pi-ai.js +94 -34
  141. package/dist/orbitdb/memory-sync.js +70 -0
  142. package/dist/pi-ecosystem-judgment/adaptive-scan.js +1 -1
  143. package/dist/security/tool-gate.js +2 -0
  144. package/dist/setup/setup-store.js +34 -6
  145. package/dist/social/heartbeat.js +18 -5
  146. package/dist/web/bolloon-web.json +3 -3
  147. package/dist/web/client.js +74 -35
  148. package/dist/web/icons/icon-1400x1400.png +0 -0
  149. package/dist/web/icons/icon-1400x1400.webp +0 -0
  150. package/dist/web/index.html +37 -25
  151. package/dist/web/mobile-agent.js +351 -45
  152. package/dist/web/mobile-contacts.js +0 -4
  153. package/dist/web/mobile-core.js +7430 -4590
  154. package/dist/web/mobile-harness.js +1004 -0
  155. package/dist/web/mobile-orbitdb.js +194 -0
  156. package/dist/web/mobile-persona.js +80 -0
  157. package/dist/web/mobile-privacy.js +2 -3
  158. package/dist/web/mobile.css +286 -21
  159. package/dist/web/mobile.html +121 -107
  160. package/dist/web/mobile.js +409 -136
  161. package/dist/web/routes-contacts.js +94 -1
  162. package/dist/web/routes-world.js +4 -1
  163. package/dist/web/server.js +4 -2
  164. package/dist/web/style.css +13 -5
  165. package/dist/web/sw.js +1 -1
  166. package/dist/web/ui/message-renderer.js +4 -18
  167. package/dist/web/ui/step-timeline.js +14 -1
  168. package/package.json +3 -2
  169. package/scripts/build-web.ts +16 -1
@@ -13,6 +13,7 @@ import { checkWritePath } from './shell-guard.js';
13
13
  import { p2pDocumentTools } from './p2p-document-tools.js';
14
14
  import { runSelfImproveLoop } from './pi-sdk-session-factory.js';
15
15
  import { getMinimax } from '../constraints/index.js';
16
+ import { llmConfigStore } from '../llm/config-store.js';
16
17
  import { delegateToEngine, buildDelegateCommand } from '../external-engines/delegate.js';
17
18
  import { spawnBackground } from './process-runner.js';
18
19
  import { DELEGATE_ENV_MARK } from './background-notices.js';
@@ -858,11 +859,9 @@ export function registerBuiltinTools(ctx) {
858
859
  // 与 shell_exec 的区别: 直接接受完整 shell 命令字符串, 更适合模型自主写命令.
859
860
  // 2026-10-01 (优化 #2 v1): **工具发现** —— 125 个工具不可能都记住, 给一个"按关键词找工具"的入口。
860
861
  // 空关键词只回**分类索引**(名称清单), 有关键词才回"名称+用途摘要" ⇒ 不刷屏。
861
- // 2026-10-01 (用户: 「skills 教训学习, bolloon 有吗, 从 hermes 触发的方法是什么, 学一下」):
862
- // **实测缺口**: `skill-loader` 把 1300 个技能**注册**进注册表 ✓, 但 `describeSkill` 只 import 从没调用 ✗
863
- // ⇒ 技能**描述从不进提示** ⇒ 模型不知道它们存在 ⇒ 技能触发链**断在这里** ✗。
864
- // 她那边的做法: 启动自动装载 + 按**描述**在提示里露面 + 需要时用 `skill_view` 读全文 ✓。
865
- // bolloon 有 1300 个(她百来号) ⇒ 全量注入会撑爆提示 ✗ ⇒ 改成**两个发现工具**(和 `list_tools` 同一思路 ✓)。
862
+ // 2026-10-01 (用户: 「skills 教训学习, bolloon 有吗」): 技能发现 = 两个工具而非全量注入 —
863
+ // 技能库上千个, 全量注入会撑爆提示 (历史: 19 个 skill 曾把 systemPrompt 顶到 22K 导致模型不输出)。
864
+ // 做法同 `list_tools`: 按关键词找 (`list_skills`) → 读全文 (`read_skill`)。
866
865
  ctx.tools.set('list_skills', {
867
866
  name: 'list_skills',
868
867
  description: '用于**找技能**: 给关键词(中文/英文都行)⇒ 返回已装载技能的名字+说明(≤20 条); 不给关键词 ⇒ 先给总数与前若干条. 找到后用 read_skill 读全文再照做.',
@@ -4445,6 +4444,182 @@ export function registerWalletTools(ctx) {
4445
4444
  }
4446
4445
  }
4447
4446
  });
4447
+ // 2026-10-14 (leo: 「需要向量记忆和去中心化记忆」, P0): 结构化记忆工具
4448
+ // —— MemoryEntry 本地 JSONL 持久化; embedding 向量 (P1) / OrbitDB 复制 (P3) 后续接
4449
+ const memoryAgentId = () => ctx.identity?.did || String(ctx.cwd || '');
4450
+ /**
4451
+ * P1: 记忆 embedding 配置。读 llm-config 的 `embedding` 段:
4452
+ * { embedding: { provider, model, baseUrl?, apiKey?, groupId? } }
4453
+ * 未配置 / provider 不可用 → { provider: 'none' } (记忆层回退关键词, 行为不变)。
4454
+ * 诚实边界: 本仓现有 provider (glm 中转 / minimax 订阅 key) 实测无可用 embedding,
4455
+ * 故默认 none; 将来任一供应商 key 就绪, 在此配置段填上即可, 记忆层零改动。
4456
+ */
4457
+ const getMemoryEmbeddingConfig = () => {
4458
+ try {
4459
+ const cfg = llmConfigStore?.config;
4460
+ const emb = cfg?.embedding;
4461
+ if (emb && typeof emb === 'object' && emb.provider && emb.provider !== 'none') {
4462
+ return {
4463
+ provider: String(emb.provider),
4464
+ model: emb.model ? String(emb.model) : undefined,
4465
+ baseUrl: emb.baseUrl ? String(emb.baseUrl) : undefined,
4466
+ apiKey: emb.apiKey ? String(emb.apiKey) : undefined,
4467
+ groupId: emb.groupId ? String(emb.groupId) : undefined,
4468
+ };
4469
+ }
4470
+ }
4471
+ catch { /* 配置读取失败 → none */ }
4472
+ return { provider: 'none' };
4473
+ };
4474
+ ctx.tools.set('memory_save', {
4475
+ name: 'memory_save',
4476
+ description: '保存一条**结构化长期记忆** (事实/偏好/洞察/事件/关系/技能反馈)。用于: 用户明确说"记住 X"、反复出现的偏好、值得跨会话保留的事实。自动去重 (同内容不重复), 自动打分重要性。',
4477
+ parameters: {
4478
+ content: '记忆内容 (必填, ≥2 字符)',
4479
+ kind: '类型: fact | insight | preference | event | relationship | skill-feedback (默认 fact)',
4480
+ importance: '手动重要性 0-1 (可选, 缺省自动启发式打分)',
4481
+ refs: '关联证据 JSON 数组 (可选, 如 goalId/runId)',
4482
+ },
4483
+ execute: async (args) => {
4484
+ try {
4485
+ const { getMemoryStore } = await import('./memory-store.js');
4486
+ const store = getMemoryStore(memoryAgentId());
4487
+ // P1: 有 embedding adapter 时 save 顺便向量化 (存 vec, 供语义召回)
4488
+ let adapter;
4489
+ try {
4490
+ const { createEmbeddingAdapter } = await import('../llm/embedding-adapter.js');
4491
+ const a = createEmbeddingAdapter(getMemoryEmbeddingConfig());
4492
+ if (a.provider !== 'none')
4493
+ adapter = a;
4494
+ }
4495
+ catch { /* 无向量能力 → 只存文本 */ }
4496
+ const entry = await store.save({
4497
+ content: String(args.content || '').trim(),
4498
+ kind: args.kind || 'fact',
4499
+ importance: args.importance !== undefined ? Number(args.importance) : undefined,
4500
+ refs: Array.isArray(args.refs) ? args.refs.map(String) : undefined,
4501
+ source: 'tool',
4502
+ }, adapter);
4503
+ // P3: 条目级 OrbitDB 同步 (失败静默 — 本地记忆仍权威)
4504
+ try {
4505
+ const { syncMemoryEntryToOrbit } = await import('../orbitdb/memory-sync.js');
4506
+ await syncMemoryEntryToOrbit(entry);
4507
+ }
4508
+ catch { /* 同步失败不阻塞保存 */ }
4509
+ return { success: true, output: `✅ 已保存记忆 #${entry.id.slice(0, 8)} (${entry.kind}, 重要性 ${entry.importance.toFixed(2)})` };
4510
+ }
4511
+ catch (e) {
4512
+ return { success: false, error: `memory_save 失败: ${String(e.message || e)}` };
4513
+ }
4514
+ }
4515
+ });
4516
+ ctx.tools.set('memory_recall', {
4517
+ name: 'memory_recall',
4518
+ description: '按查询关键词**召回长期记忆** (事实/偏好/洞察)。每轮对话已自动做记忆召回, 这里用于显式深挖: 用户说"你还记得我之前说的 X"、或当前任务需要历史上下文时调用。',
4519
+ parameters: {
4520
+ query: '查询内容 (必填, 用关键词)',
4521
+ limit: '返回条数 (默认 3)',
4522
+ },
4523
+ execute: async (args) => {
4524
+ try {
4525
+ const { getMemoryStore, formatRecallBlock } = await import('./memory-store.js');
4526
+ const store = getMemoryStore(memoryAgentId());
4527
+ await store.load();
4528
+ // P3: 召回前先拉远端条目合并 (LWW, updatedAt 大者胜; 网络不可用静默)
4529
+ try {
4530
+ const { fetchRemoteEntries } = await import('../orbitdb/memory-sync.js');
4531
+ const remote = await fetchRemoteEntries(memoryAgentId());
4532
+ if (remote.length > 0)
4533
+ await store.mergeRemote(remote);
4534
+ }
4535
+ catch { /* 远端拉取失败 → 只用本地 */ }
4536
+ const query = String(args.query || '');
4537
+ // P1: 有 embedding adapter 时走向量召回; 无 → 关键词回退 (P0 行为不变)
4538
+ let queryVec;
4539
+ try {
4540
+ const { createEmbeddingAdapter } = await import('../llm/embedding-adapter.js');
4541
+ const adapter = createEmbeddingAdapter(getMemoryEmbeddingConfig());
4542
+ if (adapter.provider !== 'none' && query.trim()) {
4543
+ queryVec = await adapter.embed(query);
4544
+ }
4545
+ }
4546
+ catch { /* vector 不可用 → 关键词回退 */ }
4547
+ const hits = await store.recall({
4548
+ query,
4549
+ limit: args.limit ? Number(args.limit) : 3,
4550
+ queryVec,
4551
+ });
4552
+ if (!hits.length)
4553
+ return { success: true, output: '📭 没有找到相关记忆 (可能是新话题或记忆未保存)' };
4554
+ return { success: true, output: formatRecallBlock(hits) };
4555
+ }
4556
+ catch (e) {
4557
+ return { success: false, error: `memory_recall 失败: ${String(e.message || e)}` };
4558
+ }
4559
+ }
4560
+ });
4561
+ // P2: 价值分层工具 — 固化 / 遗忘 / 统计 (2026-10-11, decentralized-memory-design §2.7)
4562
+ ctx.tools.set('memory_crystallize', {
4563
+ name: 'memory_crystallize',
4564
+ description: '固化一条记忆 (crystallize): 标记为「重要, 不淡忘」+ importance 加成。用于: 用户明确说"这个要记住"、反复确认的重要约定、多次命中的核心事实。返回固化后的条目。',
4565
+ parameters: {
4566
+ entryId: '记忆 ID (从 memory_recall 的返回里拿, 形如 #xxxxxxxx)',
4567
+ },
4568
+ execute: async (args) => {
4569
+ try {
4570
+ const { getMemoryStore } = await import('./memory-store.js');
4571
+ const store = getMemoryStore(memoryAgentId());
4572
+ await store.load();
4573
+ const id = String(args.entryId || '').replace(/^#/, '');
4574
+ const e = await store.crystallize(id);
4575
+ if (!e)
4576
+ return { success: false, error: `未找到记忆 #${id}` };
4577
+ return { success: true, output: `✅ 已固化记忆 #${e.id.slice(0, 8)} (${e.kind}, importance ${e.importance.toFixed(2)}, 不再淡忘)` };
4578
+ }
4579
+ catch (e) {
4580
+ return { success: false, error: `memory_crystallize 失败: ${String(e.message || e)}` };
4581
+ }
4582
+ }
4583
+ });
4584
+ ctx.tools.set('memory_forget', {
4585
+ name: 'memory_forget',
4586
+ description: '删除一条记忆 (entryId 从 memory_recall 返回拿)。用于: 用户说"忘掉这个/删掉这条记忆"、记忆过时/错误。删除不可恢复。',
4587
+ parameters: {
4588
+ entryId: '记忆 ID (形如 #xxxxxxxx)',
4589
+ },
4590
+ execute: async (args) => {
4591
+ try {
4592
+ const { getMemoryStore } = await import('./memory-store.js');
4593
+ const store = getMemoryStore(memoryAgentId());
4594
+ await store.load();
4595
+ const id = String(args.entryId || '').replace(/^#/, '');
4596
+ const ok = await store.remove(id);
4597
+ if (!ok)
4598
+ return { success: false, error: `未找到记忆 #${id}` };
4599
+ return { success: true, output: `🗑️ 已删除记忆 #${id.slice(0, 8)}` };
4600
+ }
4601
+ catch (e) {
4602
+ return { success: false, error: `memory_forget 失败: ${String(e.message || e)}` };
4603
+ }
4604
+ }
4605
+ });
4606
+ ctx.tools.set('memory_stats', {
4607
+ name: 'memory_stats',
4608
+ description: '查看记忆库统计: 总条数 / 已固化 / 已淡化 (decay)。用于: 用户问"我存了多少记忆"、诊断记忆衰减是否生效。',
4609
+ parameters: {},
4610
+ execute: async () => {
4611
+ try {
4612
+ const { getMemoryStore } = await import('./memory-store.js');
4613
+ const store = getMemoryStore(memoryAgentId());
4614
+ await store.load();
4615
+ const s = store.stats();
4616
+ return { success: true, output: `📊 记忆统计: 共 ${s.total} 条 · 已固化 ${s.crystallized} · 已淡化 ${s.decayed}` };
4617
+ }
4618
+ catch (e) {
4619
+ return { success: false, error: `memory_stats 失败: ${String(e.message || e)}` };
4620
+ }
4621
+ }
4622
+ });
4448
4623
  // 2026-07-28: 注册 LSP 工具 (代码智能)
4449
4624
  // registerBuiltinTools 不是 async, 用同步 import 兜底
4450
4625
  try {
@@ -63,6 +63,7 @@ import { initDocumentReceiver } from './p2p-document-tools.js';
63
63
  import { startRun, recordStep, finishRun, readRun, recordDegradation, recordHarnessEvent, recordRecovery, setRunStatus, prepareResume, buildResumeInstruction, argsDigestOf, repeatedFailureCount, classifyError as classifyRunError } from './run-store.js';
64
64
  import { createGoal, attachRun } from './goal-store.js';
65
65
  import { captureRunModelConfig } from '../llm/model-selection.js';
66
+ // 2026-09-26: 工具名出网净化 (pi-ai.ts 唯一边界) + 回程派发还原 (原名 ↔ API 名)
66
67
  // 2026-09-28 (前缀 KV 可命中): CURRENT TURN 注入标记 —— "当前轮是否已注入"的唯一判据
67
68
  import { CURRENT_TURN_MARKER } from '../llm/pi-ai.js';
68
69
  import { PiAgentHarness } from './pi-harness.js';
@@ -200,6 +201,10 @@ export class PiAgentSession {
200
201
  identity;
201
202
  persona = null;
202
203
  minimaxAvailable = false;
204
+ /** 2026-10-13: pivot 路径会话累计真实消耗 (每轮 loop 的 totalTokens 累加, 刻度随会话单调增长) */
205
+ pivotCumulativeTokens = 0;
206
+ /** 2026-10-13: 上下文管理周期压缩在飞标志 (防一轮里重复触发) */
207
+ _ctxCycleCompacting = false;
203
208
  workflows = new Map();
204
209
  constraintLayer;
205
210
  workflowEngine;
@@ -294,6 +299,8 @@ export class PiAgentSession {
294
299
  /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
295
300
  _toolRegistry = new ToolRegistry();
296
301
  skillRegistry = new SkillRegistry();
302
+ /** 2026-10-13: 技能扫描 promise (loadSkills 保存, buildSkillsIndex await) — 确保首次对话前技能已就绪 */
303
+ _skillsReady = Promise.resolve();
297
304
  /** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
298
305
  cachedToolDefinitions = '';
299
306
  /** M2.4: 缓存 persona section */
@@ -327,6 +334,41 @@ export class PiAgentSession {
327
334
  return bg ? `${bg}\n${plans}` : plans;
328
335
  }
329
336
  cachedPersonaSection = '';
337
+ /**
338
+ * 2026-10-13 (leo: 「重新阅读理解,整体开发整个技能的实现方式… 成整个加载循环」):
339
+ * 技能库索引摘要 —— 注入 systemPrompt, 让模型**知道技能存在**(名字+一句话描述),
340
+ * 需要时用 `list_skills`(按关键词找) → `read_skill`(读全文, 结果进上下文) 按需加载。
341
+ * 为什么必须有: 技能描述**从不进提示** ⇒ 模型不知道 `read_skill` 该读什么 ⇒ 技能加载循环
342
+ * 断在起点 (此前 `describeSkill` 只 import 从不调用, 见 pi-sdk-tools.ts:915 注释)。
343
+ * 预算控制: 只列前 40 个 + 总数, 完整清单由 `list_skills` 按需取 —— 不撑爆 systemPrompt。
344
+ * 失败静默(返回 ''): 技能库读不到时循环退化成 "无技能", 不阻塞主流程。
345
+ */
346
+ async buildSkillsIndex() {
347
+ // 2026-10-13 (leo: 「成整个加载循环」): 先等技能扫描 promise 落地再读 registry ——
348
+ // loadSkills 是构造期 fire-and-forget, 不 await 的话首次对话时 registry 还是空的。
349
+ try {
350
+ await this._skillsReady;
351
+ }
352
+ catch { /* 扫描失败不影响 (registry 为空时返回 '') */ }
353
+ try {
354
+ const all = this.skillRegistry.list();
355
+ if (!all.length)
356
+ return '';
357
+ const budget = 40;
358
+ const lines = all.slice(0, budget).map((sk) => {
359
+ const name = String(sk?.name ?? '').trim();
360
+ const desc = String(sk?.description ?? '').trim().replace(/\s+/g, ' ').slice(0, 100);
361
+ return name ? `- ${name}: ${desc}` : null;
362
+ }).filter(Boolean);
363
+ if (!lines.length)
364
+ return '';
365
+ const more = all.length > budget ? `\n…还有 ${all.length - budget} 个 (用 list_skills 按关键词找)` : '';
366
+ return `## 技能库 (${all.length} 个技能 — 需要时用 list_skills 找, read_skill 读全文执行; 内容进上下文, 按需加载不占首屏)\n${lines.join('\n')}${more}`;
367
+ }
368
+ catch {
369
+ return '';
370
+ }
371
+ }
330
372
  /** 本轮迭代预算 (每轮重置; 批处理工具会退还 —— 见 iteration-budget.ts) */
331
373
  /** 上一个工具调用的参数指纹 (遥测判"是否重复调用") */
332
374
  lastToolSig = null;
@@ -808,14 +850,16 @@ export class PiAgentSession {
808
850
  * 2026-07-04: 移除 18 个 bollharness builtin skill (findBolloonBuiltinSkillsPath).
809
851
  * 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
810
852
  * 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
811
- * `<final gen>`, session 落盘拿不到最终回答.
853
+ * 最终回答, session 落盘拿不到最终回答.
812
854
  * 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
813
855
  *
814
856
  * 静默忽略不存在的目录.
815
857
  */
816
858
  loadSkills(paths) {
817
859
  const resolved = (paths && paths.length > 0) ? paths : defaultSkillPaths(os.homedir(), this.cwd);
818
- loadSkillsFromPaths(resolved)
860
+ // 2026-10-13 (leo: 「成整个加载循环」): 保存 promise 供 buildSkillsIndex await ——
861
+ // 原来 fire-and-forget, 构造后立即读 registry = 0 ⇒ 技能索引空 ⇒ 模型不知道技能 ⇒ 循环断在起点。
862
+ this._skillsReady = loadSkillsFromPaths(resolved)
819
863
  .then((skills) => {
820
864
  for (const s of skills) {
821
865
  if (this.skillRegistry.has(s.name)) {
@@ -912,7 +956,7 @@ export class PiAgentSession {
912
956
  _inboxMessages: this._inboxMessages,
913
957
  getChannelWallet: async () => {
914
958
  try {
915
- const { CHANNELS_PATH } = await import('../web/server-types.js');
959
+ const {} = await import('../web/server-types.js');
916
960
  const { loadChannels } = await import('../web/server-storage.js');
917
961
  const channels = await loadChannels();
918
962
  const ch = channels.find((c) => c.id === this.actor.state.channelId);
@@ -1035,10 +1079,50 @@ export class PiAgentSession {
1035
1079
  * 之前只有 runReActLoop 迭代内上报, chitchat/fallback/pivot 路径状态栏恒 0 */
1036
1080
  reportUsageToContextManager() {
1037
1081
  try {
1038
- getContextManager().updateUsage(this.estimateHistoryTokens());
1082
+ const cm = getContextManager();
1083
+ const estimated = this.estimateHistoryTokens();
1084
+ // 2026-10-13 (leo: 刻度被覆盖回 0): pivot 路径在本轮内已上报真实消耗
1085
+ // (promptWithPivotLoop 内 updateUsage(pivotCumulative)); 这里若再用 actor 历史估算**覆盖**,
1086
+ // 刻度会被拉回 ~0 (pivot 路径 actor 历史每轮只有 1-2 条)。⇒ 取 max, 只升不降。
1087
+ cm.updateUsage(Math.max(estimated, cm.getUsage().usedTokens));
1039
1088
  }
1040
1089
  catch { /* 非致命 */ }
1041
1090
  }
1091
+ /**
1092
+ * 2026-10-13: 上下文管理周期 — 阈值触发压缩 (pivot 路径)。
1093
+ * ContextManager 的 markCompressStart/Complete + snapshot 原先只在 promptStream 的
1094
+ * maybeAutoCompact 里被调用; CLI 主路径 (prompt → promptWithPivotLoop) 从没触发过 ⇒
1095
+ * 刻度显示到 'compressing' 但永远不会真压缩。这里补上最小周期:
1096
+ * 累计上报 → stage 达压缩阈值 → 同步前 3 层压缩 (compressHistorySync, 零 LLM 成本) → snapshot。
1097
+ * 失败静默, 不阻塞主流程。
1098
+ */
1099
+ async runContextCycle(usage) {
1100
+ try {
1101
+ const cm = getContextManager();
1102
+ const history = this.actor.state.messageHistory;
1103
+ if (history.length < 10) {
1104
+ console.log(`[PiAgent] 上下文管理周期: 刻度达压缩阈值 (${Math.round(usage.pct * 100)}%), 历史仅 ${history.length} 条, 跳过压缩 (stage=compressing 留作警示)`);
1105
+ return;
1106
+ }
1107
+ const beforeTokens = this.estimateHistoryTokens();
1108
+ cm.markCompressStart(beforeTokens);
1109
+ const compacted = this.compressHistorySync(history);
1110
+ this.replaceHistory(compacted);
1111
+ const afterTokens = this.estimateHistoryTokens();
1112
+ const snap = cm.makeSnapshot({
1113
+ beforeTokens,
1114
+ afterTokens,
1115
+ summary: `pivot 阈值压缩 (同步前3层): ${beforeTokens.toLocaleString()} → ${afterTokens.toLocaleString()} tokens`,
1116
+ agentId: this.actor.state.agentId,
1117
+ channelId: this.actor.state.channelId,
1118
+ });
1119
+ cm.markCompressComplete(snap);
1120
+ console.log(`[PiAgent] 上下文管理周期: 压缩完成 ${beforeTokens.toLocaleString()}→${afterTokens.toLocaleString()} tokens, stage=${cm.getUsage().stage}, saved=${cm.getUsage().lastSavedTokens.toLocaleString()}`);
1121
+ }
1122
+ catch (err) {
1123
+ console.warn('[PiAgent] 上下文管理周期: 压缩失败 (不阻塞主流程):', String(err?.message ?? err).slice(0, 160));
1124
+ }
1125
+ }
1042
1126
  // ============================================================
1043
1127
  // 2026-08-08: 运行轨迹采集 (trajectory) — 每轮运行 → 落盘 + OrbitDB, 失败静默
1044
1128
  // ============================================================
@@ -1119,6 +1203,32 @@ export class PiAgentSession {
1119
1203
  role: 'user',
1120
1204
  content: input
1121
1205
  });
1206
+ // 2026-10-13 (leo: 「记忆加载和管理要变成一种执行的规范, 我刚开始就被复利性质地使用过, 所以这个没有实现」):
1207
+ // 记忆加载原来只在 promptStream 调用 (1514), CLI 主路径 prompt() 从不召回 ⇒ 命令行 Agent 根本没有"记忆"
1208
+ // (用户贴屏: 「没有在工具的调用初始化过程当中看到记忆加载和记忆管理」)。这里与 promptStream 同款召回,
1209
+ // 注入 systemPrompt 的 contextHintAddition —— 每轮执行都过这道闸, 就是"执行的规范"。
1210
+ try {
1211
+ const { recallMemory } = await import('./memory-recall.js');
1212
+ const recalled = await recallMemory({ query: input, agentId: this.actor.state.agentId || this.peerId || '' });
1213
+ if (recalled) {
1214
+ this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
1215
+ }
1216
+ }
1217
+ catch { /* 记忆召回失败静默 (增强层) */ }
1218
+ // 2026-10-14 (P0, decentralized-memory-design §2.7): 结构化记忆 (MemoryEntry) 每轮同步召回 —
1219
+ // 与上面会话摘要召回互补: 摘要是「整段会话的压缩」, 这里是「单条结构化记忆」 (带 kind/importance)。
1220
+ // 借鉴 Hermes 冻结快照: 写入落盘不改 prompt, 召回走每轮注入 (progressive disclosure: 只带摘要+权重)。
1221
+ try {
1222
+ const { getMemoryStore, formatRecallBlock } = await import('./memory-store.js');
1223
+ const store = getMemoryStore(this.actor.state.agentId || this.peerId || '');
1224
+ await store.load();
1225
+ const hits = await store.recall({ query: input, limit: 3 });
1226
+ const block = formatRecallBlock(hits);
1227
+ if (block) {
1228
+ this.contextHintAddition = [this.contextHintAddition, block].filter(Boolean).join('\n\n');
1229
+ }
1230
+ }
1231
+ catch { /* 结构化记忆召回失败静默 (增强层, 不阻塞对话) */ }
1122
1232
  // K9 (2026-10-02): 同 `promptWithPivotLoop` —— **回合入口的可用性判据必须问当前适配器**。
1123
1233
  // 这类 `!minimaxAvailable` 闸一共 6 处: 1223(prompt)/1466(promptStream)/1714(pivot, 已修)
1124
1234
  // 是**回合入口** ⇒ 全部改用 `inferenceAvailable()`; 其余三处是 Pi 自己的文档类功能
@@ -1526,7 +1636,11 @@ export class PiAgentSession {
1526
1636
  if (attempt > MAX_LOOP_RETRIES) {
1527
1637
  console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
1528
1638
  if (onStream) {
1529
- onStream({ type: 'status', content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
1639
+ // 2026-10-14 (leo: 「loop 自动重试提示漏进内容栏」): 老代码这条**漏标 internal** ——
1640
+ // `onStream({type:'status'})` 事件在 CLI 渲染层 (index.ts) 只认发送点的显式 internal 声明,
1641
+ // 没标 → 走 appendLine 上内容栏。与下面 1717 行的「重试中」事件对齐, 补标 internal:true
1642
+ // ⇒ 同一判据 (appendInternalRunLog: 落盘 + --verbose 上屏), 不进对话流。
1643
+ onStream({ type: 'status', internal: true, content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
1530
1644
  }
1531
1645
  result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
1532
1646
  break;
@@ -1850,6 +1964,8 @@ ${PROACTIVE_WORK_DISCIPLINE}
1850
1964
 
1851
1965
  ${await this.renderActivePlansSection()}
1852
1966
 
1967
+ ${await this.buildSkillsIndex()}
1968
+
1853
1969
  工作模式:
1854
1970
  1. 理解用户自然语言请求
1855
1971
  2. 分析需要哪些工具来完成
@@ -1860,7 +1976,7 @@ ${await this.renderActivePlansSection()}
1860
1976
  重要 (一次命中要求):
1861
1977
  - 每次只调用一个工具
1862
1978
  - 仔细分析工具返回结果
1863
- - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1979
+ - 任务未完成就继续调用工具推进, 直到全部完成
1864
1980
  - 如果需要更多信息,继续调用工具
1865
1981
 
1866
1982
  【工具调用格式 (严格遵守, 否则系统无法解析)】
@@ -1868,8 +1984,7 @@ ${await this.renderActivePlansSection()}
1868
1984
  - 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
1869
1985
  - 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
1870
1986
  - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
1871
- - 工具调用后必须等结果, 不要在同一个回复里继续输出
1872
- - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1987
+ - 工具调用后必须等结果, 不要在同一个回复里继续输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1873
1988
  // 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1874
1989
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1875
1990
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
@@ -1903,6 +2018,25 @@ ${await this.renderActivePlansSection()}
1903
2018
  let result;
1904
2019
  try {
1905
2020
  result = await loop.execute(input, llmWithHarness, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
2021
+ // 2026-10-12 (leo: 「没有根据上下文加载刻度」): pivot 的 messageHistory 是 process-local,
2022
+ // `estimateHistoryTokens()` 读 actor 的旧历史 ⇒ 状态栏上下文刻度恒 0.00% 的根因。
2023
+ // pivot 自己累计的 `state.totalTokens` 才是这一轮**真实**消耗 —— 直接用它上报 ContextManager,
2024
+ // 刻度立刻有数 (旧: 只在上面的 reportUsageToContextManager 里用 actor 历史估, 恒 0)。
2025
+ try {
2026
+ const pivotTokens = Number(result?.state?.totalTokens);
2027
+ if (Number.isFinite(pivotTokens) && pivotTokens > 0) {
2028
+ // 2026-10-13: 会话累计 (多轮对话刻度单调增长; 单轮 totalTokens 只是本轮)
2029
+ this.pivotCumulativeTokens += pivotTokens;
2030
+ const cm = getContextManager();
2031
+ const usage = cm.updateUsage(this.pivotCumulativeTokens);
2032
+ // 上下文管理周期: 上报 → 阈值判断 → 触发压缩 (刻度有数之外, 到阈值真的会动)
2033
+ if (usage.stage === 'compressing' && !this._ctxCycleCompacting) {
2034
+ this._ctxCycleCompacting = true;
2035
+ void this.runContextCycle(usage).finally(() => { this._ctxCycleCompacting = false; });
2036
+ }
2037
+ }
2038
+ }
2039
+ catch { /* 上报失败不阻塞主流程 */ }
1906
2040
  }
1907
2041
  finally {
1908
2042
  try {
@@ -1923,7 +2057,7 @@ ${await this.renderActivePlansSection()}
1923
2057
  runId: this.actor.state.activeRun || undefined,
1924
2058
  goalId: this.actor.state.goalBinding || undefined,
1925
2059
  });
1926
- console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程): ${JSON.stringify(reviewDecision).slice(0, 160)}`);
2060
+ console.log(`[PiAgent] reviewFinal (K4-B 恢复调用, 判定不参与流程): kind=${reviewDecision?.kind} reason=${String(reviewDecision?.reason ?? '').slice(0, 60)}`);
1927
2061
  }
1928
2062
  catch { /* 审查异常不改变本次结论 (门面内部对"审查失效"自有留痕策略) */ }
1929
2063
  if (result.response) {
@@ -30,7 +30,15 @@ export function spawnBackground(raw, cwd = process.cwd()) {
30
30
  const shell = process.platform === 'win32' ? 'cmd' : '/bin/sh';
31
31
  const shellArgs = process.platform === 'win32' ? ['/c', raw] : ['-c', raw];
32
32
  try {
33
- const proc = spawn(shell, shellArgs, { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, windowsHide: true });
33
+ const proc = spawn(shell, shellArgs, {
34
+ cwd,
35
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
36
+ windowsHide: true,
37
+ // 2026-10-13 (leo: 「有没有错误回收机制」): detached:true = 每条命令独立进程组,
38
+ // killSession 才能按组杀 (负 pid) 回收整棵进程树, 不留 shell 的孤儿子进程。
39
+ // 副作用 (进程随父退出不自动回收) 由既有落盘恢复机制兜底 (persistSessions/restoreSessions)。
40
+ detached: process.platform !== 'win32',
41
+ });
34
42
  session.proc = proc;
35
43
  session.pid = proc.pid;
36
44
  persistSessions();
@@ -59,8 +67,11 @@ export function pollSession(id) {
59
67
  },
60
68
  };
61
69
  }
62
- /** 等待后台进程结束 (最多 timeoutMs). 返回最终状态. */
63
- export async function waitSession(id, timeoutMs = 30000) {
70
+ /** 等待后台进程结束 (最多 timeoutMs). 返回最终状态.
71
+ * 2026-10-13 (leo: 「有没有错误回收机制, 超时回收机制?」): 超时后**回收** —— 默认 kill 挂起进程
72
+ * (wait 语义 = "等它跑完取结果", 超时说明它不会在预算内跑完, 留着占资源没意义);
73
+ * 不想杀传 killOnTimeout=false (纯等待/轮询场景). */
74
+ export async function waitSession(id, timeoutMs = 30000, killOnTimeout = true) {
64
75
  const s = sessions.get(id);
65
76
  if (!s)
66
77
  return { ok: false, error: `未知 session ${id}` };
@@ -71,9 +82,13 @@ export async function waitSession(id, timeoutMs = 30000) {
71
82
  while (Date.now() < deadline && s.status === 'running') {
72
83
  await new Promise((r) => setTimeout(r, 250));
73
84
  }
85
+ const timedOut = s.status === 'running';
86
+ if (timedOut && killOnTimeout) {
87
+ killSession(id); // 超时回收: 终止挂起进程, 不占资源
88
+ }
74
89
  return {
75
- ok: s.status !== 'running',
76
- session: { id: s.id, status: s.status, exitCode: s.exitCode, output: s.output.slice(-4000), timedOut: s.status === 'running' },
90
+ ok: !timedOut,
91
+ session: { id: s.id, status: s.status, exitCode: s.exitCode, output: s.output.slice(-4000), timedOut },
77
92
  };
78
93
  }
79
94
  /** 杀死后台进程. 返回是否已终止. */
@@ -85,10 +100,14 @@ export function killSession(id) {
85
100
  return { ok: true, reason: `进程已 ${s.status}` };
86
101
  }
87
102
  try {
88
- if (process.platform === 'win32')
89
- s.proc.kill('SIGTERM');
90
- else
91
- s.proc.kill('SIGTERM');
103
+ // 2026-10-13 (leo: 「有没有错误回收机制」): kill 只杀 shell 不杀子进程 = 进程树泄漏;
104
+ // spawn 时 detached:true 让每条命令独立进程组 ⇒ 这里 kill 整个进程组 (负 pid = 组信号)。
105
+ // spawnBackground 已带 detached:true (见该函数)。
106
+ try {
107
+ process.kill(-(s.proc.pid), 'SIGTERM');
108
+ }
109
+ catch { /* 组信号可能 EPERM (非组长) */ }
110
+ s.proc.kill('SIGTERM');
92
111
  s.status = 'killed';
93
112
  return { ok: true };
94
113
  }
@@ -8,13 +8,12 @@
8
8
  * 决策表 (apply 到 LLM 回复 + 当前 state):
9
9
  * 1. reply 是 AI failure sentinel → continue (push error 进 history, 让 LLM 反思)
10
10
  * 2. reply 可 parse 出 tool call → execute-tool (调 tool, push result 进 history)
11
- * 3. reply 含 <final gen> 且无 tool_call → final (LLM 显式终止)
12
- * 4. reply 无 <final gen>, 无 tool_call → continue (LLM 可能再想想, 让下一轮重新生成)
11
+ * 3. reply 有 tool_call → execute-tool (工具优先)
13
12
  *
14
13
  * 这个模块不调 LLM, 只接受 reply/state 返回下一步决策. LLM 调用 + 真正状态变更
15
14
  * 仍由 PiAgentSession 协调. 这样 claude code 可以独立 unit-test 决策边界.
16
15
  */
17
- import { parseToolCall, isFinalResponse } from './parse-tool-call.js';
16
+ import { parseToolCall } from './parse-tool-call.js';
18
17
  /** 把 LLM 回复归到一种 next action — 纯函数. */
19
18
  export function decideNext(ctx) {
20
19
  if (ctx.signalAborted) {
@@ -42,12 +41,10 @@ export function decideNext(ctx) {
42
41
  reason: 'parse-tool-call',
43
42
  };
44
43
  }
45
- // final gen 终止
46
- if (isFinalResponse(ctx.reply)) {
47
- return { kind: 'final', answer: ctx.reply, reason: 'final-gen-marker' };
48
- }
49
- // 默认 — 普通 text, 让 LLM 继续想
50
- return { kind: 'continue', reason: 'no-tool-no-final' };
44
+ // 2026-10-12 (leo 拍板: 全项目去掉 <final gen> 截止符): 旧实现这里用
45
+ // isFinalResponse (找 `<final gen>` 标记) 判结束 — 已移除。
46
+ // 无工具调用 = 正常 text, 让 LLM 继续想 (由外层质量/无进展门收尾)。
47
+ return { kind: 'continue', reason: 'no-tool' };
51
48
  }
52
49
  /**
53
50
  * isAiFailureSentinel — 检测 LLM 上游失败哨兵.
@@ -62,14 +59,9 @@ export function isAiFailureSentinel(reply) {
62
59
  trimmed.startsWith('[AI 调用失败]') ||
63
60
  trimmed.startsWith('[错误:');
64
61
  }
65
- /** extractFinalText — 抽 LLM 输出里能呈现给用户的最终文字 (已是 Message 状态). */
66
- export function extractFinalText(reply, marker = '<final gen>') {
67
- const idx = reply.indexOf(marker);
68
- if (idx !== -1) {
69
- const after = reply.substring(idx + marker.length).trim();
70
- return after || reply.substring(0, idx).trim();
71
- }
72
- return reply;
62
+ /** extractFinalText — 抽 LLM 输出里能呈现给用户的最终文字. (2026-10-12: 不再按 <final gen> 切) */
63
+ export function extractFinalText(reply) {
64
+ return reply.trim();
73
65
  }
74
66
  /**
75
67
  * 错误累计判定 — 当 totalErrors 达阈值时, 给 final 答案 + 汇总.
@@ -44,20 +44,21 @@ function classify(err, stage) {
44
44
  /** 读本机 LLM 配置摘要 (只看结构与有无, 不打印任何密钥) */
45
45
  export async function probeLlmConfig(home = os.homedir()) {
46
46
  try {
47
- const raw = JSON.parse(await fsp.readFile(path.join(home, '.bolloon', 'llm-config.json'), 'utf8'));
47
+ // 2026-10-06 技术债清理: 只读 canonical bolloon-config.json (旧 llm-config.json 已废弃, 迁移逻辑在 config-store)
48
+ const raw = JSON.parse(await fsp.readFile(path.join(home, '.bolloon', 'bolloon-config.json'), 'utf8'));
48
49
  const active = raw.activeProvider || raw.provider;
49
50
  const providers = raw.providers || {};
50
51
  const p = providers[active] || {};
51
52
  const needsKey = p.requiresApiKey !== false;
52
53
  const hasKey = !!(p.apiKey || p.api_key || process.env[`${String(active).toUpperCase()}_API_KEY`]);
53
54
  if (!active)
54
- return { ok: false, reason: 'llm-config.json 没有 activeProvider' };
55
+ return { ok: false, reason: 'bolloon-config.json 没有 activeProvider' };
55
56
  if (needsKey && !hasKey)
56
57
  return { ok: false, provider: active, model: p.model, hasKey: false, reason: `provider ${active} 需要 apiKey 但没配` };
57
58
  return { ok: true, provider: active, model: p.model, hasKey };
58
59
  }
59
60
  catch (err) {
60
- return { ok: false, reason: `llm-config.json 不可读: ${String(err?.message || err).slice(0, 100)}` };
61
+ return { ok: false, reason: `bolloon-config.json 不可读: ${String(err?.message || err).slice(0, 100)}` };
61
62
  }
62
63
  }
63
64
  async function stage(stages, name, required, timeoutMs, fn, log, noteOf) {