@bolloon/bolloon-agent 0.2.8 → 0.2.10

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 (268) hide show
  1. package/README.md +467 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/agent-manifest-protocol.js +0 -52
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/pi-sdk.js +287 -1213
  8. package/dist/agents/shell-guard.js +2 -2
  9. package/dist/agents/workflow-pivot-loop.js +2 -81
  10. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  11. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  12. package/dist/bollharness-integration/context-router.js +292 -292
  13. package/dist/bollharness-integration/gate-state-machine.js +13 -26
  14. package/dist/bollharness-integration/integration.js +0 -71
  15. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  16. package/dist/bollharness-integration/skill-adapter.js +127 -52
  17. package/dist/bootstrap/context-collector.js +3 -6
  18. package/dist/bootstrap/lifecycle-hooks.js +1 -15
  19. package/dist/cli-entry.js +35 -39
  20. package/dist/constraint-runtime/src/_archive_helper.js +9 -0
  21. package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
  22. package/dist/constraint-runtime/src/agent/index.js +1 -0
  23. package/dist/constraint-runtime/src/assistant/index.js +12 -0
  24. package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
  25. package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
  26. package/dist/constraint-runtime/src/bridge/index.js +12 -0
  27. package/dist/constraint-runtime/src/buddy/index.js +12 -0
  28. package/dist/constraint-runtime/src/cli/index.js +12 -0
  29. package/dist/constraint-runtime/src/command_graph.js +10 -0
  30. package/dist/constraint-runtime/src/commands.js +60 -0
  31. package/dist/constraint-runtime/src/components/index.js +12 -0
  32. package/dist/constraint-runtime/src/constants/index.js +12 -0
  33. package/dist/constraint-runtime/src/constraint/budget.js +22 -0
  34. package/dist/constraint-runtime/src/constraint/index.js +2 -0
  35. package/dist/constraint-runtime/src/constraint/permission.js +20 -0
  36. package/dist/constraint-runtime/src/context.js +30 -0
  37. package/dist/constraint-runtime/src/coordinator/index.js +12 -0
  38. package/dist/constraint-runtime/src/cost_hook.js +4 -0
  39. package/dist/constraint-runtime/src/cost_tracker.js +8 -0
  40. package/dist/constraint-runtime/src/deferred_init.js +10 -0
  41. package/dist/constraint-runtime/src/direct_modes.js +6 -0
  42. package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
  43. package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
  44. package/dist/constraint-runtime/src/execution_registry.js +44 -0
  45. package/dist/constraint-runtime/src/history.js +9 -0
  46. package/dist/constraint-runtime/src/hooks/index.js +12 -0
  47. package/dist/constraint-runtime/src/index.js +26 -0
  48. package/dist/constraint-runtime/src/ink.js +4 -0
  49. package/dist/constraint-runtime/src/keybindings/index.js +12 -0
  50. package/dist/constraint-runtime/src/memdir/index.js +12 -0
  51. package/dist/constraint-runtime/src/migrations/index.js +12 -0
  52. package/dist/constraint-runtime/src/models.js +1 -0
  53. package/dist/constraint-runtime/src/moreright/index.js +12 -0
  54. package/dist/constraint-runtime/src/native_ts/index.js +12 -0
  55. package/dist/constraint-runtime/src/output_styles/index.js +12 -0
  56. package/dist/constraint-runtime/src/parity_audit.js +12 -0
  57. package/dist/constraint-runtime/src/plugins/index.js +12 -0
  58. package/dist/constraint-runtime/src/port_manifest.js +11 -0
  59. package/dist/constraint-runtime/src/prefetch.js +9 -0
  60. package/dist/constraint-runtime/src/query.js +1 -0
  61. package/dist/constraint-runtime/src/remote/index.js +12 -0
  62. package/dist/constraint-runtime/src/remote_runtime.js +9 -0
  63. package/dist/constraint-runtime/src/runtime/index.js +1 -0
  64. package/dist/constraint-runtime/src/runtime/session.js +35 -0
  65. package/dist/constraint-runtime/src/schemas/index.js +12 -0
  66. package/dist/constraint-runtime/src/screens/index.js +12 -0
  67. package/dist/constraint-runtime/src/server/index.js +12 -0
  68. package/dist/constraint-runtime/src/services/index.js +12 -0
  69. package/dist/constraint-runtime/src/session_store.js +22 -0
  70. package/dist/constraint-runtime/src/setup.js +30 -0
  71. package/dist/constraint-runtime/src/skills/index.js +1 -0
  72. package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
  73. package/dist/constraint-runtime/src/state/index.js +12 -0
  74. package/dist/constraint-runtime/src/system_init.js +20 -0
  75. package/dist/constraint-runtime/src/thinking/engine.js +42 -0
  76. package/dist/constraint-runtime/src/thinking/index.js +1 -0
  77. package/dist/constraint-runtime/src/tool_pool.js +8 -0
  78. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
  79. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
  80. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
  81. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
  82. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
  83. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
  84. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
  85. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
  86. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
  87. package/dist/constraint-runtime/src/tools.js +80 -0
  88. package/dist/constraint-runtime/src/transcript.js +19 -0
  89. package/dist/constraint-runtime/src/types/index.js +12 -0
  90. package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
  91. package/dist/constraint-runtime/src/utils/index.js +12 -0
  92. package/dist/constraint-runtime/src/vim/index.js +12 -0
  93. package/dist/constraint-runtime/src/voice/index.js +12 -0
  94. package/dist/constraint-runtime/tests/agent.test.js +16 -0
  95. package/dist/constraint-runtime/tests/constraint.test.js +41 -0
  96. package/dist/constraint-runtime/tests/skill.test.js +19 -0
  97. package/dist/constraint-runtime/tests/thinking.test.js +22 -0
  98. package/dist/context-compaction/auto-compact.js +7 -7
  99. package/dist/electron-preload.js +1 -18
  100. package/dist/electron-preload.js.map +1 -1
  101. package/dist/electron.js +168 -3
  102. package/dist/electron.js.map +1 -1
  103. package/dist/index.js +116 -136
  104. package/dist/llm/config-store.js +8 -24
  105. package/dist/llm/llm-judgment-client.js +102 -102
  106. package/dist/llm/pi-ai.js +85 -126
  107. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  108. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  109. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  110. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  111. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  112. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  113. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  114. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  115. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  116. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  117. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  118. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  119. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  120. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  121. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  122. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  123. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  124. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  125. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  126. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  127. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  128. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  129. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  130. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  131. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  132. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  133. package/dist/llm/tool-manifest/bash.js +3 -3
  134. package/dist/llm/tool-manifest/create_file.js +4 -4
  135. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  136. package/dist/llm/tool-manifest/image_search.js +2 -2
  137. package/dist/llm/tool-manifest/mcp.js +2 -2
  138. package/dist/llm/tool-manifest/message_compose.js +3 -3
  139. package/dist/llm/tool-manifest/places.js +2 -2
  140. package/dist/llm/tool-manifest/present_files.js +1 -1
  141. package/dist/llm/tool-manifest/recipe.js +2 -2
  142. package/dist/llm/tool-manifest/str_replace.js +5 -5
  143. package/dist/llm/tool-manifest/view.js +3 -3
  144. package/dist/llm/tool-manifest/weather.js +4 -4
  145. package/dist/llm/tool-manifest/web.js +4 -4
  146. package/dist/pi-ecosystem-colony/index.js +365 -0
  147. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  148. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  149. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  150. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  151. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  152. package/dist/security/context-router-tool.js +15 -15
  153. package/dist/security/tool-gate.js +0 -11
  154. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  155. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  156. package/dist/social/ant-colony/index.js +6 -0
  157. package/dist/social/ant-colony/types.js +24 -0
  158. package/dist/test/ai-judgment-test.js +80 -0
  159. package/dist/test/bollharness-integration.test.js +318 -0
  160. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  161. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  162. package/dist/test/constraint-layer.test.js +164 -0
  163. package/dist/test/diap-identity-test.js +172 -0
  164. package/dist/test/diap-quick-test.js +62 -0
  165. package/dist/test/global-shared-context.test.js +315 -0
  166. package/dist/test/harness-judgment-injection.test.js +246 -0
  167. package/dist/test/harness-workflow-integrator-test.js +228 -0
  168. package/dist/test/human-value-store.test.js +243 -0
  169. package/dist/test/hybrid-integration-test.js +118 -0
  170. package/dist/test/hybrid-messenger-verify.js +55 -0
  171. package/dist/test/iroh-bistream-debug.js +38 -0
  172. package/dist/test/iroh-communication.test.js +66 -0
  173. package/dist/test/iroh-debug-test.js +57 -0
  174. package/dist/test/iroh-diap-test.js +71 -0
  175. package/dist/test/iroh-direct-connect.js +55 -0
  176. package/dist/test/iroh-e2e-fixed.js +89 -0
  177. package/dist/test/iroh-e2e-same-process.js +63 -0
  178. package/dist/test/iroh-e2e.js +66 -0
  179. package/dist/test/iroh-final-e2e.js +72 -0
  180. package/dist/test/iroh-relay-test.js +37 -0
  181. package/dist/test/iroh-simple-test.js +41 -0
  182. package/dist/test/iroh-transport-verify.js +54 -0
  183. package/dist/test/iroh-transport.test.js +37 -0
  184. package/dist/test/iroh-two-nodes.js +70 -0
  185. package/dist/test/iroh-verify.js +44 -0
  186. package/dist/test/judgment-decision.test.js +219 -0
  187. package/dist/test/llm-judgment-integration.test.js +220 -0
  188. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  189. package/dist/test/p2p-agent-dialogue.js +341 -0
  190. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  191. package/dist/test/p2p-agent-harness-flow.js +437 -0
  192. package/dist/test/p2p-agent-harness-single.js +143 -0
  193. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  194. package/dist/test/p2p-cid-connect-test.js +195 -0
  195. package/dist/test/p2p-connect-receiver.js +69 -0
  196. package/dist/test/p2p-doc-transfer.js +110 -0
  197. package/dist/test/p2p-identity-page-test.js +77 -0
  198. package/dist/test/p2p-iroh-test.js +171 -0
  199. package/dist/test/p2p-minimal-test.js +241 -0
  200. package/dist/test/p2p-node-1.js +148 -0
  201. package/dist/test/p2p-node-2.js +148 -0
  202. package/dist/test/p2p-server.js +281 -0
  203. package/dist/test/p2p-two-nodes-test.js +438 -0
  204. package/dist/test/pi-sdk.test.js +44 -0
  205. package/dist/test/set-persona.js +40 -0
  206. package/dist/test/simple.test.js +9 -0
  207. package/dist/test/storage-integration.test.js +150 -0
  208. package/dist/test/subagent-manager.test.js +276 -0
  209. package/dist/test/test-gate-flow.test.js +81 -0
  210. package/dist/test/workflow-engine.test.js +87 -0
  211. package/dist/test/workflow-pivot-loop.test.js +246 -0
  212. package/dist/web/api-config.html +520 -520
  213. package/dist/web/client.js +2917 -3791
  214. package/dist/web/components/p2p/index.js +234 -276
  215. package/dist/web/components/wallet-viem.mjs +118 -118
  216. package/dist/web/index.html +374 -373
  217. package/dist/web/manifest.json +20 -20
  218. package/dist/web/server.js +23 -775
  219. package/dist/web/style.css +4737 -4737
  220. package/dist/web/ui/message-renderer.js +373 -451
  221. package/dist/web/ui/step-timeline.js +255 -351
  222. package/package.json +160 -157
  223. package/scripts/build-cli.js +216 -0
  224. package/scripts/build-web.ts +125 -0
  225. package/scripts/postinstall.js +153 -0
  226. package/bin/bolloon-daemon.sh +0 -207
  227. package/bin/ipfs +0 -0
  228. package/dist/agents/chat-segmenter.js +0 -298
  229. package/dist/agents/judgment-protocol.js +0 -479
  230. package/dist/agents/parse-tool-call.js +0 -304
  231. package/dist/agents/peer-manifest-loader.js +0 -210
  232. package/dist/agents/react-loop.js +0 -120
  233. package/dist/agents/session-store.js +0 -171
  234. package/dist/agents/tool-registry.js +0 -136
  235. package/dist/bootstrap/chat-archiver.js +0 -276
  236. package/dist/bootstrap/memory-compressor.js +0 -170
  237. package/dist/bootstrap/persona-loader.js +0 -94
  238. package/dist/electron/config.js +0 -16
  239. package/dist/electron/config.js.map +0 -1
  240. package/dist/electron/dialogs.js +0 -71
  241. package/dist/electron/dialogs.js.map +0 -1
  242. package/dist/electron/first-run.js +0 -129
  243. package/dist/electron/first-run.js.map +0 -1
  244. package/dist/electron/ipc.js +0 -16
  245. package/dist/electron/ipc.js.map +0 -1
  246. package/dist/electron/logger.js +0 -77
  247. package/dist/electron/logger.js.map +0 -1
  248. package/dist/electron/main.js +0 -60
  249. package/dist/electron/main.js.map +0 -1
  250. package/dist/electron/menu.js +0 -140
  251. package/dist/electron/menu.js.map +0 -1
  252. package/dist/electron/paths.js +0 -33
  253. package/dist/electron/paths.js.map +0 -1
  254. package/dist/electron/server.js +0 -74
  255. package/dist/electron/server.js.map +0 -1
  256. package/dist/electron/tray.js +0 -73
  257. package/dist/electron/tray.js.map +0 -1
  258. package/dist/electron/window.js +0 -69
  259. package/dist/electron/window.js.map +0 -1
  260. package/dist/network/local-inbox-bus.js +0 -73
  261. package/dist/network/p2p-outbox.js +0 -161
  262. package/dist/network/peer-fs.js +0 -420
  263. package/dist/network/peer-resource-bridge.js +0 -216
  264. package/dist/web/components/p2p/P2PModal.js +0 -188
  265. package/dist/web/components/p2p/p2p-modal.js +0 -664
  266. package/dist/web/components/p2p/p2p-tools.js +0 -248
  267. package/dist/web/input-validator.js +0 -103
  268. package/dist/web/util/safe-name.js +0 -32
@@ -19,7 +19,7 @@ import { getBranchPrefix, getCooldownMs, checkWritePath } from './shell-guard.js
19
19
  import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
20
20
  import { getGlobalSharedContext } from '../social/global-shared-context.js';
21
21
  import { Session, SkillRegistry, saveSession, loadSession } from '@bolloon/constraint-runtime';
22
- import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
22
+ import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
23
23
  // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
24
24
  // 失败静默, 不阻塞主对话
25
25
  import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
@@ -31,10 +31,6 @@ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
31
31
  import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
32
32
  // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
33
33
  import { ReactHarness } from '../security/react-harness.js';
34
- import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
35
- import { sessionStore as defaultSessionStore } from './session-store.js';
36
- import { ToolRegistry } from './tool-registry.js';
37
- import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
38
34
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
39
35
  const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
40
36
  export class PiSessionManager {
@@ -321,18 +317,18 @@ export class PiSessionManager {
321
317
  await this.sharedContext.updateAgentStatus(this.agentId, status);
322
318
  }
323
319
  }
324
- const TOOL_DEFINITIONS = `
325
- 可用工具:
326
- 1. read_document(path) - 读取文档内容,支持 .txt, .md, .pdf, .docx
327
- 2. summarize_document(path, context?) - 总结文档内容,可选提供上下文
328
- 3. improve_document(path, requirements) - 改进文档,需提供文件路径和改进要求
329
- 4. list_peers() - 列出已连接的对等节点
330
- 5. send_message(peer_id, message) - 向指定对等节点发送消息
331
- 6. broadcast_message(message) - 向所有对等节点广播消息
332
- 7. get_identity() - 获取当前智能体身份信息
333
- 8. set_persona(persona_json) - 更新智能体 persona,包含 name、description、personality、greeting 等
334
- 9. run_workflow(steps) - 执行预定义工作流
335
- 10. get_operation_logs() - 获取操作日志
320
+ const TOOL_DEFINITIONS = `
321
+ 可用工具:
322
+ 1. read_document(path) - 读取文档内容,支持 .txt, .md, .pdf, .docx
323
+ 2. summarize_document(path, context?) - 总结文档内容,可选提供上下文
324
+ 3. improve_document(path, requirements) - 改进文档,需提供文件路径和改进要求
325
+ 4. list_peers() - 列出已连接的对等节点
326
+ 5. send_message(peer_id, message) - 向指定对等节点发送消息
327
+ 6. broadcast_message(message) - 向所有对等节点广播消息
328
+ 7. get_identity() - 获取当前智能体身份信息
329
+ 8. set_persona(persona_json) - 更新智能体 persona,包含 name、description、personality、greeting 等
330
+ 9. run_workflow(steps) - 执行预定义工作流
331
+ 10. get_operation_logs() - 获取操作日志
336
332
  `;
337
333
  class PiAgentSession {
338
334
  cwd;
@@ -348,17 +344,11 @@ class PiAgentSession {
348
344
  socialHeartbeat = null;
349
345
  messageHistory = [];
350
346
  tools = new Map();
351
- /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
352
- _toolRegistry = new ToolRegistry();
353
347
  skillRegistry = new SkillRegistry();
354
348
  /** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
355
349
  cachedToolDefinitions = '';
356
350
  /** M2.4: 缓存 persona section */
357
351
  cachedPersonaSection = '';
358
- /** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
359
- _sessionStore;
360
- /** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
361
- _readyPromise = null;
362
352
  // 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
363
353
  // 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
364
354
  // 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
@@ -370,10 +360,6 @@ class PiAgentSession {
370
360
  /** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
371
361
  * 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
372
362
  MAX_TOTAL_ERRORS = 20;
373
- /** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
374
- successfulToolResults = [];
375
- /** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
376
- _inboxMessages = [];
377
363
  /** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
378
364
  LOOP_COMPACT_RATIO = 0.8;
379
365
  /** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
@@ -396,9 +382,6 @@ class PiAgentSession {
396
382
  */
397
383
  judgmentGateAddition = '';
398
384
  judgmentGateUsedIds = [];
399
- // 2026-06-18: 来自 web server markedPrompt 外的 contextHint (channel/judgment/distill/remote channels),
400
- // 拼到 systemPrompt 末尾, 别再混进 user message
401
- contextHintAddition = '';
402
385
  /**
403
386
  * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
404
387
  * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
@@ -411,8 +394,6 @@ class PiAgentSession {
411
394
  promptStartTime = 0;
412
395
  /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
413
396
  currentChannelId = '';
414
- /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
415
- currentAgentId = '';
416
397
  /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
417
398
  currentIntent = 'chitchat';
418
399
  currentIntentHint = '';
@@ -456,10 +437,6 @@ class PiAgentSession {
456
437
  this.peerId = config.peerId || 'local';
457
438
  this.identity = config.identityDoc || this.createDefaultIdentity();
458
439
  this.minimaxAvailable = this.checkMinimax();
459
- // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
460
- this.currentAgentId = config.agentId || '';
461
- // 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
462
- this._sessionStore = config.sessionStore ?? defaultSessionStore;
463
440
  this.constraintLayer = new ConstraintLayer();
464
441
  this.workflowEngine = new WorkflowEngine(this.constraintLayer);
465
442
  this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
@@ -475,37 +452,38 @@ class PiAgentSession {
475
452
  // 之前 messageHistory 是空的, 服务重启后 LLM 看到的是新对话
476
453
  // 现在 loadSessionKey 形如 "channel-xxx:default" 走 ~/.bolloon/sessions/cache/<key>.json
477
454
  if (config.loadSessionKey) {
478
- this._readyPromise = this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30).catch((err) => {
479
- // 失败静默, 但不让 whenReady 永久 hang
480
- console.warn(`[PiAgent] hydrateMessageHistory failed: ${err.message?.slice(0, 100)}`);
481
- });
455
+ this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30);
482
456
  }
483
457
  }
484
458
  /**
485
- * 2026-06-30: 让外部 await 构造期间的 hydrate 完成.
486
- * 解决 fire-and-forget 让 messageHistory 不可预测的问题.
487
- * 不传 loadSessionKey 时立即返回.
488
- */
489
- whenReady() {
490
- return this._readyPromise ?? Promise.resolve();
491
- }
492
- /**
493
- * M2.3 (2026-06-30 重构): 从 SessionStore 加载历史, 转成 messageHistory 格式
459
+ * M2.3: session JSON 加载历史, 转成 messageHistory 格式
494
460
  * - 失败静默 (历史加载失败不应该阻塞 agent 启动)
495
461
  * - 限制 max 条数, 防止 context 爆
496
- * - 跳过错误消息 ([AI 服务调用失败] / [错误:...]) 不污染 LLM
497
- * - 委托 SessionStore 完成 IO, 保证 save/load 路径对称
498
- *
499
- * 历史格式兼容旧 schema ({type, content}) 和新 schema (PersistedMessage[])
462
+ * - user 消息 role=user, ai 消息 role=assistant
463
+ * - 跳过 metadata 中含 error (错误消息会污染 LLM)
500
464
  */
501
465
  async hydrateMessageHistory(sessionKey, maxMessages) {
502
466
  try {
503
- const loaded = await this._sessionStore.loadMessages(sessionKey);
504
- if (!loaded) {
505
- console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
506
- return;
467
+ const sessionPath = path.join(os.homedir(), '.bolloon', 'sessions', 'cache', `${sessionKey}.json`);
468
+ const content = await fs.readFile(sessionPath, 'utf-8');
469
+ const session = JSON.parse(content);
470
+ const messages = Array.isArray(session?.messages) ? session.messages : [];
471
+ // 保留最后 N 条, 转换 role 字段
472
+ const tail = messages.slice(-maxMessages);
473
+ const hydrated = [];
474
+ for (const m of tail) {
475
+ // 跳过错误的 AI 消息 (M1.2 之后, AI 错误时 reply 是 [AI 服务调用失败] 字符串, 不该进 history)
476
+ if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
477
+ continue;
478
+ if (m?.type === 'ai' && typeof m.content === 'string' && m.content.startsWith('[错误:'))
479
+ continue;
480
+ if (!m?.content)
481
+ continue;
482
+ const role = m.type === 'user' ? 'user' : m.type === 'ai' ? 'assistant' : null;
483
+ if (!role)
484
+ continue;
485
+ hydrated.push({ role, content: String(m.content) });
507
486
  }
508
- const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
509
487
  if (hydrated.length > 0) {
510
488
  this.messageHistory = hydrated;
511
489
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
@@ -515,84 +493,6 @@ class PiAgentSession {
515
493
  console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
516
494
  }
517
495
  }
518
- /**
519
- * 2026-06-30: 把当前 messageHistory 持久化到 SessionStore.
520
- * 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
521
- * 即可获得"重启 / 跨进程接续"的语义.
522
- */
523
- async saveCurrentSession(key) {
524
- const persisted = this.messageHistory.map((m) => ({
525
- role: m.role,
526
- content: m.content,
527
- toolCall: m.toolCall,
528
- toolResult: m.toolResult,
529
- toolCallId: m.toolCallId,
530
- timestamp: Date.now(),
531
- source: 'pi-session',
532
- }));
533
- await this._sessionStore.saveMessages(key, persisted);
534
- }
535
- /**
536
- * 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
537
- * 返回加载条数 — 失败或空则返回 0.
538
- * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
539
- */
540
- async resumeSession(key, maxMessages = 30) {
541
- const before = this.messageHistory.length;
542
- await this.hydrateMessageHistory(key, maxMessages);
543
- return this.messageHistory.length - before;
544
- }
545
- /**
546
- * 2026-06-30: 读历史不修改 messageHistory.
547
- * 给 claude code / 测试做"先看一下历史"用 — 不破坏当前会话.
548
- * 返回 Message[] 数组 (空数组表示无历史).
549
- */
550
- async peekSessionHistory(key, maxMessages = 30) {
551
- try {
552
- const loaded = await this._sessionStore.loadMessages(key);
553
- if (!loaded)
554
- return [];
555
- return this._filterToMessage(loaded).slice(-maxMessages);
556
- }
557
- catch {
558
- return [];
559
- }
560
- }
561
- /** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
562
- _filterToMessage(loaded) {
563
- const hydrated = [];
564
- const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
565
- for (const m of loaded) {
566
- // role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
567
- if (!VALID_ROLES.has(m.role))
568
- continue;
569
- // 跳过污染消息
570
- if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
571
- continue;
572
- if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
573
- continue;
574
- // 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
575
- // 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
576
- // 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
577
- if (!m.content && !m.toolCall && !m.toolResult)
578
- continue;
579
- // 跳过空 tool role (tool result 占位但没有任何内容)
580
- if (m.role === 'tool' && !m.toolResult)
581
- continue;
582
- hydrated.push({
583
- role: m.role,
584
- content: m.content ?? '',
585
- toolCall: m.toolCall,
586
- toolResult: m.toolResult,
587
- toolCallId: m.toolCallId,
588
- });
589
- }
590
- return hydrated;
591
- }
592
- /** 暴露 store 给测试 / 高级集成用. */
593
- get sessionStoreInstance() {
594
- return this._sessionStore;
595
- }
596
496
  /**
597
497
  * 从 SKILL.md 目录加载 skills 进 skillRegistry.
598
498
  *
@@ -601,17 +501,23 @@ class PiAgentSession {
601
501
  * 2. ~/.bolloon/skills/ 全局用户级
602
502
  * 3. <cwd>/.bolloon/skills/ 项目级
603
503
  * 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
604
- *
605
- * 2026-07-04: 移除 18bollharness builtin skill (findBolloonBuiltinSkillsPath).
606
- * 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
607
- * 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
608
- * `<final gen>`, session 落盘拿不到最终回答.
609
- * 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
504
+ * 5. <bolloon-repo>/src/bollharness/.boll/skills/ bolloon 仓库内置 skill
505
+ * (bolloon 项目本身用 pi-sdk 写核心, 19 skill 视为项目级 builtin)
610
506
  *
611
507
  * 静默忽略不存在的目录.
612
508
  */
613
509
  loadSkills(paths) {
614
- const resolved = (paths && paths.length > 0) ? paths : defaultSkillPaths(os.homedir(), this.cwd);
510
+ let resolved;
511
+ if (paths && paths.length > 0) {
512
+ resolved = paths;
513
+ }
514
+ else {
515
+ resolved = [
516
+ ...defaultSkillPaths(os.homedir(), this.cwd),
517
+ // bolloon 仓库内置 skill (相对本 npm 包的位置)
518
+ this.findBolloonBuiltinSkillsPath(),
519
+ ].filter((p) => Boolean(p));
520
+ }
615
521
  loadSkillsFromPaths(resolved)
616
522
  .then((skills) => {
617
523
  for (const s of skills) {
@@ -620,17 +526,36 @@ class PiAgentSession {
620
526
  }
621
527
  this.skillRegistry.register(s);
622
528
  }
623
- console.log(`[loadSkills] 已加载 ${skills.length} 个 skill from ${resolved.join(', ')}`);
624
- if (skills.length > 0) {
625
- for (const s of skills) {
626
- console.log(` - ${s.name}: ${s.description.substring(0, 100)}${s.description.length > 100 ? '...' : ''}`);
627
- }
628
- }
529
+ console.log(`[loadSkills] 已加载 ${skills.length} 个 skill: ${skills.map(describeSkill).join(' | ')}`);
629
530
  })
630
531
  .catch((err) => {
631
532
  console.error('[loadSkills] 加载失败:', err);
632
533
  });
633
534
  }
535
+ /**
536
+ * 定位 bolloon 仓库内置的 bollharness skill 目录.
537
+ * 向上回溯 cwd, 找第一个包含 src/bollharness/.boll/skills 的祖先.
538
+ * 找不到时返回 null (例如把 bolloon-agent 作为外部依赖安装时).
539
+ */
540
+ findBolloonBuiltinSkillsPath() {
541
+ let dir = this.cwd;
542
+ for (let i = 0; i < 6; i++) {
543
+ const candidate = path.join(dir, 'src', 'bollharness', '.boll', 'skills');
544
+ try {
545
+ if (fsSync.existsSync(candidate) && fsSync.statSync(candidate).isDirectory()) {
546
+ return candidate;
547
+ }
548
+ }
549
+ catch {
550
+ // 忽略 stat 异常, 继续向上
551
+ }
552
+ const parent = path.dirname(dir);
553
+ if (parent === dir)
554
+ break;
555
+ dir = parent;
556
+ }
557
+ return null;
558
+ }
634
559
  async initHarness() {
635
560
  try {
636
561
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
@@ -747,162 +672,6 @@ class PiAgentSession {
747
672
  }
748
673
  }
749
674
  });
750
- // 2026-06-19: 本地 + 远端智能体通信工具集 (Agent Mesh 通信层)
751
- // 解决"另一个智能体发消息"的能力缺口:
752
- // - 本地 channel: 走 PiSessionManager.getAllChannels / addMessage / getChannelMessages
753
- // - 远端 P2P: 走 p2pNetwork.sendMessage
754
- this._inboxMessages = [];
755
- this._setupInboxListener();
756
- // list_local_channels: 列出本 session 内所有 channel
757
- this.tools.set('list_local_channels', {
758
- name: 'list_local_channels',
759
- description: '列出当前 PiAgentSession 注册的所有 channel. 每个 channel 是 bolloon 内的智能体会话, 可含 messages[] / peerDid(对端 DID) / peerName. 供 send_to_channel 选 channel_id.',
760
- parameters: {},
761
- execute: async () => {
762
- const channels = this.sessionManager.getAllChannels();
763
- if (channels.length === 0) {
764
- return { success: true, output: '📭 当前 session 没有注册任何 channel (用 send_to_channel 留空 channel_id 会自动创建一个)' };
765
- }
766
- const lines = channels.map((ch, i) => {
767
- const peer = ch.peerDid ? ` peer=${ch.peerDid.substring(0, 20)}` : (ch.peerName ? ` peer="${ch.peerName}"` : '');
768
- const msgCount = ch.messages?.length || 0;
769
- return ` ${i + 1}. ${ch.id} name="${ch.name}" msgs=${msgCount}${peer}`;
770
- });
771
- return { success: true, output: `📬 当前 session 有 ${channels.length} 个 channel:\n${lines.join('\n')}` };
772
- }
773
- });
774
- // send_to_channel: 把消息发到指定 channel (本地 + 如果 channel 有关联 peerDid 也走 P2P 转发)
775
- this.tools.set('send_to_channel', {
776
- name: 'send_to_channel',
777
- description: '发送消息到指定 channel. channel_id 留空会自动创建一个. 如果 channel 已关联 peerDid, 会同时通过 P2P 转发到对端 agent.',
778
- parameters: { channel_id: '目标 channel id (留空自动创建)', message: '消息内容 (必填)', peer_did: '可选 创建新 channel 时绑定的对端 DID (e.g. did:key:...)' },
779
- execute: async (args) => {
780
- const message = String(args.message || '').trim();
781
- if (!message)
782
- return { success: false, error: 'message 必填' };
783
- let channelId = String(args.channel_id || '').trim();
784
- const peerDid = args.peer_did ? String(args.peer_did) : undefined;
785
- if (!channelId) {
786
- // 自动创建 channel
787
- const ch = await this.sessionManager.getOrCreatePeerChannel(peerDid || 'auto-created', 'auto-created');
788
- channelId = ch.id;
789
- }
790
- // 用 AgentSession 接口方法发消息 (它会自动设置 type/agentId/timestamp)
791
- await this.sendSocialMessage(channelId, message);
792
- // 如果 channel 关联了 peer, 走 P2P 转发
793
- const ch = this.sessionManager.getAllChannels().find(c => c.id === channelId);
794
- if (ch?.peerDid) {
795
- try {
796
- const peerId = ch.peerDid.replace(/^did:key:/, '').replace(/^did:pi:/, '');
797
- await p2pNetwork.sendMessage(peerId, 'channel-message', JSON.stringify({ channelId, content: message, from: this.identity.did, timestamp: new Date().toISOString() }));
798
- return { success: true, output: `📨 消息已存到 channel ${channelId} + P2P 转发到 ${ch.peerDid.substring(0, 30)}...` };
799
- }
800
- catch (e) {
801
- return { success: true, output: `📨 消息已存到 channel ${channelId} (P2P 转发失败: ${String(e).slice(0, 100)})` };
802
- }
803
- }
804
- return { success: true, output: `📨 消息已存到 channel ${channelId}` };
805
- }
806
- });
807
- // check_channel_inbox: 读指定 channel 的所有 messages
808
- this.tools.set('check_channel_inbox', {
809
- name: 'check_channel_inbox',
810
- description: '读取指定 channel 的所有 messages, 按时间排序. channel_id 必填, 来自 list_local_channels.',
811
- parameters: { channel_id: '目标 channel id (必填)', max: '可选, 最多返回 N 条 (默认 50)' },
812
- execute: async (args) => {
813
- const channelId = String(args.channel_id || '').trim();
814
- if (!channelId)
815
- return { success: false, error: 'channel_id 必填' };
816
- const max = Number(args.max) || 50;
817
- const messages = await this.sessionManager.getChannelMessages(channelId);
818
- const channel = this.sessionManager.getAllChannels().find(c => c.id === channelId);
819
- const channelName = channel?.name || channelId;
820
- if (messages.length === 0) {
821
- return { success: true, output: `📭 channel "${channelName}" (${channelId}) 空, 0 条消息` };
822
- }
823
- const slice = messages.slice(-max);
824
- const lines = slice.map((m, i) => {
825
- const from = m.sender || m.from || m.fromDid || m.agentId || '?';
826
- const ts = m.timestamp || m.createdAt || '?';
827
- const content = (m.content || m.text || '').substring(0, 200);
828
- return ` ${i + 1}. [${m.type || 'text'}] from=${from} time=${ts}\n ${content}`;
829
- });
830
- return { success: true, output: `📬 channel "${channelName}" (${channelId}) 有 ${slice.length} 条消息 (共 ${messages.length}):\n${lines.join('\n')}` };
831
- }
832
- });
833
- // check_inbox: 兼容旧名, 实际走 check_channel_inbox
834
- this.tools.set('check_inbox', {
835
- name: 'check_inbox',
836
- description: "兼容旧名: 默认读第一个 channel 的 messages. 推荐用 list_local_channels + check_channel_inbox(channel_id)",
837
- parameters: { max: '可选, 最多返回 N 条 (默认 50)' },
838
- execute: async (args) => {
839
- const channels = this.sessionManager.getAllChannels();
840
- if (channels.length === 0) {
841
- return { success: true, output: '📭 当前 session 没有 channel, 0 条消息' };
842
- }
843
- const first = channels[0];
844
- // 直接调 check_channel_inbox 复用逻辑
845
- return await this.tools.get('check_channel_inbox').execute({ channel_id: first.id, max: String(args.max || 50) });
846
- }
847
- });
848
- // send_to_peer: 发送结构化消息到指定 P2P 节点
849
- this.tools.set('send_to_peer', {
850
- name: 'send_to_peer',
851
- description: '发送结构化消息到指定 P2P 节点 (远端 bolloon 实例). 对方会通过远端 channel 收到.',
852
- parameters: { peer_id: '目标 P2P 节点 publicKey (用 list_peers 查)', message: '消息内容 (任意字符串)', type: '可选, 消息类型标签 (默认 agent-message)' },
853
- execute: async (args) => {
854
- const peerId = String(args.peer_id || '').trim();
855
- const msg = String(args.message || '').trim();
856
- const type = String(args.type || 'agent-message').trim();
857
- if (!peerId)
858
- return { success: false, error: 'peer_id 必填' };
859
- if (!msg)
860
- return { success: false, error: 'message 必填' };
861
- try {
862
- await p2pNetwork.sendMessage(peerId, type, msg);
863
- return { success: true, output: `✅ 消息已发送到 ${peerId.substring(0, 16)}...` };
864
- }
865
- catch (e) {
866
- return { success: false, error: `发送失败: ${String(e)}` };
867
- }
868
- }
869
- });
870
- // p2p_broadcast: 广播给所有 P2P 节点
871
- this.tools.set('p2p_broadcast', {
872
- name: 'p2p_broadcast',
873
- description: '广播消息到所有连接的 P2P 节点',
874
- parameters: { message: '消息内容', type: '可选, 消息类型标签 (默认 agent-broadcast)' },
875
- execute: async (args) => {
876
- const msg = String(args.message || '').trim();
877
- const type = String(args.type || 'agent-broadcast').trim();
878
- if (!msg)
879
- return { success: false, error: 'message 必填' };
880
- try {
881
- await p2pNetwork.broadcast(type, msg);
882
- return { success: true, output: `📡 已广播 type=${type}` };
883
- }
884
- catch (e) {
885
- return { success: false, error: `广播失败: ${String(e)}` };
886
- }
887
- }
888
- });
889
- // agent_call: RPC 风格远端 agent 任务调用
890
- this.tools.set('agent_call', {
891
- name: 'agent_call',
892
- description: 'RPC: 让远端 P2P agent 跑一个任务, 等待结果返回. 远端 agent 会基于 task 描述自主完成, 完成后回复. (RPC 结果回收机制待实现)',
893
- parameters: { peer_id: '目标 P2P 节点', task: '任务描述 (远端 agent 收到的 prompt)', timeoutMs: '可选 超时 (ms, 默认 30000)' },
894
- execute: async (args) => {
895
- const peerId = String(args.peer_id || '').trim();
896
- const task = String(args.task || '').trim();
897
- if (!peerId)
898
- return { success: false, error: 'peer_id 必填' };
899
- if (!task)
900
- return { success: false, error: 'task 必填' };
901
- const requestId = `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
902
- await p2pNetwork.sendMessage(peerId, 'agent-call', JSON.stringify({ requestId, task, from: this.identity?.did }));
903
- return { success: true, output: `📞 RPC 任务已发送给 ${peerId.substring(0, 16)}...\n requestId=${requestId}` };
904
- }
905
- });
906
675
  this.tools.set('get_identity', {
907
676
  name: 'get_identity',
908
677
  description: '获取当前智能体身份信息',
@@ -1031,32 +800,7 @@ class PiAgentSession {
1031
800
  const cmd = String(args.command || '').trim();
1032
801
  if (!cmd)
1033
802
  return { success: false, error: 'command 必填' };
1034
- // 2026-06-19: 支持多种 args 格式
1035
- // 1. JSON 数组: ["checkout", "-b", "branch"] (LLM 偏好)
1036
- // 2. 逗号分隔: "checkout,-b,branch" (旧格式)
1037
- // 3. 字符串: "checkout -b branch" (单参数)
1038
- let argList = [];
1039
- const rawArgs = args.args;
1040
- if (Array.isArray(rawArgs)) {
1041
- argList = rawArgs.map((s) => String(s).trim()).filter(Boolean);
1042
- }
1043
- else if (typeof rawArgs === 'string') {
1044
- const trimmed = rawArgs.trim();
1045
- if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
1046
- // JSON 数组字符串
1047
- try {
1048
- const parsed = JSON.parse(trimmed);
1049
- if (Array.isArray(parsed)) {
1050
- argList = parsed.map((s) => String(s).trim()).filter(Boolean);
1051
- }
1052
- }
1053
- catch { /* fall through to comma split */ }
1054
- }
1055
- if (argList.length === 0) {
1056
- // 逗号分隔 或 单字符串
1057
- argList = trimmed.split(',').map(s => s.trim()).filter(Boolean);
1058
- }
1059
- }
803
+ const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
1060
804
  const timeoutMs = Number(args.timeoutMs) || 30000;
1061
805
  const result = await shellExec(cmd, argList, { timeoutMs });
1062
806
  if (result.deniedByGuard) {
@@ -1082,10 +826,6 @@ class PiAgentSession {
1082
826
  return await runSelfImproveLoop(goal);
1083
827
  }
1084
828
  });
1085
- // 2026-06-24: Wallet + Polymarket + Safe 工具集 (基于 constraint-runtime/src/tools/)
1086
- // 真实可用实现: ethers v6 + JsonRpcProvider, 默认走 eth.llamarpc.com
1087
- // agent 可以创建/导入/查询 EVM 钱包, 查余额, 签名, 转账, 部署 Safe 多签钱包
1088
- this._registerWalletTools();
1089
829
  // list_skills 工具: 列出当前 session 已加载的 skills
1090
830
  // 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
1091
831
  this.tools.set('list_skills', {
@@ -1281,270 +1021,6 @@ class PiAgentSession {
1281
1021
  return { success: true, output: `✅ ${create ? 'created + checked out' : 'checked out'} ${name}\n${result.output || ''}` };
1282
1022
  }
1283
1023
  });
1284
- // 2026-06-19: 实用代码修改工具集 (M4) — 补全 read/delete/mkdir/move + grep/git log/show
1285
- // 这些是 LLM 自主完成代码修改的最小工具集, 之前只能通过 shell_exec 间接调用
1286
- // 通用文件读取 (支持任意扩展名, 跟 read_document 区别是后者只支持 txt/md/pdf/docx)
1287
- this.tools.set('read_file', {
1288
- name: 'read_file',
1289
- description: '读取任意文件内容 (相对 cwd). 与 read_document 区别: 不限制文件类型. 受 shell-guard 路径白名单保护.',
1290
- parameters: { path: '相对路径 (必填, e.g. "src/agents/pi-sdk.ts")', startLine: '起始行号 (可选, 默认 0)', maxLines: '最大行数 (可选, 默认 500)' },
1291
- execute: async (args) => {
1292
- const relPath = String(args.path || '').trim();
1293
- if (!relPath)
1294
- return { success: false, error: 'path 必填' };
1295
- const pathResult = checkWritePath(relPath);
1296
- if (!pathResult.allowed)
1297
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
1298
- try {
1299
- const absPath = path.resolve(this.cwd, relPath);
1300
- const content = fsSync.readFileSync(absPath, 'utf-8');
1301
- const start = Math.max(0, parseInt(String(args.startLine || '0')) || 0);
1302
- const max = parseInt(String(args.maxLines || '500')) || 500;
1303
- const lines = content.split('\n');
1304
- const slice = lines.slice(start, start + max);
1305
- return { success: true, output: `📄 ${relPath} (第 ${start + 1}-${start + slice.length} 行, 共 ${lines.length} 行):\n${slice.map((l, i) => `${String(start + i + 1).padStart(4)} | ${l}`).join('\n')}` };
1306
- }
1307
- catch (e) {
1308
- return { success: false, error: `读取失败: ${e.message}` };
1309
- }
1310
- }
1311
- });
1312
- // 删除文件
1313
- this.tools.set('delete_file', {
1314
- name: 'delete_file',
1315
- description: '删除一个文件. 受 shell-guard 路径白名单保护. 不可恢复, 调用前请确认.',
1316
- parameters: { path: '相对路径 (必填)' },
1317
- execute: async (args) => {
1318
- const relPath = String(args.path || '').trim();
1319
- if (!relPath)
1320
- return { success: false, error: 'path 必填' };
1321
- const pathResult = checkWritePath(relPath);
1322
- if (!pathResult.allowed)
1323
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
1324
- try {
1325
- const absPath = path.resolve(this.cwd, relPath);
1326
- if (!fsSync.existsSync(absPath))
1327
- return { success: false, error: `文件不存在: ${relPath}` };
1328
- fsSync.unlinkSync(absPath);
1329
- return { success: true, output: `✅ deleted ${relPath}` };
1330
- }
1331
- catch (e) {
1332
- return { success: false, error: `删除失败: ${e.message}` };
1333
- }
1334
- }
1335
- });
1336
- // 创建目录
1337
- this.tools.set('mkdir', {
1338
- name: 'mkdir',
1339
- description: '创建一个或多个目录. 自动 mkdir -p (父目录不存在也创建).',
1340
- parameters: { path: '目录路径 (必填, 相对 cwd)' },
1341
- execute: async (args) => {
1342
- const relPath = String(args.path || '').trim();
1343
- if (!relPath)
1344
- return { success: false, error: 'path 必填' };
1345
- const pathResult = checkWritePath(relPath);
1346
- if (!pathResult.allowed)
1347
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
1348
- try {
1349
- const absPath = path.resolve(this.cwd, relPath);
1350
- fsSync.mkdirSync(absPath, { recursive: true });
1351
- return { success: true, output: `✅ mkdir ${relPath}` };
1352
- }
1353
- catch (e) {
1354
- return { success: false, error: `创建失败: ${e.message}` };
1355
- }
1356
- }
1357
- });
1358
- // 移动/重命名文件
1359
- this.tools.set('move_file', {
1360
- name: 'move_file',
1361
- description: '移动或重命名文件. 源和目标路径都在白名单内才允许.',
1362
- parameters: { from: '源路径 (必填)', to: '目标路径 (必填)' },
1363
- execute: async (args) => {
1364
- const from = String(args.from || '').trim();
1365
- const to = String(args.to || '').trim();
1366
- if (!from || !to)
1367
- return { success: false, error: 'from 和 to 都必填' };
1368
- const fromCheck = checkWritePath(from);
1369
- if (!fromCheck.allowed)
1370
- return { success: false, error: `from 路径被护栏拒: ${fromCheck.reason}` };
1371
- const toCheck = checkWritePath(to);
1372
- if (!toCheck.allowed)
1373
- return { success: false, error: `to 路径被护栏拒: ${toCheck.reason}` };
1374
- try {
1375
- const fromAbs = path.resolve(this.cwd, from);
1376
- const toAbs = path.resolve(this.cwd, to);
1377
- if (!fsSync.existsSync(fromAbs))
1378
- return { success: false, error: `源文件不存在: ${from}` };
1379
- fsSync.mkdirSync(path.dirname(toAbs), { recursive: true });
1380
- fsSync.renameSync(fromAbs, toAbs);
1381
- return { success: true, output: `✅ ${from} → ${to}` };
1382
- }
1383
- catch (e) {
1384
- return { success: false, error: `移动失败: ${e.message}` };
1385
- }
1386
- }
1387
- });
1388
- // 文件内搜索 (类似 grep)
1389
- this.tools.set('grep_files', {
1390
- name: 'grep_files',
1391
- description: '在文件中搜索匹配 pattern 的行. 类似 grep -rn. 路径必须在白名单.',
1392
- parameters: { pattern: '搜索 pattern (必填, 字符串, 不是正则)', path: '搜索目录 (可选, 默认 .)', filePattern: '文件名 glob (可选, e.g. "*.ts")' },
1393
- execute: async (args) => {
1394
- const pattern = String(args.pattern || '').trim();
1395
- if (!pattern)
1396
- return { success: false, error: 'pattern 必填' };
1397
- const searchPath = String(args.path || '.').trim();
1398
- const pathResult = checkWritePath(searchPath);
1399
- if (!pathResult.allowed)
1400
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
1401
- try {
1402
- const { execFile } = await import('child_process');
1403
- const { promisify } = await import('util');
1404
- const pExecFile = promisify(execFile);
1405
- const argv = ['-rn', '--include=' + (args.filePattern || '*'), pattern, searchPath];
1406
- const { stdout, stderr } = await pExecFile('grep', argv, { cwd: this.cwd, maxBuffer: 1024 * 1024 });
1407
- const lines = stdout.split('\n').filter(Boolean).slice(0, 50);
1408
- return { success: true, output: `🔍 grep "${pattern}" in ${searchPath} (${args.filePattern || '*'}, 最多 50 行):\n${lines.join('\n')}${lines.length === 50 ? '\n... (truncated)' : ''}` };
1409
- }
1410
- catch (e) {
1411
- if (e.code === 1)
1412
- return { success: true, output: `🔍 grep "${pattern}" in ${searchPath}: 0 matches` };
1413
- return { success: false, error: `grep 失败: ${e.message}` };
1414
- }
1415
- }
1416
- });
1417
- // glob 找文件
1418
- this.tools.set('glob_files', {
1419
- name: 'glob_files',
1420
- description: '用 glob pattern 找文件. 例如 "**/*.test.ts".',
1421
- parameters: { pattern: 'glob pattern (必填, e.g. "src/**/*.ts")' },
1422
- execute: async (args) => {
1423
- const pattern = String(args.pattern || '').trim();
1424
- if (!pattern)
1425
- return { success: false, error: 'pattern 必填' };
1426
- try {
1427
- const pathResult = checkWritePath(pattern.replace(/\*\*.*$/, '').replace(/\/\*.*$/, '') || '.');
1428
- if (!pathResult.allowed && pattern !== '**/*' && pattern !== '*')
1429
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
1430
- const { execFile } = await import('child_process');
1431
- const { promisify } = await import('util');
1432
- const pExecFile = promisify(execFile);
1433
- // 用 shell 的 find + glob (避免引入额外依赖)
1434
- const { stdout } = await pExecFile('find', [this.cwd, '-path', `*${pattern.replace(/\*\*\//, '*').replace(/\*\*$/, '*').replace(/\*$/, '*')}`, '-type', 'f'], { maxBuffer: 1024 * 1024 });
1435
- const files = stdout.split('\n').filter(Boolean).slice(0, 100);
1436
- const relFiles = files.map(f => path.relative(this.cwd, f));
1437
- return { success: true, output: `🔍 glob "${pattern}" 找到 ${relFiles.length} 个文件${relFiles.length === 100 ? ' (truncated)' : ''}:\n${relFiles.join('\n')}` };
1438
- }
1439
- catch (e) {
1440
- return { success: false, error: `glob 失败: ${e.message}` };
1441
- }
1442
- }
1443
- });
1444
- // git log
1445
- this.tools.set('git_log', {
1446
- name: 'git_log',
1447
- description: '查看 git log. 默认 --oneline -10. 支持过滤和范围.',
1448
- parameters: { range: '可选, e.g. "HEAD~5..HEAD" 或分支名', maxCount: '可选, 默认 10', oneline: '可选, "true" 用 --oneline (默认 true)' },
1449
- execute: async (args) => {
1450
- const range = String(args.range || '').trim();
1451
- const maxCount = parseInt(String(args.maxCount || '10')) || 10;
1452
- const oneline = String(args.oneline || 'true') === 'true';
1453
- const argv = ['log'];
1454
- if (oneline)
1455
- argv.push('--oneline');
1456
- if (maxCount > 0)
1457
- argv.push(`-n`, String(maxCount));
1458
- if (range)
1459
- argv.push(range);
1460
- const result = await shellExec('git', argv, { timeoutMs: 10_000 });
1461
- if (result.deniedByGuard)
1462
- return { success: false, error: result.error };
1463
- if (!result.success)
1464
- return { success: false, error: result.error, output: result.output };
1465
- return { success: true, output: `📜 git log:\n${result.output || '(empty)'}` };
1466
- }
1467
- });
1468
- // git show
1469
- this.tools.set('git_show', {
1470
- name: 'git_show',
1471
- description: '查看 commit 内容. 默认 HEAD. 支持 --stat 看统计, --patch 看 diff.',
1472
- parameters: { ref: '可选, 默认 HEAD', stat: '可选, "true" 只看 stat (默认 false)' },
1473
- execute: async (args) => {
1474
- const ref = String(args.ref || 'HEAD').trim();
1475
- const argv = ['show', ref];
1476
- if (String(args.stat || 'false') === 'true')
1477
- argv.push('--stat');
1478
- const result = await shellExec('git', argv, { timeoutMs: 15_000 });
1479
- if (result.deniedByGuard)
1480
- return { success: false, error: result.error };
1481
- if (!result.success)
1482
- return { success: false, error: result.error, output: result.output };
1483
- return { success: true, output: `📜 git show ${ref}:\n${result.output || '(empty)'}` };
1484
- }
1485
- });
1486
- // git stash
1487
- this.tools.set('git_stash', {
1488
- name: 'git_stash',
1489
- description: 'git stash 暂存当前未提交改动. action: save/pop/list/apply/drop. 支持 message.',
1490
- parameters: { action: '动作 (必填, "save" | "pop" | "list" | "apply" | "drop")', message: '可选, save 时的描述', index: '可选, apply/pop/drop 的 stash index (e.g. "0")' },
1491
- execute: async (args) => {
1492
- const action = String(args.action || '').trim();
1493
- if (!['save', 'pop', 'list', 'apply', 'drop'].includes(action)) {
1494
- return { success: false, error: `action 必须是 save/pop/list/apply/drop 之一, 收到: ${action}` };
1495
- }
1496
- const argv = ['stash', action];
1497
- if (action === 'save' && args.message)
1498
- argv.push('-m', String(args.message));
1499
- if ((action === 'apply' || action === 'drop' || action === 'pop') && args.index) {
1500
- argv.push('stash@{' + String(args.index) + '}');
1501
- }
1502
- const result = await shellExec('git', argv, { timeoutMs: 10_000 });
1503
- if (result.deniedByGuard)
1504
- return { success: false, error: result.error };
1505
- if (!result.success)
1506
- return { success: false, error: result.error, output: result.output };
1507
- return { success: true, output: `✅ git stash ${action}\n${result.output || ''}` };
1508
- }
1509
- });
1510
- // vitest 跑测试
1511
- this.tools.set('vitest_run', {
1512
- name: 'vitest_run',
1513
- description: '跑 vitest 测试. 自动 bail (失败就停). 默认 60s timeout.',
1514
- parameters: { pattern: '可选, 文件 glob e.g. "src/agents/pi-sdk.test.ts"', timeoutMs: '可选, 默认 60000' },
1515
- execute: async (args) => {
1516
- const argv = ['vitest', 'run', '--reporter=default', '--no-color', '--bail=1'];
1517
- if (args.pattern)
1518
- argv.push(String(args.pattern));
1519
- const timeoutMs = parseInt(String(args.timeoutMs || '60000')) || 60000;
1520
- const result = await shellExec('npx', argv, { timeoutMs });
1521
- if (result.deniedByGuard)
1522
- return { success: false, error: result.error };
1523
- if (!result.success)
1524
- return { success: false, error: result.error || 'vitest 失败', output: result.output };
1525
- return { success: true, output: `✅ vitest 通过:\n${(result.output || '').slice(0, 2000)}` };
1526
- }
1527
- });
1528
- // tsc 编译检查
1529
- this.tools.set('tsc_check', {
1530
- name: 'tsc_check',
1531
- description: '跑 tsc --noEmit 检查 TypeScript 编译. 默认 60s timeout.',
1532
- parameters: { project: '可选, tsconfig 路径 (默认 tsconfig.json)', timeoutMs: '可选, 默认 60000' },
1533
- execute: async (args) => {
1534
- const argv = ['tsc', '--noEmit'];
1535
- if (args.project)
1536
- argv.push('-p', String(args.project));
1537
- else
1538
- argv.push('-p', 'tsconfig.json');
1539
- const timeoutMs = parseInt(String(args.timeoutMs || '60000')) || 60000;
1540
- const result = await shellExec('npx', argv, { timeoutMs });
1541
- if (result.deniedByGuard)
1542
- return { success: false, error: result.error };
1543
- if (!result.success)
1544
- return { success: false, error: result.error || 'tsc 失败', output: result.output };
1545
- return { success: true, output: `✅ tsc 通过:\n${(result.output || 'no errors').slice(0, 1000)}` };
1546
- }
1547
- });
1548
1024
  // M3.2: 任务状态机工具 — 让 agent 自己维护 multi_step 任务的状态
1549
1025
  this.tools.set('create_task', {
1550
1026
  name: 'create_task',
@@ -1685,11 +1161,6 @@ class PiAgentSession {
1685
1161
  // - 失败结果不缓存 (避免缓存 transient 错误)
1686
1162
  // - 缓存容量 200 条, 超过就清空
1687
1163
  this.wrapToolsWithIdempotency();
1688
- // 2026-06-30: 镜像到 ToolRegistry — resolveToolName 走 registry 的 alias 表.
1689
- // 被 wrap 的 tool (idempotency cache) 也直接拿来用, registry 不重复 wrap.
1690
- for (const [name, tool] of this.tools.entries()) {
1691
- this._toolRegistry.register(tool);
1692
- }
1693
1164
  }
1694
1165
  /** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
1695
1166
  idempotencyCache = new Map();
@@ -1736,13 +1207,10 @@ class PiAgentSession {
1736
1207
  // M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
1737
1208
  if (this.cachedToolDefinitions)
1738
1209
  return this.cachedToolDefinitions;
1739
- const defs = ['可用工具 (name(params) - 简介):'];
1210
+ const defs = ['可用工具:'];
1740
1211
  for (const tool of this.tools.values()) {
1741
- // 2026-06-19: 压缩 tool 定义 只显示参数名 (不显示描述, 减少 60% 长度)
1742
- // 完整 description history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
1743
- // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
1744
- const paramNames = Object.keys(tool.parameters).join(',');
1745
- defs.push(`- ${tool.name}(${paramNames})`);
1212
+ const params = Object.entries(tool.parameters).map(([k, v]) => `${k}: ${v}`).join(', ');
1213
+ defs.push(`- ${tool.name}(${params}) - ${tool.description}`);
1746
1214
  }
1747
1215
  this.cachedToolDefinitions = defs.join('\n');
1748
1216
  return this.cachedToolDefinitions;
@@ -1839,27 +1307,15 @@ class PiAgentSession {
1839
1307
  }
1840
1308
  }
1841
1309
  async promptStream(input, onStream, signal, channelId) {
1842
- console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
1843
1310
  this.minimaxAvailable = this.checkMinimax();
1844
- console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
1845
1311
  this.currentChannelId = channelId ?? this.currentChannelId;
1846
- // 2026-06-18 (supervisor): web server 把 46K markedPrompt 喂过来
1847
- // (【本轮用户请求】\n<text>\n【请求结束】\n\n<contextHint>).
1848
- // 整个 input 走下游, pivot loop 之前拿 47K buildContext 当 user message 发出去,
1849
- // 模型撞 context window. 提取 userText 替代 input, contextHint 拼到 systemPrompt 末尾.
1850
- const markerMatch = input.match(/【本轮用户请求】\s*([\s\S]*?)\s*【请求结束】/);
1851
- const userText = markerMatch ? markerMatch[1].trim() : input;
1852
- const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
1853
- console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
1854
1312
  this.messageHistory.push({
1855
1313
  role: 'user',
1856
- content: userText
1314
+ content: input
1857
1315
  });
1858
- // 2026-06-18: web server 喂的 markedPrompt 外的 contextHint 拼到 system 末尾 (而不是当 user message)
1859
- this.contextHintAddition = contextHint;
1860
1316
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1861
1317
  if (!this.minimaxAvailable) {
1862
- const response = await this.handleFallback(userText);
1318
+ const response = await this.handleFallback(input);
1863
1319
  this.messageHistory.push({ role: 'assistant', content: response });
1864
1320
  onStream({ type: 'done', content: '' });
1865
1321
  return response;
@@ -1867,11 +1323,11 @@ class PiAgentSession {
1867
1323
  // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1868
1324
  this.currentOnStream = onStream;
1869
1325
  this.currentSignal = signal ?? null;
1870
- await this.computeJudgmentGate(userText);
1326
+ await this.computeJudgmentGate(input);
1871
1327
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1872
1328
  try {
1873
1329
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1874
- this.currentIntent = classifyIntent(userText);
1330
+ this.currentIntent = classifyIntent(input);
1875
1331
  this.currentIntentHint = intentHint(this.currentIntent);
1876
1332
  if (this.currentIntent !== 'chitchat') {
1877
1333
  onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
@@ -1892,13 +1348,9 @@ class PiAgentSession {
1892
1348
  }
1893
1349
  // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
1894
1350
  // (失败静默, 5s 限流防止循环)
1895
- // 2026-07-04: 透传 agentId 让 onSessionStart 加载 persona 文档
1896
1351
  let bootstrapAddition = '';
1897
1352
  try {
1898
- const ss = await onSessionStart({
1899
- channelId: this.currentChannelId || undefined,
1900
- agentId: this.currentAgentId || undefined,
1901
- });
1353
+ const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
1902
1354
  bootstrapAddition = ss.systemAddition || '';
1903
1355
  }
1904
1356
  catch (err) {
@@ -1920,7 +1372,7 @@ class PiAgentSession {
1920
1372
  if (this.usePivotLoop) {
1921
1373
  let pivotResult = '';
1922
1374
  try {
1923
- const lr = await this.promptWithPivotLoop(userText, undefined, channelId);
1375
+ const lr = await this.promptWithPivotLoop(input, undefined, channelId);
1924
1376
  pivotResult = lr.response || '';
1925
1377
  onStream({ type: 'done', content: '' });
1926
1378
  }
@@ -1944,7 +1396,7 @@ class PiAgentSession {
1944
1396
  }
1945
1397
  catch { }
1946
1398
  }
1947
- monitorAfterReply(userText, pivotResult);
1399
+ monitorAfterReply(input, pivotResult);
1948
1400
  const stopStartTime = this.promptStartTime || Date.now();
1949
1401
  onStop({
1950
1402
  channelId: this.currentChannelId || 'unknown',
@@ -1955,7 +1407,6 @@ class PiAgentSession {
1955
1407
  this.currentOnStream = null;
1956
1408
  this.currentSignal = null;
1957
1409
  this.bootstrapAddition = '';
1958
- this.contextHintAddition = '';
1959
1410
  this.promptStartTime = 0;
1960
1411
  }
1961
1412
  return pivotResult;
@@ -2089,45 +1540,37 @@ class PiAgentSession {
2089
1540
  }
2090
1541
  // M2.4: persona 缓存
2091
1542
  if (!this.cachedPersonaSection && this.persona) {
2092
- this.cachedPersonaSection = `
2093
- 角色描述: ${this.persona.description || '无'}
2094
- 性格特点: ${this.persona.personality || '无'}
2095
- 问候语: ${this.persona.greeting || '无'}
1543
+ this.cachedPersonaSection = `
1544
+ 角色描述: ${this.persona.description || '无'}
1545
+ 性格特点: ${this.persona.personality || '无'}
1546
+ 问候语: ${this.persona.greeting || '无'}
2096
1547
  `;
2097
1548
  }
2098
- const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${this.cachedPersonaSection}
2099
- 当前工作目录: ${this.cwd}
2100
- 当前身份: ${this.identity.name} (${this.identity.did})
2101
- ${this.currentIntentHint}
2102
-
2103
- ${this.getToolDefinitions()}
2104
-
2105
- 工作模式:
2106
- 1. 理解用户自然语言请求
2107
- 2. 分析需要哪些工具来完成
2108
- 3. 按顺序调用工具并观察结果
2109
- 4. 根据观察结果决定下一步
2110
- 5. 最终给出完整回答
2111
-
2112
- 重要 (一次命中要求):
2113
- - 每次只调用一个工具
2114
- - 仔细分析工具返回结果
2115
- - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
2116
- - 如果需要更多信息,继续调用工具
2117
-
2118
- 【工具调用格式 (严格遵守, 否则系统无法解析)】
2119
- - 你只能输出**一个**工具调用, 不要堆叠多个 invoke
2120
- - 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
2121
- - 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
2122
- - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
2123
- - 工具调用后必须等结果, 不要在同一个回复里继续输出
2124
- - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1549
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${this.cachedPersonaSection}
1550
+ 当前工作目录: ${this.cwd}
1551
+ 当前身份: ${this.identity.name} (${this.identity.did})
1552
+ ${this.currentIntentHint}
1553
+
1554
+ ${this.getToolDefinitions()}
1555
+
1556
+ 工作模式:
1557
+ 1. 理解用户自然语言请求
1558
+ 2. 分析需要哪些工具来完成
1559
+ 3. 按顺序调用工具并观察结果
1560
+ 4. 根据观察结果决定下一步
1561
+ 5. 最终给出完整回答
1562
+
1563
+ 重要:
1564
+ - 每次只调用一个工具
1565
+ - 仔细分析工具返回结果
1566
+ - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1567
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
2125
1568
  // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
2126
1569
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
2127
1570
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
2128
1571
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
2129
1572
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
2130
- const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined, this.currentSignal ?? undefined);
1573
+ const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined);
2131
1574
  this.messageHistory.push({ role: 'user', content: input });
2132
1575
  if (result.response) {
2133
1576
  this.messageHistory.push({ role: 'assistant', content: result.response });
@@ -2171,12 +1614,10 @@ ${this.getToolDefinitions()}
2171
1614
  while (iteration < this.MAX_REACT_ITERATIONS) {
2172
1615
  iteration++;
2173
1616
  // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
2174
- // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
2175
- const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
2176
- if (maxIterDecision.shouldExit) {
1617
+ if (iteration >= this.MAX_REACT_ITERATIONS) {
2177
1618
  console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
2178
1619
  onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
2179
- finalResponse = finalResponse || maxIterDecision.finalAnswer;
1620
+ finalResponse = finalResponse || '(本轮 ReAct 循环达到最大步数, 强制结束)';
2180
1621
  break;
2181
1622
  }
2182
1623
  // 停止条件 2: signal.aborted (显式 abort / 用户中断)
@@ -2190,23 +1631,14 @@ ${this.getToolDefinitions()}
2190
1631
  if (totalErrors >= this.MAX_TOTAL_ERRORS) {
2191
1632
  console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
2192
1633
  onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
2193
- // 2026-06-19: 即使 LLM 一直失败, 也汇总之前成功执行的 tool result 给用户
2194
- if (this.successfulToolResults.length > 0) {
2195
- finalResponse = `✅ 之前步骤成功执行了 ${this.successfulToolResults.length} 个工具 (但 LLM 后续 ${totalErrors} 次调用失败):\n` +
2196
- this.successfulToolResults.map((r, i) => ` ${i + 1}. ${r.tool}: ${r.outputPreview}`).join('\n') +
2197
- `\n\n⚠️ (LLM 连续失败, 可能是 minimax 上游限流/网络问题, 工具已成功执行但 LLM 没能继续总结)`;
2198
- }
2199
- else {
2200
- finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
2201
- }
1634
+ finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
2202
1635
  break;
2203
1636
  }
2204
1637
  // 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
2205
1638
  // compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
2206
- // 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
2207
1639
  const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
2208
1640
  const estimatedTokensBefore = this.estimateHistoryTokens();
2209
- if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
1641
+ if (estimatedTokensBefore > compactThreshold) {
2210
1642
  const tokensBeforeCompact = estimatedTokensBefore;
2211
1643
  console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
2212
1644
  onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
@@ -2219,13 +1651,11 @@ ${this.getToolDefinitions()}
2219
1651
  }
2220
1652
  }
2221
1653
  // 停止条件 3: context overflow (compact 后还超, 强制终止)
2222
- // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
2223
1654
  const estimatedTokens = this.estimateHistoryTokens();
2224
- const overflowDecision = decideContextOverflow(estimatedTokens, this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD);
2225
- if (overflowDecision.shouldExit) {
1655
+ if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
2226
1656
  console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
2227
1657
  onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
2228
- finalResponse = finalResponse || overflowDecision.finalAnswer;
1658
+ finalResponse = finalResponse || '(本轮 ReAct 循环因上下文溢出终止)';
2229
1659
  break;
2230
1660
  }
2231
1661
  // 调试日志:显示每次循环开始
@@ -2250,33 +1680,33 @@ ${this.getToolDefinitions()}
2250
1680
  }
2251
1681
  // M2.4: persona section 缓存 — persona 在 loadPersona() 时一次设定, 此后不变
2252
1682
  if (!this.cachedPersonaSection && this.persona) {
2253
- this.cachedPersonaSection = `
2254
- 角色描述: ${this.persona.description || '无'}
2255
- 性格特点: ${this.persona.personality || '无'}
2256
- 问候语: ${this.persona.greeting || '无'}
1683
+ this.cachedPersonaSection = `
1684
+ 角色描述: ${this.persona.description || '无'}
1685
+ 性格特点: ${this.persona.personality || '无'}
1686
+ 问候语: ${this.persona.greeting || '无'}
2257
1687
  `;
2258
1688
  }
2259
1689
  const personaSection = this.cachedPersonaSection;
2260
- const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
2261
- 当前工作目录: ${this.cwd}
2262
- 当前身份: ${this.identity.name} (${this.identity.did})
2263
- ${refineContext}
2264
- ${this.currentIntentHint}
2265
-
2266
- ${toolDefs}
2267
-
2268
- 工作模式:
2269
- 1. 理解用户自然语言请求
2270
- 2. 分析需要哪些工具来完成
2271
- 3. 按顺序调用工具并观察结果
2272
- 4. 根据观察结果决定下一步
2273
- 5. 最终给出完整回答
2274
-
2275
- 重要:
2276
- - 每次只调用一个工具
2277
- - 仔细分析工具返回结果
2278
- - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
2279
- - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}${this.contextHintAddition}`;
1690
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1691
+ 当前工作目录: ${this.cwd}
1692
+ 当前身份: ${this.identity.name} (${this.identity.did})
1693
+ ${refineContext}
1694
+ ${this.currentIntentHint}
1695
+
1696
+ ${toolDefs}
1697
+
1698
+ 工作模式:
1699
+ 1. 理解用户自然语言请求
1700
+ 2. 分析需要哪些工具来完成
1701
+ 3. 按顺序调用工具并观察结果
1702
+ 4. 根据观察结果决定下一步
1703
+ 5. 最终给出完整回答
1704
+
1705
+ 重要:
1706
+ - 每次只调用一个工具
1707
+ - 仔细分析工具返回结果
1708
+ - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1709
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
2280
1710
  // 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
2281
1711
  // 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
2282
1712
  // 2. reactive compaction (prompt 估算超阈值, 跑压缩)
@@ -2284,31 +1714,18 @@ ${toolDefs}
2284
1714
  // 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
2285
1715
  const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream);
2286
1716
  const reply = (response.reply || '').trim();
2287
- // 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
2288
- const nativeToolCalls = response.toolCalls;
2289
- // 2026-06-19 架构 fix: 不再因 [AI 服务调用失败] break
2290
- // 旧逻辑: sentinel aiFailed=true break → 外层 retry 整个 loop (重置 history)
2291
- // 新逻辑: 把错误当 tool_result push 进 history → 下一轮 LLM 看到错误能反思重试
2292
- // 这是 dive-into 文档的"fail-open error recovery" — 错误进入 context, 不让 LLM 重复犯同样错
1717
+ // 2026-06-16: 看到 [AI 服务调用失败] sentinel 不再立即 break,
1718
+ // 而是设 aiFailed=true, 让外层 promptStream 自动重跑整个 loop 最多 N 次
1719
+ // (LLM API 401 / 网络错 / 配额满时, pi-ai 返回这个 prefix;
1720
+ // 自动 retry 兜底: 临时网络抖动可自愈, 真挂 N 次后才报失败)
2293
1721
  if (reply.startsWith('[AI 服务调用失败]')) {
2294
- console.log(`[PiAgent] 收到 AI 错误 sentinel, 推到 history LLM 反思, 继续 loop`);
2295
- console.log(`[sentinel DEBUG] 完整 reply: ${reply}`);
2296
- console.log(`[sentinel DEBUG] 上一轮 messages 数量: ${Array.isArray(messages) ? messages.length : 'N/A'}, systemPrompt 长度: ${systemPrompt.length}`);
1722
+ console.log(`[PiAgent] 收到 AI 错误 sentinel, 标记 aiFailed, 外层会自动重试整个 loop`);
1723
+ aiFailed = true;
2297
1724
  aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
2298
- totalErrors++;
2299
- consecutiveErrors++;
2300
- // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
2301
- this.messageHistory.push({
2302
- role: 'system',
2303
- content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
2304
- });
2305
1725
  if (onStream) {
2306
- onStream({ type: 'status', content: `⚠️ AI 调用失败 ${totalErrors}/${this.MAX_TOTAL_ERRORS}, push 错误到 history 让 LLM 反思`, tool: 'system' });
1726
+ onStream({ type: 'status', content: `⚠️ AI 调用失败, 将自动重试整个 loop`, tool: 'system' });
2307
1727
  }
2308
- // 退避 2s 后继续 — 临时 minimax 限流避开, 不让 loop 终止
2309
- await new Promise(resolve => setTimeout(resolve, 2000));
2310
- // 关键: 不设 aiFailed=true, 让外层不重试整个 loop (重置 history), 继续内层循环
2311
- continue;
1728
+ break;
2312
1729
  }
2313
1730
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
2314
1731
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
@@ -2316,14 +1733,20 @@ ${toolDefs}
2316
1733
  if (onStream) {
2317
1734
  onStream({ type: 'token', content: reply.substring(0, 100) });
2318
1735
  }
2319
- // 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
2320
- // 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
2321
- // 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
1736
+ if (this.isFinalResponse(reply)) {
1737
+ // 检查质量分数
1738
+ lastQualityScore = this.estimateResponseQuality(reply);
1739
+ // 如果质量太低且还有改进机会,进入改进循环
1740
+ if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
1741
+ refineAttempts++;
1742
+ console.log(`[PiAgent] 质量评分 ${(lastQualityScore * 10).toFixed(1)}/10 < ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10,自动改进中 (${refineAttempts}/${this.MAX_REFINE_ATTEMPTS})`);
1743
+ continue;
1744
+ }
1745
+ finalResponse = this.extractFinalAnswer(reply);
1746
+ break;
1747
+ }
2322
1748
  const toolCall = this.parseToolCall(reply);
2323
- // 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
2324
- // OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
2325
1749
  if (toolCall) {
2326
- toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2327
1750
  this.messageHistory.push({
2328
1751
  role: 'assistant',
2329
1752
  content: reply,
@@ -2489,7 +1912,7 @@ ${toolDefs}
2489
1912
  catch (err) {
2490
1913
  console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
2491
1914
  }
2492
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
1915
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
2493
1916
  this.logToHarness(toolCall.name, toolCall.args, result);
2494
1917
  // 通知前端工具执行结果
2495
1918
  if (onStream) {
@@ -2521,16 +1944,6 @@ ${toolDefs}
2521
1944
  }
2522
1945
  if (result.success) {
2523
1946
  consecutiveErrors = 0; // 重置连续错误计数
2524
- // 2026-06-19: 记录成功结果, 用于 LLM 失败退出时汇总给用户
2525
- if (result.output) {
2526
- this.successfulToolResults.push({
2527
- tool: toolCall.name,
2528
- outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '')
2529
- });
2530
- }
2531
- else {
2532
- this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
2533
- }
2534
1947
  // 检查工具执行质量
2535
1948
  lastQualityScore = this.estimateToolResultQuality(result);
2536
1949
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
@@ -2602,36 +2015,6 @@ ${toolDefs}
2602
2015
  if (onStream) {
2603
2016
  onStream({ type: 'token', content: reply.substring(0, 150) });
2604
2017
  }
2605
- // 2026-06-19 架构 fix: 只有 strip <think> 后才检查 isFinalResponse
2606
- // (parseToolCall 已先尝试, 既然没解析出 tool_call, 现在检查 final gen 是否真的在最终回答区)
2607
- if (this.isFinalResponse(reply)) {
2608
- // 2026-06-19 dive-into 风格修复: 如果还有 successful tool results 没汇报,
2609
- // LLM 不能提前 final_gen — harness 自动注入"请汇报剩余工具结果" hint 再 continue
2610
- // 这是 dive-into 文档"step 9 stop condition check" 的具体化:
2611
- // stop condition = (有工具结果未汇报) ? continue : break
2612
- // 检查回复是否包含工具结果内容(避免无限循环)
2613
- const hasToolResultContent = this.successfulToolResults.some(r => reply.includes(r.tool) || reply.includes(r.outputPreview.substring(0, 50)));
2614
- // 如果回复包含工具结果内容,清除 successfulToolResults
2615
- if (hasToolResultContent) {
2616
- console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
2617
- this.successfulToolResults = [];
2618
- }
2619
- if (this.successfulToolResults.length > 0 && iteration < this.MAX_REACT_ITERATIONS) {
2620
- const unreported = this.successfulToolResults.length;
2621
- console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报, push hint 让其继续`);
2622
- this.messageHistory.push({
2623
- role: 'system',
2624
- content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
2625
- });
2626
- if (onStream) {
2627
- onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结`, tool: 'system' });
2628
- }
2629
- continue;
2630
- }
2631
- lastQualityScore = this.estimateResponseQuality(reply);
2632
- finalResponse = this.extractFinalAnswer(reply);
2633
- break;
2634
- }
2635
2018
  // 检查是否需要继续循环处理
2636
2019
  // 更严格的判断:只有当回复明确表示需要更多信息时才继续
2637
2020
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
@@ -2677,12 +2060,12 @@ ${toolDefs}
2677
2060
  onStream({ type: 'status', content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2678
2061
  }
2679
2062
  const now = new Date().toISOString();
2680
- const identityPrefix = `${this.identity.name} | bolloon 智能体
2681
- <environment_details>
2682
- Current time: ${now}
2683
- Working directory: ${this.cwd}
2684
- Workspace root folder: ${this.cwd}
2685
- </environment_details>
2063
+ const identityPrefix = `${this.identity.name} | bolloon 智能体
2064
+ <environment_details>
2065
+ Current time: ${now}
2066
+ Working directory: ${this.cwd}
2067
+ Workspace root folder: ${this.cwd}
2068
+ </environment_details>
2686
2069
  `;
2687
2070
  finalResponse = identityPrefix + finalResponse;
2688
2071
  this.messageHistory.push({ role: 'assistant', content: finalResponse });
@@ -2774,45 +2157,18 @@ Workspace root folder: ${this.cwd}
2774
2157
  for (const m of recentMessages) {
2775
2158
  const role = m.role;
2776
2159
  let content = m.content;
2777
- // 2026-06-30 修: OpenAI 协议 tool role 必须带 tool_call_id, 否则 minimax (OpenAI-compatible) 400
2778
- // bolloon 之前把所有 tool result 包成 "[工具结果] ..." 当 user/assistant role 发, minimax 严格校验失败
2779
- // 现在: 保留 role='tool' + 加 tool_call_id 字段 (用 messageHistory 里自己生成的 id)
2160
+ // tool role: toolResult 序列化 ( buildContext 一样)
2780
2161
  if (role === 'tool') {
2781
2162
  const result = m.toolResult ? JSON.stringify(m.toolResult) : content;
2782
2163
  content = `[工具结果] ${result}`;
2783
- // MiniMax 等 API 不支持 tool role, 转为 user role
2784
- out.push({ role: 'user', content });
2785
- continue;
2786
2164
  }
2787
2165
  // system role (router hint 等) 直接保留
2788
2166
  if (role === 'system') {
2789
2167
  out.push({ role: 'system', content });
2790
2168
  continue;
2791
2169
  }
2792
- // 2026-06-30 修: assistant 消息如果带 toolCall (bolloon 内部对象), emit OpenAI 协议的 tool_calls 数组
2793
- // minimax 严格要求 assistant 消息含 tool_calls 字段, 后续 tool result 才能引用 tool_call_id
2794
- if (role === 'assistant') {
2795
- const tc = m.toolCall;
2796
- if (tc && tc.id) {
2797
- out.push({
2798
- role: 'assistant',
2799
- content: content || '',
2800
- tool_calls: [{
2801
- id: tc.id,
2802
- type: 'function',
2803
- function: {
2804
- name: tc.name,
2805
- arguments: JSON.stringify(tc.args || {}),
2806
- },
2807
- }],
2808
- });
2809
- }
2810
- else {
2811
- out.push({ role, content });
2812
- }
2813
- continue;
2814
- }
2815
- if (role === 'user') {
2170
+ // assistant / user / tool 直接转
2171
+ if (role === 'user' || role === 'assistant' || role === 'tool') {
2816
2172
  out.push({ role, content });
2817
2173
  }
2818
2174
  }
@@ -2899,8 +2255,7 @@ Workspace root folder: ${this.cwd}
2899
2255
  // M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
2900
2256
  // 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
2901
2257
  const response = await llm.chat(contextOrMessages, systemPrompt, signal);
2902
- // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
2903
- return { reply: response.reply || '', toolCalls: response.toolCalls };
2258
+ return { reply: response.reply || '' };
2904
2259
  }
2905
2260
  catch (err) {
2906
2261
  // 用户主动 abort: 不重试, 立即抛
@@ -3026,375 +2381,100 @@ Workspace root folder: ${this.cwd}
3026
2381
  }
3027
2382
  }
3028
2383
  isFinalResponse(content) {
3029
- // 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
3030
- return isFinalResponseImpl(content, this._parseCtx());
2384
+ // 只有明确输出 <final gen> 才认为是最终回答
2385
+ return content.includes('<final gen>');
3031
2386
  }
3032
2387
  extractFinalAnswer(content) {
3033
- // 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
3034
- return extractFinalAnswerImpl(content);
3035
- }
3036
- _parseCtx() {
3037
- return {
3038
- tools: new Set(Array.from(this.tools.keys())),
3039
- resolveAlias: (name) => this.resolveToolName(name),
3040
- };
3041
- }
3042
- parseToolCall(content) {
3043
- // 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
3044
- return parseToolCallImpl(content, this._parseCtx());
3045
- }
3046
- // [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
3047
- _dbgParseToolCall(content) {
3048
- const r = this.parseToolCall(content);
3049
- console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
3050
- return r;
3051
- }
3052
- /**
3053
- * 2026-06-24: 注册 Wallet + Polymarket + Safe 工具
3054
- * 真实实现 (ethers v6 + JsonRpcProvider) — agent 可以创建 EVM 钱包, 查余额, 转账, 签名, Polymarket 下单
3055
- * 工具来源: src/constraint-runtime/src/tools/{WalletTools,PolymarketSDK,SafeSDK}/
3056
- */
3057
- _registerWalletTools() {
3058
- // === WalletTools (EVM 钱包) ===
3059
- this.tools.set('wallet_create', {
3060
- name: 'wallet_create',
3061
- description: '创建新 EVM 钱包 (BIP-39 12 词助记词 + 私钥 + 地址). 私钥会在响应中返回, agent 应当保存到 ~/.bolloon/wallets/<address>.json',
3062
- parameters: {},
3063
- execute: async () => {
3064
- try {
3065
- const { createWallet } = await import('../constraint-runtime/dist/tools/WalletTools/createWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/createWallet.js'));
3066
- const r = await createWallet();
3067
- return { success: true, output: `✅ 钱包创建成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n mnemonic: ${r.mnemonic}\n createdAt: ${r.createdAt}\n\n⚠️ 请 agent 立即把私钥保存到 ~/.bolloon/wallets/${r.address}.json` };
3068
- }
3069
- catch (e) {
3070
- return { success: false, error: `创建失败: ${String(e.message || e)}` };
3071
- }
3072
- }
3073
- });
3074
- this.tools.set('wallet_import', {
3075
- name: 'wallet_import',
3076
- description: '导入已有 EVM 钱包 (用助记词或私钥)',
3077
- parameters: { mnemonic: '可选, 12/15/18/21/24 词助记词', privateKey: '可选, 0x 开头的私钥' },
3078
- execute: async (args) => {
3079
- try {
3080
- const { importWallet } = await import('../constraint-runtime/dist/tools/WalletTools/importWallet.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/importWallet.js'));
3081
- const r = await importWallet({ mnemonic: args.mnemonic, privateKey: args.privateKey });
3082
- return { success: true, output: `✅ 钱包导入成功:\n address: ${r.address}\n privateKey: ${r.privateKey}\n source: ${r.source}` };
3083
- }
3084
- catch (e) {
3085
- return { success: false, error: `导入失败: ${String(e.message || e)}` };
3086
- }
2388
+ // 提取 <final gen> 后的内容作为最终回答
2389
+ const marker = '<final gen>';
2390
+ const markerIndex = content.indexOf(marker);
2391
+ if (markerIndex !== -1) {
2392
+ const after = content.substring(markerIndex + marker.length).trim();
2393
+ // v3 修复: 如果 <final gen> 之后是空, fallback 用 marker 之前的内容 (去掉 marker)
2394
+ // 否则 LLM 写了 <final gen> 在末尾时, 用户看到空回复 + error
2395
+ if (after) {
2396
+ content = after;
3087
2397
  }
3088
- });
3089
- this.tools.set('wallet_get_balance', {
3090
- name: 'wallet_get_balance',
3091
- description: '查 EVM 钱包 ETH 余额. 默认 RPC = https://eth.llamarpc.com, 可指定其他 RPC URL',
3092
- parameters: { address: '0x 开头的 EVM 地址 (必填)', rpcUrl: '可选 RPC URL (默认 eth.llamarpc.com)' },
3093
- execute: async (args) => {
3094
- try {
3095
- const { getBalance } = await import('../constraint-runtime/dist/tools/WalletTools/getBalance.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/getBalance.js'));
3096
- const r = await getBalance({ address: String(args.address), rpcUrl: args.rpcUrl });
3097
- return { success: true, output: `💰 ${r.address}\n ${r.balanceEth} ${r.symbol} (${r.balance} wei)` };
3098
- }
3099
- catch (e) {
3100
- return { success: false, error: `查余额失败: ${String(e.message || e)}` };
3101
- }
3102
- }
3103
- });
3104
- this.tools.set('wallet_sign_message', {
3105
- name: 'wallet_sign_message',
3106
- description: '用私钥对消息做 EIP-191 personal_sign 签名',
3107
- parameters: { message: '要签名的消息 (必填)', privateKey: '0x 开头的私钥 (必填)' },
3108
- execute: async (args) => {
3109
- try {
3110
- const { signMessage } = await import('../constraint-runtime/dist/tools/WalletTools/signMessage.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/signMessage.js'));
3111
- const r = await signMessage({ message: String(args.message), privateKey: String(args.privateKey) });
3112
- return { success: true, output: `✅ 签名完成:\n address: ${r.address}\n message: ${r.message}\n signature: ${r.signature}` };
3113
- }
3114
- catch (e) {
3115
- return { success: false, error: `签名失败: ${String(e.message || e)}` };
3116
- }
3117
- }
3118
- });
3119
- this.tools.set('wallet_send_tx', {
3120
- name: 'wallet_send_tx',
3121
- description: '用 EVM 钱包发送交易 (转账 ETH 或调用合约). 走 EIP-1559, 默认 RPC = eth.llamarpc.com',
3122
- parameters: { privateKey: '私钥 (必填)', to: '接收地址 (必填)', value: '发送 wei 数量 (必填)', data: '可选 0x 开头的 calldata', rpcUrl: '可选 RPC URL' },
3123
- execute: async (args) => {
3124
- try {
3125
- const { sendTransaction } = await import('../constraint-runtime/dist/tools/WalletTools/sendTransaction.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/sendTransaction.js'));
3126
- const r = await sendTransaction({
3127
- privateKey: String(args.privateKey),
3128
- to: String(args.to),
3129
- value: String(args.value),
3130
- data: args.data ? String(args.data) : undefined,
3131
- rpcUrl: args.rpcUrl,
3132
- });
3133
- return { success: true, output: `✅ 交易已发送:\n hash: ${r.hash}\n from: ${r.from} → to: ${r.to}\n value: ${r.value} wei\n status: ${r.status}\n blockNumber: ${r.blockNumber || '(pending)'}\n gasUsed: ${r.gasUsed || '?'}` };
3134
- }
3135
- catch (e) {
3136
- return { success: false, error: `交易失败: ${String(e.message || e)}` };
3137
- }
3138
- }
3139
- });
3140
- this.tools.set('wallet_transfer_token', {
3141
- name: 'wallet_transfer_token',
3142
- description: '用 EVM 钱包转 ERC20 token (需要 token 合约地址)',
3143
- parameters: { privateKey: '私钥 (必填)', tokenAddress: 'ERC20 合约地址 (必填)', to: '接收地址 (必填)', amount: 'token 数量 (人类可读, 自动按 decimals 转换)', decimals: '可选 token decimals (默认查合约)', rpcUrl: '可选 RPC URL' },
3144
- execute: async (args) => {
3145
- try {
3146
- const { transferToken } = await import('../constraint-runtime/dist/tools/WalletTools/transferToken.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/transferToken.js'));
3147
- const r = await transferToken({
3148
- privateKey: String(args.privateKey),
3149
- tokenAddress: String(args.tokenAddress),
3150
- to: String(args.to),
3151
- amount: String(args.amount),
3152
- decimals: args.decimals ? Number(args.decimals) : undefined,
3153
- rpcUrl: args.rpcUrl,
3154
- });
3155
- return { success: true, output: `✅ Token 转账完成:\n hash: ${r.hash}\n ${r.amount} ${r.tokenAddress.substring(0, 10)}...\n from: ${r.from} → to: ${r.to}\n status: ${r.status}` };
3156
- }
3157
- catch (e) {
3158
- return { success: false, error: `转账失败: ${String(e.message || e)}` };
3159
- }
3160
- }
3161
- });
3162
- this.tools.set('wallet_autopay', {
3163
- name: 'wallet_autopay',
3164
- description: '设置自动付款 (订阅/定期扣款, 用 SafeSDK 自动执行). 实际能力取决于 SafeSDK autopay 实现',
3165
- parameters: { from: '付款方地址 (必填)', to: '收款方地址 (必填)', amount: '金额 (必填)', interval: '周期 (e.g. daily/weekly/monthly)', token: '可选, 默认 ETH' },
3166
- execute: async (args) => {
3167
- try {
3168
- const mod = await import('../constraint-runtime/dist/tools/WalletTools/autoPay.js').catch(() => import('../constraint-runtime/src/tools/WalletTools/autoPay.js'));
3169
- const fn = mod.autoPay || mod.setAutoPay || mod.default;
3170
- if (!fn)
3171
- return { success: false, error: 'autoPay 接口未找到' };
3172
- const r = await fn({
3173
- from: String(args.from), to: String(args.to), amount: String(args.amount),
3174
- interval: String(args.interval || 'monthly'), token: args.token,
3175
- });
3176
- return { success: true, output: `✅ 自动付款已设置: ${JSON.stringify(r)}` };
3177
- }
3178
- catch (e) {
3179
- return { success: false, error: `设置失败: ${String(e.message || e)}` };
3180
- }
3181
- }
3182
- });
3183
- // === PolymarketSDK (预测市场) ===
3184
- this.tools.set('polymarket_list_markets', {
3185
- name: 'polymarket_list_markets',
3186
- description: '列出 Polymarket 预测市场 (用 Polymarket CLOB API). limit 默认 50',
3187
- parameters: { limit: '可选 数量 (默认 50)', offset: '可选 偏移', closed: '可选 是否只显示已关闭 (默认 false = 只显示活跃)' },
3188
- execute: async (args) => {
3189
- try {
3190
- const { listMarkets } = await import('../constraint-runtime/dist/tools/PolymarketSDK/listMarkets.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/listMarkets.js'));
3191
- const markets = await listMarkets({
3192
- limit: args.limit ? Number(args.limit) : 50,
3193
- offset: args.offset ? Number(args.offset) : 0,
3194
- closed: args.closed ? String(args.closed) === 'true' : false,
3195
- });
3196
- if (!markets || markets.length === 0) {
3197
- return { success: true, output: '📊 Polymarket 当前没有活跃市场 (或 API 不可达, 因为 constraint-runtime 是占位实现, 需要装 polymarket-sdk 包)' };
3198
- }
3199
- const lines = markets.slice(0, 10).map((m, i) => ` ${i + 1}. [${m.id}] ${m.question}\n vol=${m.volume} active=${m.active}`);
3200
- return { success: true, output: `📊 Polymarket 找到 ${markets.length} 个市场 (前 10):\n${lines.join('\n')}` };
3201
- }
3202
- catch (e) {
3203
- return { success: false, error: `查询失败: ${String(e.message || e)}` };
3204
- }
3205
- }
3206
- });
3207
- this.tools.set('polymarket_get_market', {
3208
- name: 'polymarket_get_market',
3209
- description: '获取单个 Polymarket 市场的详情 (id/conditionId 都可)',
3210
- parameters: { marketId: '市场 ID (必填)' },
3211
- execute: async (args) => {
3212
- try {
3213
- const mod = await import('../constraint-runtime/dist/tools/PolymarketSDK/getMarket.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getMarket.js'));
3214
- const fn = mod.getMarket || mod.default;
3215
- const m = await fn(String(args.marketId));
3216
- if (!m)
3217
- return { success: false, error: '市场不存在' };
3218
- return { success: true, output: `📊 市场详情:\n ${JSON.stringify(m, null, 2).substring(0, 1500)}` };
3219
- }
3220
- catch (e) {
3221
- return { success: false, error: `查询失败: ${String(e.message || e)}` };
3222
- }
3223
- }
3224
- });
3225
- this.tools.set('polymarket_get_orders', {
3226
- name: 'polymarket_get_orders',
3227
- description: '查询 Polymarket 订单 (需要 CLOB client + auth, 当前 constraint-runtime 是占位实现)',
3228
- parameters: { marketId: '可选 按市场 ID 过滤' },
3229
- execute: async (args) => {
3230
- try {
3231
- const { getOrders } = await import('../constraint-runtime/dist/tools/PolymarketSDK/getOrders.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getOrders.js'));
3232
- const orders = await getOrders(args.marketId ? { marketId: String(args.marketId) } : {});
3233
- return { success: true, output: `📋 订单列表: ${JSON.stringify(orders, null, 2).substring(0, 1500)}` };
3234
- }
3235
- catch (e) {
3236
- return { success: false, error: `查询失败: ${String(e.message || e)}` };
3237
- }
3238
- }
3239
- });
3240
- this.tools.set('polymarket_create_order', {
3241
- name: 'polymarket_create_order',
3242
- description: '在 Polymarket 下单 (BUY/SELL, price 0-1, size USDC). 当前 constraint-runtime 是占位实现, 实际下单需要 CLOB client + 私钥签名',
3243
- parameters: { marketId: '市场 ID (必填)', side: 'BUY 或 SELL (必填)', price: '价格 0-1 (必填)', size: '数量 USDC (必填)' },
3244
- execute: async (args) => {
3245
- try {
3246
- const { createOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/createOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/createOrder.js'));
3247
- const r = await createOrder({
3248
- marketId: String(args.marketId),
3249
- side: String(args.side).toUpperCase() === 'SELL' ? 'SELL' : 'BUY',
3250
- price: Number(args.price),
3251
- size: Number(args.size),
3252
- });
3253
- if (r.success)
3254
- return { success: true, output: `✅ 订单: ${r.message}` };
3255
- return { success: false, error: r.message, output: r.message };
3256
- }
3257
- catch (e) {
3258
- return { success: false, error: `下单失败: ${String(e.message || e)}` };
3259
- }
3260
- }
3261
- });
3262
- this.tools.set('polymarket_cancel_order', {
3263
- name: 'polymarket_cancel_order',
3264
- description: '取消 Polymarket 订单 (order ID)',
3265
- parameters: { orderId: '订单 ID (必填)' },
3266
- execute: async (args) => {
3267
- try {
3268
- const { cancelOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/cancelOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js'));
3269
- const r = await cancelOrder({ orderId: String(args.orderId) });
3270
- if (r.success)
3271
- return { success: true, output: `✅ 取消订单: ${r.message}` };
3272
- return { success: false, error: r.message, output: r.message };
3273
- }
3274
- catch (e) {
3275
- return { success: false, error: `取消失败: ${String(e.message || e)}` };
3276
- }
3277
- }
3278
- });
3279
- // === SafeSDK (多签钱包) ===
3280
- this.tools.set('safe_deploy', {
3281
- name: 'safe_deploy',
3282
- description: '部署 Safe 多签钱包 (需要 owners/threshold 参数)',
3283
- parameters: { owners: 'JSON 数组 owner 地址 (必填)', threshold: '需要几个签名 (必填, e.g. 2)' },
3284
- execute: async (args) => {
3285
- try {
3286
- const { deploySafe } = await import('../constraint-runtime/dist/tools/SafeSDK/deploySafe.js').catch(() => import('../constraint-runtime/src/tools/SafeSDK/deploySafe.js'));
3287
- const owners = Array.isArray(args.owners) ? args.owners : JSON.parse(String(args.owners));
3288
- const r = await deploySafe({ owners, threshold: Number(args.threshold) });
3289
- return { success: true, output: `✅ Safe 部署: ${JSON.stringify(r)}` };
3290
- }
3291
- catch (e) {
3292
- return { success: false, error: `部署失败: ${String(e.message || e)}` };
3293
- }
2398
+ else {
2399
+ content = content.substring(0, markerIndex).trim();
3294
2400
  }
3295
- });
3296
- }
3297
- /**
3298
- * 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
3299
-
3300
- /**
3301
- * 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
3302
- * LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
3303
- * bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
3304
- * 返回 this.tools 里的标准名, 或 null 表示未识别
3305
- */
3306
- /**
3307
- * 把 LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
3308
- * bolloon 注册的标准工具名.
3309
- *
3310
- * 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
3311
- * 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
3312
- */
3313
- resolveToolName(name) {
3314
- return this._toolRegistry.resolve(name);
2401
+ }
2402
+ // 移除任何 tool call 标记
2403
+ let cleaned = content
2404
+ .replace(/调用工具[::]\s*\w+\s*\([^)]*\)/g, '')
2405
+ .replace(/使用工具[::]\s*\w+\s*\([^)]*\)/g, '')
2406
+ .replace(/tool[_\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
2407
+ .trim();
2408
+ return cleaned;
3315
2409
  }
3316
- /**
3317
- * 2026-06-19: 注册 P2P 消息 listener, 把收到的消息存到 _inboxMessages 供 check_inbox 读.
3318
- * 监听 type='message' (send_to_peer) 和 type='agent-message' (P2P 远端 agent).
3319
- * 失败静默, 不阻塞 PiAgentSession 构造.
3320
- */
3321
- _setupInboxListener() {
3322
- if (!p2pNetwork || typeof p2pNetwork.onMessage !== 'function')
3323
- return;
3324
- try {
3325
- // 监听所有 type 的消息, 存储到 inbox
3326
- p2pNetwork.onMessage('*', (msg, from, did) => {
3327
- try {
3328
- const text = new TextDecoder().decode(msg);
3329
- // 解析 DID:|type:payload 格式
3330
- let type = 'message';
3331
- let payload = text;
3332
- if (text.startsWith('DID:')) {
3333
- const pipeIdx = text.indexOf('|');
3334
- if (pipeIdx > 0) {
3335
- const rest = text.substring(pipeIdx + 1);
3336
- const colonIdx = rest.indexOf(':');
3337
- if (colonIdx > 0) {
3338
- type = rest.substring(0, colonIdx);
3339
- payload = rest.substring(colonIdx + 1);
3340
- }
3341
- else {
3342
- type = rest;
3343
- payload = '';
3344
- }
2410
+ parseToolCall(content) {
2411
+ const patterns = [
2412
+ /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
2413
+ /使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
2414
+ /tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
2415
+ /(\w+)\s*\(\s*([^)]*)\s*\)/,
2416
+ // 兼容 LLM 输出的对象字面量格式: {tool => "get_identity", args => {...}}
2417
+ /\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
2418
+ // 兼容: tool => "get_identity" (无 args 包裹)
2419
+ /\btool\s*=>\s*["'](\w+)["']/,
2420
+ // 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
2421
+ /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
2422
+ // 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
2423
+ // 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
2424
+ /<(\w+)>([\s\S]*?)<\/\1>/,
2425
+ ];
2426
+ for (const pattern of patterns) {
2427
+ const match = content.match(pattern);
2428
+ if (match) {
2429
+ const name = match[1];
2430
+ let args = {};
2431
+ const rawArgs = match[2] || '';
2432
+ if (rawArgs && rawArgs.trim().startsWith('{')) {
2433
+ // JSON 形式, 尝试解析
2434
+ try {
2435
+ const parsed = JSON.parse(rawArgs);
2436
+ if (parsed && typeof parsed === 'object') {
2437
+ args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
3345
2438
  }
3346
2439
  }
3347
- else {
3348
- const colonIdx = text.indexOf(':');
3349
- if (colonIdx > 0) {
3350
- type = text.substring(0, colonIdx);
3351
- payload = text.substring(colonIdx + 1);
2440
+ catch {
2441
+ // 解析失败就当字符串处理
2442
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
2443
+ for (const pair of argPairs) {
2444
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
2445
+ if (key)
2446
+ args[key] = valueParts.join(':') || '';
3352
2447
  }
3353
2448
  }
3354
- this._inboxMessages.push({
3355
- id: `p2p-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
3356
- from: from.substring(0, 32),
3357
- fromDid: did,
3358
- type,
3359
- payload,
3360
- timestamp: Date.now(),
3361
- source: 'p2p',
3362
- });
3363
- // 限制 inbox 大小, 防止内存泄漏
3364
- if (this._inboxMessages.length > 1000) {
3365
- this._inboxMessages = this._inboxMessages.slice(-1000);
2449
+ }
2450
+ else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
2451
+ // 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
2452
+ // 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
2453
+ const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
2454
+ let xmlMatch;
2455
+ while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
2456
+ const argName = xmlMatch[1];
2457
+ const argValue = xmlMatch[2].trim();
2458
+ if (argName && argValue) {
2459
+ args[argName] = argValue;
2460
+ }
3366
2461
  }
3367
2462
  }
3368
- catch (err) {
3369
- console.warn('[PiAgent] inbox listener decode error:', err);
2463
+ else if (rawArgs) {
2464
+ // 形参串, 形如 key: value, key2: value2
2465
+ const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
2466
+ for (const pair of argPairs) {
2467
+ const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
2468
+ if (key)
2469
+ args[key] = valueParts.join(':') || '';
2470
+ }
3370
2471
  }
3371
- });
3372
- }
3373
- catch (err) {
3374
- console.warn('[PiAgent] _setupInboxListener failed (non-fatal):', err);
3375
- }
3376
- // 监听本地 inbox bus 投递 (send_to_local_agent)
3377
- try {
3378
- const { LocalInboxBus } = require('../network/local-inbox-bus.js');
3379
- const myRole = process.env.BOLLOON_ROLE || 'default';
3380
- LocalInboxBus.getInstance().subscribe(myRole, (msg) => {
3381
- this._inboxMessages.push({
3382
- id: `local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
3383
- from: msg.from || 'unknown',
3384
- fromDid: msg.fromDid,
3385
- type: msg.type || 'agent-local-message',
3386
- payload: msg.payload || '',
3387
- timestamp: msg.timestamp || Date.now(),
3388
- source: 'local',
3389
- });
3390
- if (this._inboxMessages.length > 1000) {
3391
- this._inboxMessages = this._inboxMessages.slice(-1000);
2472
+ if (this.tools.has(name) || this.tools.has(name.replace(/_/g, '_'))) {
2473
+ return { name, args };
3392
2474
  }
3393
- });
3394
- }
3395
- catch (err) {
3396
- // LocalInboxBus 可能还没创建, 不阻塞
2475
+ }
3397
2476
  }
2477
+ return null;
3398
2478
  }
3399
2479
  estimateResponseQuality(response) {
3400
2480
  let score = 0.5;
@@ -3485,37 +2565,37 @@ Workspace root folder: ${this.cwd}
3485
2565
  getDefaultResponse(input) {
3486
2566
  const operationsRef = PiAgentSession.getOperationsReference();
3487
2567
  if (operationsRef) {
3488
- return `收到了: "${input}"
3489
-
3490
- 我是一个判断力处理智能体,支持自然语言交互。
3491
-
3492
- 可用操作(直接说出即可):
3493
- ${this.extractOperationsFromRef(operationsRef)}
3494
-
3495
- 示例请求:
3496
- - "读取 src/index.ts 文件"
3497
- - "总结一下 README.md"
3498
- - "查看当前连接了哪些节点"
2568
+ return `收到了: "${input}"
2569
+
2570
+ 我是一个判断力处理智能体,支持自然语言交互。
2571
+
2572
+ 可用操作(直接说出即可):
2573
+ ${this.extractOperationsFromRef(operationsRef)}
2574
+
2575
+ 示例请求:
2576
+ - "读取 src/index.ts 文件"
2577
+ - "总结一下 README.md"
2578
+ - "查看当前连接了哪些节点"
3499
2579
  - "向 QmABC... 发送测试消息"`;
3500
2580
  }
3501
- return `收到了: "${input}"
3502
-
3503
- 我是一个判断力处理智能体,支持自然语言交互。
3504
-
3505
- 可用操作(直接说出即可):
3506
- - "读取 README.md" - 读取并分析文档
3507
- - "总结文档" - 总结文档内容
3508
- - "改进文档,按照X要求" - 改进文档
3509
- - "查看节点" - 查看已连接的对等节点
3510
- - "向X发送消息Y" - 向对等节点发送消息
3511
- - "广播消息X" - 广播消息到所有节点
3512
- - "查看身份" - 查看当前智能体身份
3513
- - "查看日志" - 查看最近操作日志
3514
-
3515
- 示例请求:
3516
- - "读取 src/index.ts 文件"
3517
- - "总结一下 README.md"
3518
- - "查看当前连接了哪些节点"
2581
+ return `收到了: "${input}"
2582
+
2583
+ 我是一个判断力处理智能体,支持自然语言交互。
2584
+
2585
+ 可用操作(直接说出即可):
2586
+ - "读取 README.md" - 读取并分析文档
2587
+ - "总结文档" - 总结文档内容
2588
+ - "改进文档,按照X要求" - 改进文档
2589
+ - "查看节点" - 查看已连接的对等节点
2590
+ - "向X发送消息Y" - 向对等节点发送消息
2591
+ - "广播消息X" - 广播消息到所有节点
2592
+ - "查看身份" - 查看当前智能体身份
2593
+ - "查看日志" - 查看最近操作日志
2594
+
2595
+ 示例请求:
2596
+ - "读取 src/index.ts 文件"
2597
+ - "总结一下 README.md"
2598
+ - "查看当前连接了哪些节点"
3519
2599
  - "向 QmABC... 发送测试消息"`;
3520
2600
  }
3521
2601
  extractOperationsFromRef(ref) {
@@ -3540,13 +2620,13 @@ ${this.extractOperationsFromRef(operationsRef)}
3540
2620
  }
3541
2621
  }
3542
2622
  return operationLines.length > 0 ? operationLines.join('\n') :
3543
- ` - "读取 README.md" - 读取并分析文档
3544
- - "总结文档" - 总结文档内容
3545
- - "改进文档,按照X要求" - 改进文档
3546
- - "查看节点" - 查看已连接的对等节点
3547
- - "向X发送消息Y" - 向对等节点发送消息
3548
- - "广播消息X" - 广播消息到所有节点
3549
- - "查看身份" - 查看当前智能体身份
2623
+ ` - "读取 README.md" - 读取并分析文档
2624
+ - "总结文档" - 总结文档内容
2625
+ - "改进文档,按照X要求" - 改进文档
2626
+ - "查看节点" - 查看已连接的对等节点
2627
+ - "向X发送消息Y" - 向对等节点发送消息
2628
+ - "广播消息X" - 广播消息到所有节点
2629
+ - "查看身份" - 查看当前智能体身份
3550
2630
  - "查看日志" - 查看最近操作日志`;
3551
2631
  }
3552
2632
  async suggestRename(messages) {
@@ -3886,14 +2966,11 @@ export async function createAgentSession(config, forceNew) {
3886
2966
  const key = config.peerId;
3887
2967
  if (!forceNew && independentSessions.has(key)) {
3888
2968
  console.log(`[createAgentSession] 找到现有独立 session, key=${key}`);
3889
- const existing = independentSessions.get(key);
3890
- await existing.whenReady();
3891
- return existing;
2969
+ return independentSessions.get(key);
3892
2970
  }
3893
2971
  const session = new PiAgentSession(config);
3894
2972
  independentSessions.set(key, session);
3895
2973
  console.log(`[createAgentSession] 创建独立 session, key=${key}, DID=${incomingDid}`);
3896
- await session.whenReady();
3897
2974
  return session;
3898
2975
  }
3899
2976
  // 如果指定了 forceNew 但没有 peerId,生成带时间戳的 key
@@ -3902,7 +2979,6 @@ export async function createAgentSession(config, forceNew) {
3902
2979
  const session = new PiAgentSession(config);
3903
2980
  independentSessions.set(key, session);
3904
2981
  console.log(`[createAgentSession] 创建强制新 session, key=${key}`);
3905
- await session.whenReady();
3906
2982
  return session;
3907
2983
  }
3908
2984
  // 如果有新的 DID,强制重建 session
@@ -3922,13 +2998,11 @@ export async function createAgentSession(config, forceNew) {
3922
2998
  createdAt: Date.now()
3923
2999
  });
3924
3000
  }
3925
- await sessionInstance.whenReady();
3926
3001
  return sessionInstance;
3927
3002
  }
3928
3003
  sessionInstance = new PiAgentSession(config);
3929
3004
  lastIdentityDid = config.identityDoc?.did || null;
3930
3005
  console.log(`[createAgentSession] 新建 session, DID=${lastIdentityDid}`);
3931
- await sessionInstance.whenReady();
3932
3006
  return sessionInstance;
3933
3007
  }
3934
3008
  export function getAgentSession() {