@bolloon/bolloon-agent 0.2.12 → 0.2.13

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 (284) hide show
  1. package/README.md +495 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon-daemon.sh +207 -0
  4. package/bin/bolloon.cjs +0 -0
  5. package/bin/ipfs +0 -0
  6. package/dist/agents/agent-manifest-protocol.js +52 -0
  7. package/dist/agents/chat-segmenter.js +298 -0
  8. package/dist/agents/constraint-layer.js +19 -19
  9. package/dist/agents/judgment-protocol.js +479 -0
  10. package/dist/agents/parse-tool-call.js +304 -0
  11. package/dist/agents/peer-manifest-loader.js +210 -0
  12. package/dist/agents/pi-sdk-session-factory.js +110 -0
  13. package/dist/agents/pi-sdk-session-manager.js +297 -0
  14. package/dist/agents/pi-sdk-tools.js +1288 -0
  15. package/dist/agents/pi-sdk-types.js +13 -0
  16. package/dist/agents/pi-sdk.js +553 -1306
  17. package/dist/agents/react-loop.js +120 -0
  18. package/dist/agents/session-store.js +171 -0
  19. package/dist/agents/shell-guard.js +2 -2
  20. package/dist/agents/tool-registry.js +136 -0
  21. package/dist/agents/workflow-pivot-loop.js +200 -22
  22. package/dist/bollharness/src/scripts/context_router.js +7 -0
  23. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  24. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  25. package/dist/bollharness-integration/context-router.js +292 -292
  26. package/dist/bollharness-integration/gate-state-machine.js +26 -13
  27. package/dist/bollharness-integration/integration.js +71 -0
  28. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  29. package/dist/bollharness-integration/skill-adapter.js +57 -127
  30. package/dist/bootstrap/chat-archiver.js +276 -0
  31. package/dist/bootstrap/context-collector.js +6 -3
  32. package/dist/bootstrap/event-log.js +160 -0
  33. package/dist/bootstrap/lifecycle-hooks.js +15 -1
  34. package/dist/bootstrap/memory-compressor.js +170 -0
  35. package/dist/bootstrap/persona-loader.js +94 -0
  36. package/dist/bootstrap/project-state.js +159 -0
  37. package/dist/bootstrap/remote-mirror.js +97 -0
  38. package/dist/bootstrap/session-window.js +121 -0
  39. package/dist/bootstrap/vector-index.js +248 -0
  40. package/dist/cli/loading-tui.js +40 -0
  41. package/dist/cli-entry.js +41 -37
  42. package/dist/context-compaction/auto-compact.js +7 -7
  43. package/dist/electron/config.js +21 -0
  44. package/dist/electron/config.js.map +1 -0
  45. package/dist/electron/dialogs.js +108 -0
  46. package/dist/electron/dialogs.js.map +1 -0
  47. package/dist/electron/first-run.js +170 -0
  48. package/dist/electron/first-run.js.map +1 -0
  49. package/dist/electron/ipc.js +20 -0
  50. package/dist/electron/ipc.js.map +1 -0
  51. package/dist/electron/logger.js +114 -0
  52. package/dist/electron/logger.js.map +1 -0
  53. package/dist/electron/main.js +63 -0
  54. package/dist/electron/main.js.map +1 -0
  55. package/dist/electron/menu.js +145 -0
  56. package/dist/electron/menu.js.map +1 -0
  57. package/dist/electron/paths.js +75 -0
  58. package/dist/electron/paths.js.map +1 -0
  59. package/dist/electron/server.js +119 -0
  60. package/dist/electron/server.js.map +1 -0
  61. package/dist/electron/tray.js +111 -0
  62. package/dist/electron/tray.js.map +1 -0
  63. package/dist/electron/window.js +111 -0
  64. package/dist/electron/window.js.map +1 -0
  65. package/dist/electron-preload.js +26 -6
  66. package/dist/electron-preload.js.map +1 -1
  67. package/dist/electron.js +6 -168
  68. package/dist/electron.js.map +1 -1
  69. package/dist/index.js +323 -266
  70. package/dist/llm/config-store.js +24 -8
  71. package/dist/llm/llm-judgment-client.js +102 -102
  72. package/dist/llm/pi-ai.js +137 -85
  73. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  74. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  75. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  76. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  77. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  78. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  79. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  80. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  81. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  82. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  83. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  84. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  85. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  86. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  87. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  88. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  89. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  90. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  91. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  92. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  93. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  94. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  95. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  96. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  97. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  98. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  99. package/dist/llm/tool-manifest/bash.js +3 -3
  100. package/dist/llm/tool-manifest/create_file.js +4 -4
  101. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  102. package/dist/llm/tool-manifest/image_search.js +2 -2
  103. package/dist/llm/tool-manifest/mcp.js +2 -2
  104. package/dist/llm/tool-manifest/message_compose.js +3 -3
  105. package/dist/llm/tool-manifest/places.js +2 -2
  106. package/dist/llm/tool-manifest/present_files.js +1 -1
  107. package/dist/llm/tool-manifest/recipe.js +2 -2
  108. package/dist/llm/tool-manifest/str_replace.js +5 -5
  109. package/dist/llm/tool-manifest/view.js +3 -3
  110. package/dist/llm/tool-manifest/weather.js +4 -4
  111. package/dist/llm/tool-manifest/web.js +4 -4
  112. package/dist/network/local-inbox-bus.js +73 -0
  113. package/dist/network/p2p-outbox.js +161 -0
  114. package/dist/network/peer-fs.js +420 -0
  115. package/dist/network/peer-resource-bridge.js +216 -0
  116. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  117. package/dist/pi-ecosystem-judgment/cleanup.js +126 -0
  118. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  119. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  120. package/dist/pi-ecosystem-judgment/human-value-store.js +45 -11
  121. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  122. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  123. package/dist/security/context-router-tool.js +15 -15
  124. package/dist/security/tool-gate.js +11 -0
  125. package/dist/web/api-config.html +520 -520
  126. package/dist/web/client-loop-status.js +220 -0
  127. package/dist/web/client.js +22 -5
  128. package/dist/web/client.js.map +7 -0
  129. package/dist/web/components/wallet-viem.mjs +118 -118
  130. package/dist/web/index.html +375 -375
  131. package/dist/web/input-validator.js +103 -0
  132. package/dist/web/manifest.json +20 -20
  133. package/dist/web/routes-judgments.js +738 -0
  134. package/dist/web/routes-llm-config.js +279 -0
  135. package/dist/web/routes-tasks.js +222 -0
  136. package/dist/web/server-sse.js +121 -0
  137. package/dist/web/server-storage.js +154 -0
  138. package/dist/web/server-types.js +15 -0
  139. package/dist/web/server-v3-p2p.js +224 -0
  140. package/dist/web/server.js +1070 -1307
  141. package/dist/web/style.css +4742 -4742
  142. package/dist/web/util/safe-name.js +32 -0
  143. package/package.json +162 -160
  144. package/scripts/build-cli.js +215 -215
  145. package/scripts/build-web.ts +130 -130
  146. package/scripts/postinstall.js +152 -152
  147. package/bin/bolloon.js +0 -157
  148. package/dist/constraint-runtime/src/_archive_helper.js +0 -9
  149. package/dist/constraint-runtime/src/agent/coordinator.js +0 -48
  150. package/dist/constraint-runtime/src/agent/index.js +0 -1
  151. package/dist/constraint-runtime/src/assistant/index.js +0 -12
  152. package/dist/constraint-runtime/src/bootstrap/index.js +0 -12
  153. package/dist/constraint-runtime/src/bootstrap_graph.js +0 -13
  154. package/dist/constraint-runtime/src/bridge/index.js +0 -12
  155. package/dist/constraint-runtime/src/buddy/index.js +0 -12
  156. package/dist/constraint-runtime/src/cli/index.js +0 -12
  157. package/dist/constraint-runtime/src/command_graph.js +0 -10
  158. package/dist/constraint-runtime/src/commands.js +0 -60
  159. package/dist/constraint-runtime/src/components/index.js +0 -12
  160. package/dist/constraint-runtime/src/constants/index.js +0 -12
  161. package/dist/constraint-runtime/src/constraint/budget.js +0 -22
  162. package/dist/constraint-runtime/src/constraint/index.js +0 -2
  163. package/dist/constraint-runtime/src/constraint/permission.js +0 -20
  164. package/dist/constraint-runtime/src/context.js +0 -30
  165. package/dist/constraint-runtime/src/coordinator/index.js +0 -12
  166. package/dist/constraint-runtime/src/cost_hook.js +0 -4
  167. package/dist/constraint-runtime/src/cost_tracker.js +0 -8
  168. package/dist/constraint-runtime/src/deferred_init.js +0 -10
  169. package/dist/constraint-runtime/src/direct_modes.js +0 -6
  170. package/dist/constraint-runtime/src/dynamic-tool-loader.js +0 -85
  171. package/dist/constraint-runtime/src/entrypoints/index.js +0 -12
  172. package/dist/constraint-runtime/src/execution_registry.js +0 -44
  173. package/dist/constraint-runtime/src/history.js +0 -9
  174. package/dist/constraint-runtime/src/hooks/index.js +0 -12
  175. package/dist/constraint-runtime/src/index.js +0 -26
  176. package/dist/constraint-runtime/src/ink.js +0 -4
  177. package/dist/constraint-runtime/src/keybindings/index.js +0 -12
  178. package/dist/constraint-runtime/src/memdir/index.js +0 -12
  179. package/dist/constraint-runtime/src/migrations/index.js +0 -12
  180. package/dist/constraint-runtime/src/models.js +0 -1
  181. package/dist/constraint-runtime/src/moreright/index.js +0 -12
  182. package/dist/constraint-runtime/src/native_ts/index.js +0 -12
  183. package/dist/constraint-runtime/src/output_styles/index.js +0 -12
  184. package/dist/constraint-runtime/src/parity_audit.js +0 -12
  185. package/dist/constraint-runtime/src/plugins/index.js +0 -12
  186. package/dist/constraint-runtime/src/port_manifest.js +0 -11
  187. package/dist/constraint-runtime/src/prefetch.js +0 -9
  188. package/dist/constraint-runtime/src/query.js +0 -1
  189. package/dist/constraint-runtime/src/remote/index.js +0 -12
  190. package/dist/constraint-runtime/src/remote_runtime.js +0 -9
  191. package/dist/constraint-runtime/src/runtime/index.js +0 -1
  192. package/dist/constraint-runtime/src/runtime/session.js +0 -35
  193. package/dist/constraint-runtime/src/schemas/index.js +0 -12
  194. package/dist/constraint-runtime/src/screens/index.js +0 -12
  195. package/dist/constraint-runtime/src/server/index.js +0 -12
  196. package/dist/constraint-runtime/src/services/index.js +0 -12
  197. package/dist/constraint-runtime/src/session_store.js +0 -22
  198. package/dist/constraint-runtime/src/setup.js +0 -30
  199. package/dist/constraint-runtime/src/skills/index.js +0 -1
  200. package/dist/constraint-runtime/src/skills/skill-registry.js +0 -28
  201. package/dist/constraint-runtime/src/state/index.js +0 -12
  202. package/dist/constraint-runtime/src/system_init.js +0 -20
  203. package/dist/constraint-runtime/src/thinking/engine.js +0 -42
  204. package/dist/constraint-runtime/src/thinking/index.js +0 -1
  205. package/dist/constraint-runtime/src/tool_pool.js +0 -8
  206. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +0 -7
  207. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +0 -7
  208. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +0 -7
  209. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +0 -6
  210. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +0 -8
  211. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +0 -6
  212. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +0 -6
  213. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +0 -6
  214. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +0 -6
  215. package/dist/constraint-runtime/src/tools.js +0 -80
  216. package/dist/constraint-runtime/src/transcript.js +0 -19
  217. package/dist/constraint-runtime/src/types/index.js +0 -12
  218. package/dist/constraint-runtime/src/upstream_proxy/index.js +0 -12
  219. package/dist/constraint-runtime/src/utils/index.js +0 -12
  220. package/dist/constraint-runtime/src/vim/index.js +0 -12
  221. package/dist/constraint-runtime/src/voice/index.js +0 -12
  222. package/dist/constraint-runtime/tests/agent.test.js +0 -16
  223. package/dist/constraint-runtime/tests/constraint.test.js +0 -41
  224. package/dist/constraint-runtime/tests/skill.test.js +0 -19
  225. package/dist/constraint-runtime/tests/thinking.test.js +0 -22
  226. package/dist/pi-ecosystem-colony/index.js +0 -365
  227. package/dist/social/ant-colony/AdaptiveHeartbeat.js +0 -101
  228. package/dist/social/ant-colony/PheromoneEngine.js +0 -227
  229. package/dist/social/ant-colony/index.js +0 -6
  230. package/dist/social/ant-colony/types.js +0 -24
  231. package/dist/test/ai-judgment-test.js +0 -80
  232. package/dist/test/bollharness-integration.test.js +0 -318
  233. package/dist/test/channel-agent-multi-dialogue.js +0 -205
  234. package/dist/test/channel-heartbeat-agent-test.js +0 -201
  235. package/dist/test/constraint-layer.test.js +0 -164
  236. package/dist/test/diap-identity-test.js +0 -172
  237. package/dist/test/diap-quick-test.js +0 -62
  238. package/dist/test/global-shared-context.test.js +0 -315
  239. package/dist/test/harness-judgment-injection.test.js +0 -246
  240. package/dist/test/harness-workflow-integrator-test.js +0 -228
  241. package/dist/test/human-value-store.test.js +0 -243
  242. package/dist/test/hybrid-integration-test.js +0 -118
  243. package/dist/test/hybrid-messenger-verify.js +0 -55
  244. package/dist/test/iroh-bistream-debug.js +0 -38
  245. package/dist/test/iroh-communication.test.js +0 -66
  246. package/dist/test/iroh-debug-test.js +0 -57
  247. package/dist/test/iroh-diap-test.js +0 -71
  248. package/dist/test/iroh-direct-connect.js +0 -55
  249. package/dist/test/iroh-e2e-fixed.js +0 -89
  250. package/dist/test/iroh-e2e-same-process.js +0 -63
  251. package/dist/test/iroh-e2e.js +0 -66
  252. package/dist/test/iroh-final-e2e.js +0 -72
  253. package/dist/test/iroh-relay-test.js +0 -37
  254. package/dist/test/iroh-simple-test.js +0 -41
  255. package/dist/test/iroh-transport-verify.js +0 -54
  256. package/dist/test/iroh-transport.test.js +0 -37
  257. package/dist/test/iroh-two-nodes.js +0 -70
  258. package/dist/test/iroh-verify.js +0 -44
  259. package/dist/test/judgment-decision.test.js +0 -219
  260. package/dist/test/llm-judgment-integration.test.js +0 -220
  261. package/dist/test/p2p-agent-complex-dialogue.js +0 -385
  262. package/dist/test/p2p-agent-dialogue.js +0 -341
  263. package/dist/test/p2p-agent-full-bidirectional.js +0 -510
  264. package/dist/test/p2p-agent-harness-flow.js +0 -437
  265. package/dist/test/p2p-agent-harness-single.js +0 -143
  266. package/dist/test/p2p-ai-dialogue-test.js +0 -318
  267. package/dist/test/p2p-cid-connect-test.js +0 -195
  268. package/dist/test/p2p-connect-receiver.js +0 -69
  269. package/dist/test/p2p-doc-transfer.js +0 -110
  270. package/dist/test/p2p-identity-page-test.js +0 -77
  271. package/dist/test/p2p-iroh-test.js +0 -171
  272. package/dist/test/p2p-minimal-test.js +0 -241
  273. package/dist/test/p2p-node-1.js +0 -148
  274. package/dist/test/p2p-node-2.js +0 -148
  275. package/dist/test/p2p-server.js +0 -281
  276. package/dist/test/p2p-two-nodes-test.js +0 -438
  277. package/dist/test/pi-sdk.test.js +0 -44
  278. package/dist/test/set-persona.js +0 -40
  279. package/dist/test/simple.test.js +0 -9
  280. package/dist/test/storage-integration.test.js +0 -150
  281. package/dist/test/subagent-manager.test.js +0 -276
  282. package/dist/test/test-gate-flow.test.js +0 -81
  283. package/dist/test/workflow-engine.test.js +0 -87
  284. package/dist/test/workflow-pivot-loop.test.js +0 -246
@@ -1,8 +1,14 @@
1
1
  /**
2
2
  * Pi-SDK - Agent Session for Document Processing
3
3
  * Part of OpenClaw dual-layer architecture
4
+ *
5
+ * 模块拆分 (2026-07-06):
6
+ * - types → ./pi-sdk-types.ts (interface / type)
7
+ * - session mgr → ./pi-sdk-session-manager.ts (PiSessionManager 类)
8
+ * - tools → ./pi-sdk-tools.ts (registerBuiltinTools / Wallet / IdempotencyCache)
9
+ * - factory → ./pi-sdk-session-factory.ts (createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop)
10
+ * - 本文件 (PiAgentSession 类: LLM 循环 / 系统提示 / 工具调用分发 / 压缩 / persistence)
4
11
  */
5
- import * as fs from 'fs/promises';
6
12
  import * as fsSync from 'fs';
7
13
  import * as os from 'os';
8
14
  import * as path from 'path';
@@ -13,13 +19,16 @@ import { ConstraintLayer } from './constraint-layer.js';
13
19
  import { WorkflowEngine } from './workflow-engine.js';
14
20
  import { DeepThinkingEngine, AgentCoordinator } from '@bolloon/constraint-runtime';
15
21
  import { WorkflowPivotLoop, createDefaultPivotConfig } from './workflow-pivot-loop.js';
16
- import { p2pDocumentTools, initDocumentReceiver } from './p2p-document-tools.js';
17
- import { shellExec } from './shell-tool.js';
18
- import { getBranchPrefix, getCooldownMs, checkWritePath } from './shell-guard.js';
22
+ import { initDocumentReceiver } from './p2p-document-tools.js';
19
23
  import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartbeat.js';
20
- import { getGlobalSharedContext } from '../social/global-shared-context.js';
21
- import { Session, SkillRegistry, saveSession, loadSession } from '@bolloon/constraint-runtime';
22
- import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
24
+ import { SkillRegistry } from '@bolloon/constraint-runtime';
25
+ import { loadSkillsFromPaths, defaultSkillPaths } from './skill-loader.js';
26
+ // 拆分后的子模块 重新导出保 backward compat
27
+ export { TOOL_DEFINITIONS, } from './pi-sdk-types.js';
28
+ export { PiSessionManager } from './pi-sdk-session-manager.js';
29
+ import { PiSessionManager } from './pi-sdk-session-manager.js';
30
+ import { registerBuiltinTools, registerWalletTools, setupInboxListener, IdempotencyCache, } from './pi-sdk-tools.js';
31
+ export { createAgentSession, getAgentSession, resetAgentSession, runSelfImproveLoop, } from './pi-sdk-session-factory.js';
23
32
  // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
24
33
  // 失败静默, 不阻塞主对话
25
34
  import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
@@ -31,306 +40,14 @@ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
31
40
  import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
32
41
  // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
33
42
  import { ReactHarness } from '../security/react-harness.js';
34
- const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
35
- const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
36
- export class PiSessionManager {
37
- session;
38
- state;
39
- memory;
40
- persona = null;
41
- channels = new Map();
42
- channelsPath;
43
- initialized = false;
44
- sessionDir;
45
- cwd;
46
- sharedContext;
47
- agentId;
48
- constructor(agentId, cwd) {
49
- this.cwd = cwd;
50
- this.sessionDir = path.join(cwd, '.port_sessions');
51
- this.agentId = agentId;
52
- const sessionId = `pi-session-${Date.now()}`;
53
- this.session = new Session(sessionId);
54
- this.state = {
55
- id: sessionId,
56
- agentId,
57
- cwd,
58
- startedAt: new Date().toISOString(),
59
- lastActive: new Date().toISOString()
60
- };
61
- this.memory = {
62
- workingMemory: [],
63
- summarizedMemory: [],
64
- fileContext: new Map()
65
- };
66
- this.channelsPath = path.join(SHARED_SESSION_PATH, 'pi-channels.json');
67
- this.sharedContext = getGlobalSharedContext();
68
- }
69
- get sessionId() {
70
- return this.session.sessionId;
71
- }
72
- get turnCount() {
73
- return this.session.turnCount;
74
- }
75
- addSessionMessage(msg) {
76
- this.session.addMessage(msg);
77
- this.persistSession();
78
- }
79
- getSessionHistory() {
80
- return this.session.history;
81
- }
82
- setSessionContext(key, value) {
83
- this.session.setContext(key, value);
84
- this.persistSession();
85
- }
86
- getSessionContext(key) {
87
- return this.session.getContext(key);
88
- }
89
- persistSession() {
90
- try {
91
- const stored = {
92
- sessionId: this.session.sessionId,
93
- messages: this.session.history,
94
- inputTokens: 0,
95
- outputTokens: 0
96
- };
97
- saveSession(stored);
98
- }
99
- catch (e) {
100
- console.warn('Failed to persist session:', e);
101
- }
102
- }
103
- loadPersistedSession() {
104
- try {
105
- const sessionId = this.state.id;
106
- const stored = loadSession(sessionId);
107
- for (const msg of stored.messages) {
108
- this.session.addMessage(msg);
109
- }
110
- }
111
- catch {
112
- // No persisted session found, start fresh
113
- }
114
- }
115
- async initialize() {
116
- if (this.initialized)
117
- return;
118
- await fs.mkdir(SHARED_SESSION_PATH, { recursive: true });
119
- await fs.mkdir(this.sessionDir, { recursive: true });
120
- this.persona = await this.loadPersona();
121
- await this.loadChannels();
122
- this.loadPersistedSession();
123
- await this.sharedContext.initialize();
124
- await this.sharedContext.registerAgent({
125
- agentId: this.agentId,
126
- sessionId: this.sessionId,
127
- channelId: 'system',
128
- capabilities: this.persona?.capabilities || [],
129
- status: 'active',
130
- name: this.persona?.name,
131
- persona: this.persona ? {
132
- name: this.persona.name,
133
- description: this.persona.description,
134
- capabilities: this.persona.capabilities
135
- } : undefined
136
- });
137
- this.initialized = true;
138
- }
139
- async loadPersona() {
140
- try {
141
- const data = await fs.readFile(PERSONA_PATH, 'utf-8');
142
- return JSON.parse(data);
143
- }
144
- catch {
145
- return null;
146
- }
147
- }
148
- async loadChannels() {
149
- try {
150
- const data = await fs.readFile(this.channelsPath, 'utf-8');
151
- const channelsArray = JSON.parse(data);
152
- this.channels.clear();
153
- for (const channel of channelsArray) {
154
- this.channels.set(channel.id, channel);
155
- }
156
- }
157
- catch {
158
- this.channels.clear();
159
- }
160
- }
161
- async saveChannels() {
162
- const channelsArray = Array.from(this.channels.values());
163
- await fs.writeFile(this.channelsPath, JSON.stringify(channelsArray, null, 2));
164
- }
165
- async savePersona(persona) {
166
- await fs.writeFile(PERSONA_PATH, JSON.stringify(persona, null, 2));
167
- this.persona = persona;
168
- }
169
- getPersona() {
170
- return this.persona;
171
- }
172
- getState() {
173
- return { ...this.state, lastActive: new Date().toISOString() };
174
- }
175
- getMemory() {
176
- return this.memory;
177
- }
178
- addToWorkingMemory(content) {
179
- this.memory.workingMemory.push(content);
180
- if (this.memory.workingMemory.length > 100) {
181
- this.memory.workingMemory = this.memory.workingMemory.slice(-100);
182
- }
183
- this.state.lastActive = new Date().toISOString();
184
- }
185
- addSummarizedMemory(content) {
186
- this.memory.summarizedMemory.push(content);
187
- if (this.memory.summarizedMemory.length > 50) {
188
- this.memory.summarizedMemory = this.memory.summarizedMemory.slice(-50);
189
- }
190
- }
191
- addFileContext(filePath, content) {
192
- this.memory.fileContext.set(filePath, content);
193
- if (this.memory.fileContext.size > 20) {
194
- const entries = Array.from(this.memory.fileContext.entries());
195
- this.memory.fileContext = new Map(entries.slice(-20));
196
- }
197
- }
198
- updateTokenUsage(promptTokens, completionTokens) {
199
- this.state.tokenUsage = {
200
- promptTokens: (this.state.tokenUsage?.promptTokens || 0) + promptTokens,
201
- completionTokens: (this.state.tokenUsage?.completionTokens || 0) + completionTokens,
202
- totalTokens: (this.state.tokenUsage?.totalTokens || 0) + promptTokens + completionTokens
203
- };
204
- }
205
- async addMessage(channelId, message) {
206
- await this.initialize();
207
- if (!this.channels.has(channelId)) {
208
- this.channels.set(channelId, {
209
- id: channelId,
210
- name: channelId,
211
- messages: [],
212
- createdAt: new Date().toISOString(),
213
- updatedAt: new Date().toISOString()
214
- });
215
- }
216
- const channel = this.channels.get(channelId);
217
- channel.messages.push(message);
218
- channel.updatedAt = new Date().toISOString();
219
- await this.saveChannels();
220
- }
221
- async getChannelMessages(channelId) {
222
- await this.initialize();
223
- return this.channels.get(channelId)?.messages || [];
224
- }
225
- async createChannel(name, peerInfo, persona) {
226
- await this.initialize();
227
- const channelId = `ch_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
228
- const channel = {
229
- id: channelId,
230
- name,
231
- messages: [],
232
- createdAt: new Date().toISOString(),
233
- updatedAt: new Date().toISOString(),
234
- ...peerInfo,
235
- persona: persona || undefined
236
- };
237
- this.channels.set(channelId, channel);
238
- await this.saveChannels();
239
- return channel;
240
- }
241
- async getOrCreatePeerChannel(peerDid, peerName, persona) {
242
- await this.initialize();
243
- for (const channel of this.channels.values()) {
244
- if (channel.peerDid === peerDid) {
245
- return channel;
246
- }
247
- }
248
- return this.createChannel(`与 ${peerName} 的对话`, {
249
- peerDid,
250
- peerName
251
- }, persona);
252
- }
253
- async setChannelInfo(channelId, info) {
254
- await this.initialize();
255
- const channel = this.channels.get(channelId);
256
- if (channel) {
257
- Object.assign(channel, info, { updatedAt: new Date().toISOString() });
258
- await this.saveChannels();
259
- }
260
- }
261
- getAllChannels() {
262
- return Array.from(this.channels.values());
263
- }
264
- getChannelPersona(channelId) {
265
- return this.channels.get(channelId)?.persona;
266
- }
267
- async setChannelPersona(channelId, persona) {
268
- await this.initialize();
269
- const channel = this.channels.get(channelId);
270
- if (channel) {
271
- channel.persona = persona;
272
- channel.updatedAt = new Date().toISOString();
273
- await this.saveChannels();
274
- }
275
- }
276
- async addUserActionToSharedContext(content, importance) {
277
- await this.initialize();
278
- await this.sharedContext.addUserAction(content, this.agentId, undefined, importance);
279
- await this.sharedContext.updateAgentStatus(this.agentId, 'active');
280
- }
281
- async addSharedKnowledge(knowledge) {
282
- await this.initialize();
283
- await this.sharedContext.addSharedKnowledge(knowledge);
284
- }
285
- async getRecentActionsSummary(count) {
286
- return this.sharedContext.getRecentActionsSummary(count);
287
- }
288
- async getSharedKnowledge() {
289
- return this.sharedContext.getSharedKnowledge();
290
- }
291
- async getGlobalContext() {
292
- return this.sharedContext.getFullContext();
293
- }
294
- async getGlobalContextSummary() {
295
- return this.sharedContext.getContextSummary();
296
- }
297
- async createCooperation(type, task, toAgentId, context) {
298
- await this.initialize();
299
- return this.sharedContext.createCooperation(type, this.agentId, task, toAgentId, context);
300
- }
301
- async getPendingCooperations() {
302
- return this.sharedContext.getPendingCooperations(this.agentId);
303
- }
304
- async updateCooperationStatus(cooperationId, status, result) {
305
- await this.sharedContext.updateCooperationStatus(cooperationId, status, result);
306
- }
307
- async getAllRegisteredAgents() {
308
- return this.sharedContext.getAllAgents();
309
- }
310
- async findAgentByCapability(capability) {
311
- return this.sharedContext.findAgentByCapability(capability);
312
- }
313
- async getCooperation(cooperationId) {
314
- return this.sharedContext.getCooperation(cooperationId);
315
- }
316
- async updateAgentStatusInRegistry(status) {
317
- await this.sharedContext.updateAgentStatus(this.agentId, status);
318
- }
319
- }
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() - 获取操作日志
332
- `;
333
- class PiAgentSession {
43
+ import { parseToolCall as parseToolCallImpl, isFinalResponse as isFinalResponseImpl, extractFinalAnswer as extractFinalAnswerImpl } from './parse-tool-call.js';
44
+ import { sessionStore as defaultSessionStore } from './session-store.js';
45
+ import { ToolRegistry } from './tool-registry.js';
46
+ import { decideMaxIterations, decideContextOverflow, shouldCompactBeforeIteration } from './react-loop.js';
47
+ // PiSessionManager 已抽到 ./pi-sdk-session-manager.ts (2026-07-06)
48
+ // Tool / ToolResult / Message / StreamCallback / StreamEvent / HeartbeatConfig / AgentSession / TOOL_DEFINITIONS
49
+ // 已抽到 ./pi-sdk-types.ts (2026-07-06)
50
+ export class PiAgentSession {
334
51
  cwd;
335
52
  peerId;
336
53
  identity;
@@ -344,11 +61,17 @@ class PiAgentSession {
344
61
  socialHeartbeat = null;
345
62
  messageHistory = [];
346
63
  tools = new Map();
64
+ /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
65
+ _toolRegistry = new ToolRegistry();
347
66
  skillRegistry = new SkillRegistry();
348
67
  /** M2.4: 缓存 tool 列表, registerTools() 之后不变, runReActLoop 多次循环复用 */
349
68
  cachedToolDefinitions = '';
350
69
  /** M2.4: 缓存 persona section */
351
70
  cachedPersonaSection = '';
71
+ /** 2026-06-30: 持久化层 — 默认走 ~/.bolloon/sessions/cache/, 测试可注入临时目录. */
72
+ _sessionStore;
73
+ /** 构造期间 fire-and-forget 任务的 promise — whenReady() 等它 */
74
+ _readyPromise = null;
352
75
  // 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
353
76
  // 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
354
77
  // 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
@@ -360,6 +83,10 @@ class PiAgentSession {
360
83
  /** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
361
84
  * 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
362
85
  MAX_TOTAL_ERRORS = 20;
86
+ /** 2026-06-19: 记录 loop 内成功执行的工具结果, 失败退出时汇总给用户 */
87
+ successfulToolResults = [];
88
+ /** 2026-06-19: Agent Mesh 通信 — 本地 + 远端 inbox 缓存, 给 check_inbox 工具读 */
89
+ _inboxMessages = [];
363
90
  /** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
364
91
  LOOP_COMPACT_RATIO = 0.8;
365
92
  /** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
@@ -382,6 +109,9 @@ class PiAgentSession {
382
109
  */
383
110
  judgmentGateAddition = '';
384
111
  judgmentGateUsedIds = [];
112
+ // 2026-06-18: 来自 web server markedPrompt 外的 contextHint (channel/judgment/distill/remote channels),
113
+ // 拼到 systemPrompt 末尾, 别再混进 user message
114
+ contextHintAddition = '';
385
115
  /**
386
116
  * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
387
117
  * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
@@ -394,6 +124,8 @@ class PiAgentSession {
394
124
  promptStartTime = 0;
395
125
  /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
396
126
  currentChannelId = '';
127
+ /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
128
+ currentAgentId = '';
397
129
  /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
398
130
  currentIntent = 'chitchat';
399
131
  currentIntentHint = '';
@@ -437,6 +169,10 @@ class PiAgentSession {
437
169
  this.peerId = config.peerId || 'local';
438
170
  this.identity = config.identityDoc || this.createDefaultIdentity();
439
171
  this.minimaxAvailable = this.checkMinimax();
172
+ // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
173
+ this.currentAgentId = config.agentId || '';
174
+ // 2026-06-30: 持久化层可注入 — 测试传 tmpDir, 业务用默认 ~/.bolloon/sessions/cache/
175
+ this._sessionStore = config.sessionStore ?? defaultSessionStore;
440
176
  this.constraintLayer = new ConstraintLayer();
441
177
  this.workflowEngine = new WorkflowEngine(this.constraintLayer);
442
178
  this.sessionManager = new PiSessionManager(this.identity.did, this.cwd);
@@ -452,38 +188,37 @@ class PiAgentSession {
452
188
  // 之前 messageHistory 是空的, 服务重启后 LLM 看到的是新对话
453
189
  // 现在 loadSessionKey 形如 "channel-xxx:default" 走 ~/.bolloon/sessions/cache/<key>.json
454
190
  if (config.loadSessionKey) {
455
- this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30);
191
+ this._readyPromise = this.hydrateMessageHistory(config.loadSessionKey, config.loadSessionMaxMessages ?? 30).catch((err) => {
192
+ // 失败静默, 但不让 whenReady 永久 hang
193
+ console.warn(`[PiAgent] hydrateMessageHistory failed: ${err.message?.slice(0, 100)}`);
194
+ });
456
195
  }
457
196
  }
458
197
  /**
459
- * M2.3: session JSON 加载历史, 转成 messageHistory 格式
198
+ * 2026-06-30: 让外部 await 构造期间的 hydrate 完成.
199
+ * 解决 fire-and-forget 让 messageHistory 不可预测的问题.
200
+ * 不传 loadSessionKey 时立即返回.
201
+ */
202
+ whenReady() {
203
+ return this._readyPromise ?? Promise.resolve();
204
+ }
205
+ /**
206
+ * M2.3 (2026-06-30 重构): 从 SessionStore 加载历史, 转成 messageHistory 格式
460
207
  * - 失败静默 (历史加载失败不应该阻塞 agent 启动)
461
208
  * - 限制 max 条数, 防止 context 爆
462
- * - user 消息 role=user, ai 消息 role=assistant
463
- * - 跳过 metadata 中含 error (错误消息会污染 LLM)
209
+ * - 跳过错误消息 ([AI 服务调用失败] / [错误:...]) 不污染 LLM
210
+ * - 委托 SessionStore 完成 IO, 保证 save/load 路径对称
211
+ *
212
+ * 历史格式兼容旧 schema ({type, content}) 和新 schema (PersistedMessage[])
464
213
  */
465
214
  async hydrateMessageHistory(sessionKey, maxMessages) {
466
215
  try {
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) });
216
+ const loaded = await this._sessionStore.loadMessages(sessionKey);
217
+ if (!loaded) {
218
+ console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
219
+ return;
486
220
  }
221
+ const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
487
222
  if (hydrated.length > 0) {
488
223
  this.messageHistory = hydrated;
489
224
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
@@ -493,6 +228,84 @@ class PiAgentSession {
493
228
  console.warn(`[PiAgent] hydrateMessageHistory 失败 (non-fatal): ${err.message?.slice(0, 100)}`);
494
229
  }
495
230
  }
231
+ /**
232
+ * 2026-06-30: 把当前 messageHistory 持久化到 SessionStore.
233
+ * 公开方法 — claude code / 外部 harness 在每次 prompt 完成后调一下,
234
+ * 即可获得"重启 / 跨进程接续"的语义.
235
+ */
236
+ async saveCurrentSession(key) {
237
+ const persisted = this.messageHistory.map((m) => ({
238
+ role: m.role,
239
+ content: m.content,
240
+ toolCall: m.toolCall,
241
+ toolResult: m.toolResult,
242
+ toolCallId: m.toolCallId,
243
+ timestamp: Date.now(),
244
+ source: 'pi-session',
245
+ }));
246
+ await this._sessionStore.saveMessages(key, persisted);
247
+ }
248
+ /**
249
+ * 2026-06-30: 从 disk 拉历史覆盖当前 messageHistory.
250
+ * 返回加载条数 — 失败或空则返回 0.
251
+ * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
252
+ */
253
+ async resumeSession(key, maxMessages = 30) {
254
+ const before = this.messageHistory.length;
255
+ await this.hydrateMessageHistory(key, maxMessages);
256
+ return this.messageHistory.length - before;
257
+ }
258
+ /**
259
+ * 2026-06-30: 读历史不修改 messageHistory.
260
+ * 给 claude code / 测试做"先看一下历史"用 — 不破坏当前会话.
261
+ * 返回 Message[] 数组 (空数组表示无历史).
262
+ */
263
+ async peekSessionHistory(key, maxMessages = 30) {
264
+ try {
265
+ const loaded = await this._sessionStore.loadMessages(key);
266
+ if (!loaded)
267
+ return [];
268
+ return this._filterToMessage(loaded).slice(-maxMessages);
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ }
274
+ /** hydrateMessageHistory 用的过滤逻辑 — 提到外面复用 */
275
+ _filterToMessage(loaded) {
276
+ const hydrated = [];
277
+ const VALID_ROLES = new Set(['user', 'assistant', 'tool', 'system']);
278
+ for (const m of loaded) {
279
+ // role 必须合法 (拒绝旧 schema {type:'user'} 没 role 字段的)
280
+ if (!VALID_ROLES.has(m.role))
281
+ continue;
282
+ // 跳过污染消息
283
+ if (typeof m.content === 'string' && m.content.startsWith('[AI 服务调用失败]'))
284
+ continue;
285
+ if (typeof m.content === 'string' && m.content.startsWith('[错误:'))
286
+ continue;
287
+ // 注意: '!m.content' 会跳过 content='' 的 tool call 消息 (assistant role + toolCall 字段),
288
+ // 这种是合法的 (LLM 输出只有 tool call, 没有正文) — 必须保留.
289
+ // 这里只跳过"无内容 + 也没 tool call/tool result"的废消息.
290
+ if (!m.content && !m.toolCall && !m.toolResult)
291
+ continue;
292
+ // 跳过空 tool role (tool result 占位但没有任何内容)
293
+ if (m.role === 'tool' && !m.toolResult)
294
+ continue;
295
+ hydrated.push({
296
+ role: m.role,
297
+ content: m.content ?? '',
298
+ toolCall: m.toolCall,
299
+ toolResult: m.toolResult,
300
+ toolCallId: m.toolCallId,
301
+ });
302
+ }
303
+ return hydrated;
304
+ }
305
+ /** 暴露 store 给测试 / 高级集成用. */
306
+ get sessionStoreInstance() {
307
+ return this._sessionStore;
308
+ }
496
309
  /**
497
310
  * 从 SKILL.md 目录加载 skills 进 skillRegistry.
498
311
  *
@@ -501,23 +314,17 @@ class PiAgentSession {
501
314
  * 2. ~/.bolloon/skills/ 全局用户级
502
315
  * 3. <cwd>/.bolloon/skills/ 项目级
503
316
  * 4. ~/.boll/skills/ 全局 (兼容 bollharness 旧用户)
504
- * 5. <bolloon-repo>/src/bollharness/.boll/skills/ bolloon 仓库内置 skill
505
- * (bolloon 项目本身用 pi-sdk 写核心, 19 个 skill 视为项目级 builtin)
317
+ *
318
+ * 2026-07-04: 移除 18bollharness builtin skill (findBolloonBuiltinSkillsPath).
319
+ * 历史遗留: 写 pi-sdk 时为方便演示, 把 bolloon 项目里的 19 个 skill 强制注入到 system prompt.
320
+ * 问题: system prompt 涨到 22K chars, LLM (minimax M3) 在 pivot loop 里反复 think 不输出
321
+ * `<final gen>`, session 落盘拿不到最终回答.
322
+ * 现在: 只让用户放 .bolloon/skills/SKILL.md 才生效, 干净且 project-owned.
506
323
  *
507
324
  * 静默忽略不存在的目录.
508
325
  */
509
326
  loadSkills(paths) {
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
- }
327
+ const resolved = (paths && paths.length > 0) ? paths : defaultSkillPaths(os.homedir(), this.cwd);
521
328
  loadSkillsFromPaths(resolved)
522
329
  .then((skills) => {
523
330
  for (const s of skills) {
@@ -526,36 +333,17 @@ class PiAgentSession {
526
333
  }
527
334
  this.skillRegistry.register(s);
528
335
  }
529
- console.log(`[loadSkills] 已加载 ${skills.length} 个 skill: ${skills.map(describeSkill).join(' | ')}`);
336
+ console.log(`[loadSkills] 已加载 ${skills.length} 个 skill from ${resolved.join(', ')}`);
337
+ if (skills.length > 0) {
338
+ for (const s of skills) {
339
+ console.log(` - ${s.name}: ${s.description.substring(0, 100)}${s.description.length > 100 ? '...' : ''}`);
340
+ }
341
+ }
530
342
  })
531
343
  .catch((err) => {
532
344
  console.error('[loadSkills] 加载失败:', err);
533
345
  });
534
346
  }
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
- }
559
347
  async initHarness() {
560
348
  try {
561
349
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
@@ -572,634 +360,35 @@ class PiAgentSession {
572
360
  }
573
361
  }
574
362
  registerTools() {
575
- this.tools.set('read_document', {
576
- name: 'read_document',
577
- description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
578
- parameters: { path: 'string' },
579
- execute: async (args) => {
580
- try {
581
- const content = await documentReader.read(args.path);
582
- return {
583
- success: true,
584
- output: `📄 ${content.metadata.filename}\n大小: ${content.metadata.size} 字节\n\n${content.text.substring(0, 1000)}${content.text.length > 1000 ? '...' : ''}`
585
- };
586
- }
587
- catch (e) {
588
- return { success: false, error: String(e) };
589
- }
590
- }
591
- });
592
- this.tools.set('summarize_document', {
593
- name: 'summarize_document',
594
- description: '总结文档内容,分析并生成摘要',
595
- parameters: { path: 'string', context: 'string' },
596
- execute: async (args) => {
597
- try {
598
- if (!this.minimaxAvailable) {
599
- return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
600
- }
601
- const result = await this.summarizeDocument(args.path, args.context);
602
- return {
603
- success: true,
604
- output: `📝 摘要:\n${result.summary}\n\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10`
605
- };
606
- }
607
- catch (e) {
608
- return { success: false, error: String(e) };
609
- }
610
- }
611
- });
612
- this.tools.set('improve_document', {
613
- name: 'improve_document',
614
- description: '根据要求改进文档内容',
615
- parameters: { path: 'string', requirements: 'string' },
616
- execute: async (args) => {
617
- try {
618
- if (!this.minimaxAvailable) {
619
- return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
620
- }
621
- const result = await this.improveDocument({
622
- originalPath: args.path,
623
- requirements: args.requirements
624
- });
625
- return {
626
- success: true,
627
- output: `✅ 改进${result.improved ? '成功' : '失败'}\n质量评分: ${(result.qualityScore * 10).toFixed(1)}/10\n${result.newContent ? '\n改进内容:\n' + result.newContent.substring(0, 500) + '...' : ''}`
628
- };
629
- }
630
- catch (e) {
631
- return { success: false, error: String(e) };
632
- }
633
- }
634
- });
635
- this.tools.set('list_peers', {
636
- name: 'list_peers',
637
- description: '列出已连接的对等节点',
638
- parameters: {},
639
- execute: async () => {
640
- const peers = p2pNetwork.getPeers();
641
- if (peers.length === 0) {
642
- return { success: true, output: '当前无连接的对等节点' };
643
- }
644
- return { success: true, output: `已连接节点 (${peers.length}):\n${peers.map(p => ` - ${p}`).join('\n')}` };
645
- }
646
- });
647
- this.tools.set('send_message', {
648
- name: 'send_message',
649
- description: '向指定对等节点发送消息',
650
- parameters: { peer_id: '对等节点ID', message: '消息内容' },
651
- execute: async (args) => {
652
- try {
653
- await p2pNetwork.sendMessage(args.peer_id, 'message', args.message);
654
- return { success: true, output: `消息已发送到 ${args.peer_id}` };
655
- }
656
- catch (e) {
657
- return { success: false, error: String(e) };
658
- }
659
- }
660
- });
661
- this.tools.set('broadcast_message', {
662
- name: 'broadcast_message',
663
- description: '向所有对等节点广播消息',
664
- parameters: { message: '消息内容' },
665
- execute: async (args) => {
666
- try {
667
- await p2pNetwork.broadcast('message', args.message);
668
- return { success: true, output: '消息已广播到所有节点' };
669
- }
670
- catch (e) {
671
- return { success: false, error: String(e) };
672
- }
673
- }
674
- });
675
- this.tools.set('get_identity', {
676
- name: 'get_identity',
677
- description: '获取当前智能体身份信息',
678
- parameters: {},
679
- execute: async () => {
680
- const id = this.getIdentity();
681
- const extraInfo = id.cid ? `\nCID: ${id.cid}` : '';
682
- const ipnsInfo = id.ipnsName ? `\nIPNS: ${id.ipnsName}` : '';
683
- return {
684
- success: true,
685
- output: `DID: ${id.did}\n名称: ${id.name}\n公钥: ${id.publicKey}${extraInfo}${ipnsInfo}\n创建时间: ${new Date(id.createdAt).toISOString()}`
686
- };
687
- }
688
- });
689
- this.tools.set('set_persona', {
690
- name: 'set_persona',
691
- description: '更新智能体的 persona 信息,包括名字、描述、性格等',
692
- parameters: { persona_json: 'Persona JSON 对象,包含 name、description、personality、greeting 等字段' },
693
- execute: async (args) => {
694
- try {
695
- const personaData = typeof args.persona_json === 'string' ? JSON.parse(args.persona_json) : args.persona_json;
696
- const now = new Date().toISOString();
697
- const newPersona = {
698
- name: personaData.name || this.identity.name,
699
- description: personaData.description || '',
700
- capabilities: personaData.capabilities || [],
701
- personality: personaData.personality || '',
702
- greeting: personaData.greeting || '',
703
- interests: personaData.interests || [],
704
- createdAt: this.persona?.createdAt || now,
705
- updatedAt: now
706
- };
707
- await this.setPersona(newPersona);
708
- this.persona = newPersona;
709
- if (newPersona.name) {
710
- this.identity.name = newPersona.name;
711
- }
712
- return { success: true, output: `Persona 已更新:\n名称: ${newPersona.name}\n描述: ${newPersona.description}\n性格: ${newPersona.personality}` };
713
- }
714
- catch (e) {
715
- return { success: false, error: `更新 persona 失败: ${String(e)}` };
716
- }
717
- }
718
- });
719
- this.tools.set('get_operation_logs', {
720
- name: 'get_operation_logs',
721
- description: '获取约束层的操作日志',
722
- parameters: {},
723
- execute: async () => {
724
- const logs = this.constraintLayer.getLogs();
725
- if (logs.length === 0) {
726
- return { success: true, output: '暂无操作日志' };
727
- }
728
- return {
729
- success: true,
730
- output: `操作日志 (${logs.length} 条):\n${logs.slice(-10).map(l => `[${new Date(l.timestamp).toISOString()}] ${l.action} - ${l.status}`).join('\n')}`
731
- };
732
- }
733
- });
734
- // 添加文件列表工具
735
- this.tools.set('list_files', {
736
- name: 'list_files',
737
- description: '列出目录中的文件',
738
- parameters: { path: '目录路径(可选,默认为当前目录)' },
739
- execute: async (args) => {
740
- try {
741
- const fs = await import('fs');
742
- const path = args.path || this.cwd;
743
- const files = fs.readdirSync(path);
744
- return {
745
- success: true,
746
- output: `📁 目录 ${path} 中的文件 (${files.length} 个):\n${files.slice(0, 20).map(f => ` - ${f}`).join('\n')}${files.length > 20 ? '\n ...' : ''}`
747
- };
748
- }
749
- catch (e) {
750
- return { success: false, error: String(e) };
751
- }
752
- }
753
- });
754
- // 添加目录读取工具(更完整的实现)
755
- this.tools.set('read_directory', {
756
- name: 'read_directory',
757
- description: '读取目录内容,返回文件列表和目录结构',
758
- parameters: { path: '目录路径(可选,默认为当前目录)' },
759
- execute: async (args) => {
760
- try {
761
- const fs = await import('fs');
762
- const pathModule = await import('path');
763
- const targetPath = args.path || this.cwd;
764
- const items = fs.readdirSync(targetPath);
765
- const result = [];
766
- for (const item of items.slice(0, 30)) {
767
- const fullPath = pathModule.join(targetPath, item);
768
- try {
769
- const stat = fs.statSync(fullPath);
770
- const type = stat.isDirectory() ? '📁' : '📄';
771
- result.push(`${type} ${item}${stat.isDirectory() ? '/' : ''}`);
772
- }
773
- catch {
774
- result.push(`📄 ${item}`);
775
- }
776
- }
777
- return {
778
- success: true,
779
- output: `📂 ${targetPath} (${items.length} 项):\n${result.join('\n')}${items.length > 30 ? '\n... 还有更多文件' : ''}`
780
- };
781
- }
782
- catch (e) {
783
- return { success: false, error: `无法读取目录: ${String(e)}` };
784
- }
785
- }
786
- });
787
- // P2P Document Tools
788
- for (const tool of p2pDocumentTools) {
789
- this.tools.set(tool.name, tool);
790
- }
791
- // Shell Exec 工具: 给 AI 跑受限的 shell 命令
792
- // **只能** 跑白名单内的命令 (git/npm/tsc/vitest/cat/...)
793
- // **不能** 改禁区路径 (见 shell-guard.ts 的 FORBIDDEN_PATH_PATTERNS)
794
- // 沙箱 cwd: .bolloon-shell-sandbox/
795
- this.tools.set('shell_exec', {
796
- name: 'shell_exec',
797
- description: '在 cwd (process.cwd(), 即 bolloon 当前工作目录) 跑 shell 命令. 仅支持白名单内命令: git, npm, npx, tsx, tsc, vitest, cat, head, tail, ls, wc, echo, pwd, date, mkdir, touch. 禁止管道/重定向/rm -rf/sudo. 命中护栏黑名单会被拒. 注意: cwd 是真正的 git repo 工作目录, git add/commit/push 等命令会作用于本仓库, 不是 sandbox.',
798
- parameters: { command: '可执行文件 (必填, 必须在白名单)', args: '参数数组, 逗号分隔', timeoutMs: '超时毫秒, 默认 30000' },
799
- execute: async (args) => {
800
- const cmd = String(args.command || '').trim();
801
- if (!cmd)
802
- return { success: false, error: 'command 必填' };
803
- const argList = String(args.args || '').split(',').map(s => s.trim()).filter(Boolean);
804
- const timeoutMs = Number(args.timeoutMs) || 30000;
805
- const result = await shellExec(cmd, argList, { timeoutMs });
806
- if (result.deniedByGuard) {
807
- return { success: false, error: result.error };
808
- }
809
- if (!result.success) {
810
- return { success: false, error: result.error, output: result.output };
811
- }
812
- return { success: true, output: result.output };
813
- }
814
- });
815
- // self_improve 工具: AI 触发自我改进循环
816
- // **必须** 在 branchPrefix 命名的分支上工作
817
- // 心跳事件会自动调用; 用户对话里也能手动调
818
- this.tools.set('self_improve', {
819
- name: 'self_improve',
820
- description: `触发自我改进循环. AI 会在分支 ${getBranchPrefix()}<timestamp> 上工作, 跑 tsc + vitest 验证, 通过后输出分支名给用户审. 冷却期由策略文件决定. 命中护栏禁区的改动会被拒.`,
821
- parameters: { goal: '本轮改进目标 (1 句话)' },
822
- execute: async (args) => {
823
- const goal = String(args.goal || '').trim();
824
- if (!goal)
825
- return { success: false, error: 'goal 必填' };
826
- return await runSelfImproveLoop(goal);
827
- }
828
- });
829
- // list_skills 工具: 列出当前 session 已加载的 skills
830
- // 加载源: ~/.bolloon/skills/ → <cwd>/.bolloon/skills/ → ~/.boll/skills/
831
- this.tools.set('list_skills', {
832
- name: 'list_skills',
833
- description: '列出当前 session 已加载的 skills 及其描述. Skills 是从 SKILL.md 文件加载的, 兼容 Anthropic Agent Skills 标准 frontmatter 和 bollharness 现有 frontmatter.',
834
- parameters: {},
835
- execute: async () => {
836
- const skills = this.skillRegistry.list();
837
- if (skills.length === 0) {
838
- return {
839
- success: true,
840
- output: '当前 session 没有加载任何 skill. 检查 ~/.bolloon/skills/ 或项目 .bolloon/skills/ 目录.',
841
- };
842
- }
843
- const lines = skills.map((s, i) => `${i + 1}. ${s.name} — ${s.description}`);
844
- return { success: true, output: `已加载 ${skills.length} 个 skill:\n${lines.join('\n')}` };
845
- }
846
- });
847
- // use_skill 工具: 加载指定 skill 的 body 进 LLM context
848
- // Skills 协议核心: 把 SKILL.md body 作为 Markdown 指南返回, LLM 下一轮按它执行
849
- this.tools.set('use_skill', {
850
- name: 'use_skill',
851
- description: '按名称加载一个 skill, 把它的 SKILL.md body 作为 Markdown 文档返回. 调用后 LLM 会在下一轮按 skill 指南执行. 与 shell_exec / read_document 这些 "能力工具" 不同, use_skill 是 "知识注入".',
852
- parameters: { name: 'skill 名称 (用 list_skills 查可用的)' },
853
- execute: async (args) => {
854
- const name = String(args.name || '').trim();
855
- if (!name)
856
- return { success: false, error: 'name 必填' };
857
- if (!this.skillRegistry.has(name)) {
858
- const available = this.skillRegistry.list().map((s) => s.name).join(', ');
859
- return { success: false, error: `skill "${name}" 未找到. 已加载: ${available || '(无)'}` };
860
- }
861
- try {
862
- const body = await this.skillRegistry.execute(name, {});
863
- return { success: true, output: body };
864
- }
865
- catch (e) {
866
- return { success: false, error: `执行 skill 失败: ${String(e)}` };
867
- }
868
- }
869
- });
870
- // M2.1 (2026-06-17): 注册 4 个长期缺失的工具 — 让 agent 真正能"修改 + 提交"代码
871
- // 路径限制走 shell-guard 同款 allowlist (复用 checkWritePath / checkCommand)
872
- // 这些工具之前在 tool-gate 白名单 + tool-manifest 中存在, 但 pi-sdk 未注册 — 修复孤儿
873
- this.tools.set('write_file', {
874
- name: 'write_file',
875
- description: '写入一个文件. 路径必须在白名单 (src/web/*, src/agents/workflow-*, *.md, docs/**, src/test/**, src/agents/pi-sdk.ts 等). 大文件 (> 100KB) 会被拒. 命中护栏黑名单 (shell-guard.ts, package.json, .env, .git, .bolloon, dist) 会拒.',
876
- parameters: { path: '相对路径 (必填, 相对 cwd)', content: '文件内容 (必填)' },
877
- execute: async (args) => {
878
- const relPath = String(args.path || '').trim();
879
- const content = String(args.content ?? '');
880
- if (!relPath)
881
- return { success: false, error: 'path 必填' };
882
- if (content.length > 100_000)
883
- return { success: false, error: `内容过大 (${content.length} > 100000 字节), 请分块写` };
884
- // 路径检查: 复用 shell-guard 的 checkWritePath
885
- const pathResult = checkWritePath(relPath);
886
- if (!pathResult.allowed) {
887
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
888
- }
889
- try {
890
- const absPath = path.resolve(this.cwd, relPath);
891
- await fs.mkdir(path.dirname(absPath), { recursive: true });
892
- await fs.writeFile(absPath, content, 'utf-8');
893
- return { success: true, output: `✅ wrote ${relPath} (${content.length} bytes)` };
894
- }
895
- catch (e) {
896
- return { success: false, error: `写文件失败: ${String(e)}` };
897
- }
898
- }
899
- });
900
- this.tools.set('edit_file', {
901
- name: 'edit_file',
902
- description: '编辑一个文件: 在 path 处查找 old_text, 替换为 new_text. 找不到 old_text 会失败 (避免静默不替换). 路径同样受护栏限制.',
903
- parameters: { path: '相对路径 (必填)', old_text: '要替换的文本 (必填, 全文匹配)', new_text: '新文本 (必填)' },
904
- execute: async (args) => {
905
- const relPath = String(args.path || '').trim();
906
- const oldText = String(args.old_text ?? '');
907
- const newText = String(args.new_text ?? '');
908
- if (!relPath)
909
- return { success: false, error: 'path 必填' };
910
- if (!oldText)
911
- return { success: false, error: 'old_text 必填' };
912
- const pathResult = checkWritePath(relPath);
913
- if (!pathResult.allowed) {
914
- return { success: false, error: `路径被护栏拒: ${pathResult.reason}` };
915
- }
916
- try {
917
- const absPath = path.resolve(this.cwd, relPath);
918
- const original = await fs.readFile(absPath, 'utf-8');
919
- if (!original.includes(oldText)) {
920
- return { success: false, error: `old_text 在 ${relPath} 中未找到, 拒绝静默写入. 请先用 read_document 读最新内容.` };
921
- }
922
- const updated = original.replace(oldText, newText);
923
- await fs.writeFile(absPath, updated, 'utf-8');
924
- return { success: true, output: `✅ edited ${relPath} (${oldText.length} → ${newText.length} 字节)` };
925
- }
926
- catch (e) {
927
- return { success: false, error: `编辑文件失败: ${String(e)}` };
928
- }
929
- }
930
- });
931
- this.tools.set('git_diff', {
932
- name: 'git_diff',
933
- description: '查看 git diff. 默认显示未提交改动 (staged + unstaged), 可指定 ref1..ref2 看两个 commit/分支之间的 diff. 输出会截到 8000 字符避免超长.',
934
- parameters: { range: '可选. e.g. "HEAD~3..HEAD" 或 "master..agent/feat-x". 省略则看未提交改动.' },
935
- execute: async (args) => {
936
- const range = String(args.range || '').trim();
937
- const argv = range ? ['diff', range] : ['diff'];
938
- const result = await shellExec('git', argv, { timeoutMs: 10_000 });
939
- if (result.deniedByGuard)
940
- return { success: false, error: result.error };
941
- if (!result.success)
942
- return { success: false, error: result.error, output: result.output };
943
- const out = (result.output || '').slice(0, 8000);
944
- return { success: true, output: out || '(空 diff — 没有未提交改动)' };
945
- }
946
- });
947
- this.tools.set('git_commit', {
948
- name: 'git_commit',
949
- description: 'git add -A + git commit. 提交信息由 LLM 提供. 不会 push — push 是 agent 显式调 git_commit_and_push 触发. 命中护栏 (push to master/main, force-push) 仍会被拒. 内部自动设 BOLLOON_AUTO_EVOLVE=1 让 pre-commit hook 跳过 vitest/tsc (auto-evolve 模式, CI 兜底).',
950
- parameters: { message: 'commit message (必填, 用 HEREDOC 多行)' },
951
- execute: async (args) => {
952
- const message = String(args.message || '').trim();
953
- if (!message)
954
- return { success: false, error: 'message 必填' };
955
- // M3.4 (2026-06-17): agent git_commit 自动设 BOLLOON_AUTO_EVOLVE=1, 让 lefthook pre-commit 跳过 vitest/tsc
956
- // 这样频繁 commit 不会被 30s+ 的 pre-commit 阻塞. CI 会兜底 (push 后 GitHub Actions 跑测试)
957
- // 先 add
958
- const addResult = await shellExec('git', ['add', '-A'], { timeoutMs: 10_000 });
959
- if (addResult.deniedByGuard)
960
- return { success: false, error: addResult.error };
961
- if (!addResult.success)
962
- return { success: false, error: `git add 失败: ${addResult.error}` };
963
- // 再 commit (message 用 -m, 避免 HEREDOC 注入)
964
- // Windows shell 不支持 inline env var, 改用 child_process 直接 spawn
965
- // 简单方案: 用 spawn + 临时设 env
966
- try {
967
- const { spawn: spawnFn } = await import('child_process');
968
- const env = { ...process.env, BOLLOON_AUTO_EVOLVE: '1' };
969
- const output = await new Promise((resolve, reject) => {
970
- const proc = spawnFn('git', ['commit', '-m', message], {
971
- cwd: this.cwd, env, stdio: ['ignore', 'pipe', 'pipe'],
972
- });
973
- let stdout = '';
974
- let stderr = '';
975
- proc.stdout.on('data', (d) => stdout += d.toString());
976
- proc.stderr.on('data', (d) => stderr += d.toString());
977
- proc.on('close', (code) => code === 0 ? resolve(stdout) : reject(new Error(stderr || `git commit exited ${code}`)));
978
- proc.on('error', reject);
979
- });
980
- return { success: true, output: `✅ committed: ${message.split('\n')[0]}\n${output}` };
981
- }
982
- catch (e) {
983
- return { success: false, error: `git commit 失败: ${String(e.message || e).slice(0, 500)}` };
984
- }
985
- }
986
- });
987
- // M3.4: git_push — 改完直接 push (Q3-B 决策, 修改了 pre-push hook 让它别卡)
988
- // 命中护栏 (push to master/main, force-push) 仍会被拒 — 这两条底线不变
989
- this.tools.set('git_push', {
990
- name: 'git_push',
991
- description: 'git push 当前分支到 origin. 命中护栏 (push to master/main, --force) 仍会被拒. 用于长期项目自动 commit + push 循环.',
992
- parameters: { remote: '可选, 默认 origin', branch: '可选, 默认当前分支' },
993
- execute: async (args) => {
994
- const remote = String(args.remote || 'origin').trim();
995
- const branch = String(args.branch || '').trim();
996
- const argv = branch ? ['push', remote, branch] : ['push', remote];
997
- const result = await shellExec('git', argv, { timeoutMs: 60_000 });
998
- if (result.deniedByGuard)
999
- return { success: false, error: result.error };
1000
- if (!result.success)
1001
- return { success: false, error: result.error, output: result.output };
1002
- return { success: true, output: `✅ pushed to ${remote}${branch ? `/${branch}` : ''}\n${result.output || ''}` };
1003
- }
1004
- });
1005
- // M3.4: git_branch — 创建/切换分支 (用于多任务并行隔离)
1006
- this.tools.set('git_branch', {
1007
- name: 'git_branch',
1008
- description: 'git checkout -b <name> 或 git checkout <name>. 用于多任务并行隔离 — 改前先 checkout 到 agent/<task-id> 分支.',
1009
- parameters: { name: '分支名 (必填, e.g. "agent/task-123")', create: '可选, "true" 表示创建新分支 (默认 false = 切到已有)' },
1010
- execute: async (args) => {
1011
- const name = String(args.name || '').trim();
1012
- const create = String(args.create || 'false') === 'true';
1013
- if (!name)
1014
- return { success: false, error: 'name 必填' };
1015
- const argv = create ? ['checkout', '-b', name] : ['checkout', name];
1016
- const result = await shellExec('git', argv, { timeoutMs: 10_000 });
1017
- if (result.deniedByGuard)
1018
- return { success: false, error: result.error };
1019
- if (!result.success)
1020
- return { success: false, error: result.error, output: result.output };
1021
- return { success: true, output: `✅ ${create ? 'created + checked out' : 'checked out'} ${name}\n${result.output || ''}` };
1022
- }
1023
- });
1024
- // M3.2: 任务状态机工具 — 让 agent 自己维护 multi_step 任务的状态
1025
- this.tools.set('create_task', {
1026
- name: 'create_task',
1027
- description: '创建一个新多步任务, 初始 steps 列表由 LLM 给出. 返回 task-id, 后续用 update_task / get_task 跟踪进度. 任务状态写 ~/.bolloon/tasks/<id>.yaml 持久化.',
1028
- parameters: {
1029
- goal: '任务目标 (1 句话, 必填)',
1030
- steps: '步骤列表 (数组, 必填, 至少 1 步)',
1031
- sessionKey: '可选, 关联的 session key (channel:sessionId)',
1032
- branch: '可选, 关联的 git 分支名',
1033
- },
1034
- execute: async (args) => {
1035
- const goal = String(args.goal || '').trim();
1036
- const stepsRaw = args.steps;
1037
- if (!goal)
1038
- return { success: false, error: 'goal 必填' };
1039
- let steps = [];
1040
- if (Array.isArray(stepsRaw)) {
1041
- steps = stepsRaw.map((s) => String(s).trim()).filter(Boolean);
1042
- }
1043
- else if (typeof stepsRaw === 'string') {
1044
- // 支持 "step1\nstep2\nstep3" 或 "step1,step2,step3"
1045
- steps = stepsRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean);
1046
- }
1047
- if (steps.length === 0)
1048
- return { success: false, error: 'steps 必填且至少 1 步' };
1049
- const sessionKey = String(args.sessionKey || '').trim() || undefined;
1050
- const branch = String(args.branch || '').trim() || undefined;
1051
- try {
1052
- const { createTask } = await import('./task-state.js');
1053
- const task = await createTask({ goal, steps, sessionKey, branch });
1054
- return { success: true, output: `✅ task created: ${task.id}\nbranch: ${branch || '(未指定)'}\nsteps:\n${steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n')}` };
1055
- }
1056
- catch (e) {
1057
- return { success: false, error: `create_task 失败: ${String(e)}` };
1058
- }
1059
- }
1060
- });
1061
- this.tools.set('update_task', {
1062
- name: 'update_task',
1063
- description: '更新任务的某一步状态 (pending → running → done/failed/skipped). 系统会自动推进下一步 pending → running (当当前步 done 时).',
1064
- parameters: {
1065
- task_id: '任务 id (必填)',
1066
- step_id: '步骤 id (必填, e.g. "step-1")',
1067
- status: '新状态 (running | done | failed | skipped)',
1068
- result_summary: '可选, 结果摘要',
1069
- error: '可选, 失败原因 (status=failed 时填)',
1070
- },
1071
- execute: async (args) => {
1072
- const taskId = String(args.task_id || '').trim();
1073
- const stepId = String(args.step_id || '').trim();
1074
- const status = String(args.status || '').trim();
1075
- if (!taskId || !stepId)
1076
- return { success: false, error: 'task_id + step_id 必填' };
1077
- if (!['running', 'done', 'failed', 'skipped'].includes(status)) {
1078
- return { success: false, error: `status 必须是 running|done|failed|skipped` };
1079
- }
1080
- try {
1081
- const { updateStep } = await import('./task-state.js');
1082
- const patch = { status };
1083
- if (args.result_summary)
1084
- patch.resultSummary = String(args.result_summary);
1085
- if (args.error)
1086
- patch.error = String(args.error);
1087
- const updated = await updateStep(taskId, stepId, patch);
1088
- if (!updated)
1089
- return { success: false, error: `任务 ${taskId} 未找到` };
1090
- const nextRunning = updated.steps.find((s) => s.status === 'running');
1091
- return {
1092
- success: true,
1093
- output: `✅ step ${stepId} → ${status}\ntask 状态: ${updated.status}${nextRunning ? `\n下一步: ${nextRunning.id} — ${nextRunning.description}` : ''}`,
1094
- };
1095
- }
1096
- catch (e) {
1097
- return { success: false, error: `update_task 失败: ${String(e)}` };
1098
- }
1099
- }
1100
- });
1101
- this.tools.set('get_task', {
1102
- name: 'get_task',
1103
- description: '查任务的当前状态和步骤进度. 用于跨 loop / 跨 session 恢复.',
1104
- parameters: { task_id: '任务 id (必填)' },
1105
- execute: async (args) => {
1106
- const taskId = String(args.task_id || '').trim();
1107
- if (!taskId)
1108
- return { success: false, error: 'task_id 必填' };
1109
- try {
1110
- const { getTask } = await import('./task-state.js');
1111
- const t = await getTask(taskId);
1112
- if (!t)
1113
- return { success: false, error: `任务 ${taskId} 未找到` };
1114
- const lines = [
1115
- `任务: ${t.id}`,
1116
- `目标: ${t.goal}`,
1117
- `状态: ${t.status}`,
1118
- `branch: ${t.branch || '(未指定)'}`,
1119
- `sessionKey: ${t.sessionKey || '(未指定)'}`,
1120
- `创建: ${t.createdAt}`,
1121
- `更新: ${t.updatedAt}`,
1122
- ``,
1123
- `步骤:`,
1124
- ...t.steps.map((s) => ` ${s.status === 'done' ? '✅' : s.status === 'running' ? '🔄' : s.status === 'failed' ? '❌' : s.status === 'skipped' ? '⏭️' : '⏳'} ${s.id} — ${s.description}${s.resultSummary ? `\n 结果: ${s.resultSummary}` : ''}${s.error ? `\n 错误: ${s.error}` : ''}`),
1125
- ];
1126
- return { success: true, output: lines.join('\n') };
1127
- }
1128
- catch (e) {
1129
- return { success: false, error: `get_task 失败: ${String(e)}` };
1130
- }
1131
- }
1132
- });
1133
- this.tools.set('list_tasks', {
1134
- name: 'list_tasks',
1135
- description: '列出最近 N 个任务 (默认 10). 用于多任务并行管理.',
1136
- parameters: { limit: '可选, 默认 10' },
1137
- execute: async (args) => {
1138
- const limit = Number(args.limit) || 10;
1139
- try {
1140
- const { listTasks } = await import('./task-state.js');
1141
- const tasks = await listTasks(limit);
1142
- if (tasks.length === 0) {
1143
- return { success: true, output: '当前没有任务. 用 create_task 创建一个.' };
1144
- }
1145
- const lines = tasks.map((t) => {
1146
- const done = t.steps.filter((s) => s.status === 'done').length;
1147
- const total = t.steps.length;
1148
- return `${t.status === 'completed' ? '✅' : t.status === 'failed' ? '❌' : '🔄'} ${t.id} — ${t.goal} (${done}/${total} steps, branch: ${t.branch || '-'})`;
1149
- });
1150
- return { success: true, output: `最近 ${tasks.length} 个任务:\n${lines.join('\n')}` };
1151
- }
1152
- catch (e) {
1153
- return { success: false, error: `list_tasks 失败: ${String(e)}` };
1154
- }
1155
- }
1156
- });
1157
- // M3.3 (2026-06-17): 工具幂等性 — 显式 cache 防止重试时副作用执行两次
1158
- // 设计: 在 registerTools 末尾 wrap 所有 this.tools, 每个调用走 cache:
1159
- // - 算 (toolName + JSON.stringify(args)) 的 hash
1160
- // - 命中 cache → 返缓存结果 (不重跑副作用, 关键对 write_file / edit_file / shell_exec)
1161
- // - 失败结果不缓存 (避免缓存 transient 错误)
1162
- // - 缓存容量 200 条, 超过就清空
1163
- this.wrapToolsWithIdempotency();
1164
- }
1165
- /** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
1166
- idempotencyCache = new Map();
1167
- IDEMPOTENCY_TTL_MS = 5 * 60 * 1000; // 5 分钟内同 (tool, args) 走 cache
1168
- IDEMPOTENCY_MAX = 200;
1169
- wrapToolsWithIdempotency() {
1170
- // 只 wrap 会产生副作用的工具 — 读类工具 (list_files, read_document, get_task) 不 wrap
1171
- // (让 LLM 拿到最新数据, 不会因为缓存读到旧 task 状态)
1172
- const SIDE_EFFECT_TOOLS = new Set([
1173
- 'write_file', 'edit_file', 'shell_exec', 'git_commit', 'git_push', 'git_branch',
1174
- 'create_task', 'update_task',
1175
- ]);
363
+ // 2026-07-06: 工具注册抽到 ./pi-sdk-tools.ts, 这里只调 + 镜像到 ToolRegistry
364
+ this._inboxMessages = [];
365
+ const toolCtx = {
366
+ tools: this.tools,
367
+ cwd: this.cwd,
368
+ identity: this.identity,
369
+ persona: this.persona,
370
+ minimaxAvailable: this.minimaxAvailable,
371
+ setPersona: async (p) => { await this.setPersona(p); },
372
+ sessionManager: this.sessionManager,
373
+ constraintLayer: this.constraintLayer,
374
+ _inboxMessages: this._inboxMessages,
375
+ };
376
+ registerBuiltinTools(toolCtx);
377
+ registerWalletTools(toolCtx);
378
+ setupInboxListener(toolCtx);
379
+ // 镜像到 ToolRegistry (alias resolve 用)
1176
380
  for (const [name, tool] of this.tools.entries()) {
1177
- if (!SIDE_EFFECT_TOOLS.has(name))
1178
- continue;
1179
- const original = tool.execute;
1180
- tool.execute = async (args) => {
1181
- const key = `${name}|${JSON.stringify(args)}`;
1182
- const cached = this.idempotencyCache.get(key);
1183
- if (cached && Date.now() - cached.ts < this.IDEMPOTENCY_TTL_MS) {
1184
- // 命中 — 加标记让 LLM 知道这是 cache (不会真执行副作用)
1185
- return { ...cached.result, output: (cached.result.output || '') + '\n[↻ idempotency cache hit]' };
1186
- }
1187
- const result = await original(args);
1188
- // 只缓存成功结果, 避免缓存 transient 错误
1189
- if (result && result.success) {
1190
- if (this.idempotencyCache.size >= this.IDEMPOTENCY_MAX) {
1191
- this.idempotencyCache.clear();
1192
- }
1193
- this.idempotencyCache.set(key, { result, ts: Date.now() });
1194
- }
1195
- return result;
1196
- };
381
+ this._toolRegistry.register(tool);
1197
382
  }
383
+ // M3.3: 副作用工具走幂等性 cache
384
+ this._idempotencyCache.wrap(this.tools);
1198
385
  }
1199
386
  /** 清幂等性缓存 — 强制下次调用真正执行 (用于 agent 显式需要重新跑的场景) */
1200
387
  clearIdempotencyCache() {
1201
- this.idempotencyCache.clear();
388
+ this._idempotencyCache.clear();
1202
389
  }
390
+ /** M3.3: 工具结果缓存 — 防止 loop 重试时副作用 (写文件 / 改代码) 执行多次 */
391
+ _idempotencyCache = new IdempotencyCache();
1203
392
  async registerP2PDocumentReceiver() {
1204
393
  await initDocumentReceiver();
1205
394
  }
@@ -1207,10 +396,13 @@ class PiAgentSession {
1207
396
  // M2.4 (2026-06-17): 缓存 tool 定义 — registerTools() 在构造时调一次, 此后不变
1208
397
  if (this.cachedToolDefinitions)
1209
398
  return this.cachedToolDefinitions;
1210
- const defs = ['可用工具:'];
399
+ const defs = ['可用工具 (name(params) - 简介):'];
1211
400
  for (const tool of this.tools.values()) {
1212
- const params = Object.entries(tool.parameters).map(([k, v]) => `${k}: ${v}`).join(', ');
1213
- defs.push(`- ${tool.name}(${params}) - ${tool.description}`);
401
+ // 2026-06-19: 压缩 tool 定义 只显示参数名 (不显示描述, 减少 60% 长度)
402
+ // 完整 description 在 history 第一轮注入 (getToolDefinitionsFull 调用), 后续轮只看简短
403
+ // 避免 system prompt 太大导致 minimax 撞 max_tokens 输出空
404
+ const paramNames = Object.keys(tool.parameters).join(',');
405
+ defs.push(`- ${tool.name}(${paramNames})`);
1214
406
  }
1215
407
  this.cachedToolDefinitions = defs.join('\n');
1216
408
  return this.cachedToolDefinitions;
@@ -1307,15 +499,27 @@ class PiAgentSession {
1307
499
  }
1308
500
  }
1309
501
  async promptStream(input, onStream, signal, channelId) {
502
+ console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
1310
503
  this.minimaxAvailable = this.checkMinimax();
504
+ console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
1311
505
  this.currentChannelId = channelId ?? this.currentChannelId;
506
+ // 2026-06-18 (supervisor): web server 把 46K markedPrompt 喂过来
507
+ // (【本轮用户请求】\n<text>\n【请求结束】\n\n<contextHint>).
508
+ // 整个 input 走下游, pivot loop 之前拿 47K buildContext 当 user message 发出去,
509
+ // 模型撞 context window. 提取 userText 替代 input, contextHint 拼到 systemPrompt 末尾.
510
+ const markerMatch = input.match(/【本轮用户请求】\s*([\s\S]*?)\s*【请求结束】/);
511
+ const userText = markerMatch ? markerMatch[1].trim() : input;
512
+ const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
513
+ console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
1312
514
  this.messageHistory.push({
1313
515
  role: 'user',
1314
- content: input
516
+ content: userText
1315
517
  });
518
+ // 2026-06-18: web server 喂的 markedPrompt 外的 contextHint 拼到 system 末尾 (而不是当 user message)
519
+ this.contextHintAddition = contextHint;
1316
520
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1317
521
  if (!this.minimaxAvailable) {
1318
- const response = await this.handleFallback(input);
522
+ const response = await this.handleFallback(userText);
1319
523
  this.messageHistory.push({ role: 'assistant', content: response });
1320
524
  onStream({ type: 'done', content: '' });
1321
525
  return response;
@@ -1323,11 +527,11 @@ class PiAgentSession {
1323
527
  // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1324
528
  this.currentOnStream = onStream;
1325
529
  this.currentSignal = signal ?? null;
1326
- await this.computeJudgmentGate(input);
530
+ await this.computeJudgmentGate(userText);
1327
531
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1328
532
  try {
1329
533
  const { classifyIntent, intentHint } = await import('./intent-classifier.js');
1330
- this.currentIntent = classifyIntent(input);
534
+ this.currentIntent = classifyIntent(userText);
1331
535
  this.currentIntentHint = intentHint(this.currentIntent);
1332
536
  if (this.currentIntent !== 'chitchat') {
1333
537
  onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
@@ -1348,14 +552,68 @@ class PiAgentSession {
1348
552
  }
1349
553
  // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
1350
554
  // (失败静默, 5s 限流防止循环)
555
+ // 2026-07-04: 透传 agentId 让 onSessionStart 加载 persona 文档
1351
556
  let bootstrapAddition = '';
1352
557
  try {
1353
- const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
558
+ const ss = await onSessionStart({
559
+ channelId: this.currentChannelId || undefined,
560
+ agentId: this.currentAgentId || undefined,
561
+ });
1354
562
  bootstrapAddition = ss.systemAddition || '';
1355
563
  }
1356
564
  catch (err) {
1357
565
  console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
1358
566
  }
567
+ // 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
568
+ // 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
569
+ if (this.currentChannelId) {
570
+ try {
571
+ const { getRecentEvents } = await import('../bootstrap/event-log.js');
572
+ const events = await getRecentEvents(this.currentChannelId, 5);
573
+ if (events.length > 0) {
574
+ const eventBlock = [
575
+ '## 最近项目事件 (最近 5 条, 倒序)',
576
+ ...events.map(e => `- [${e.ts.slice(0, 16)}] [${e.type}] ${e.summary}`),
577
+ ].join('\n');
578
+ bootstrapAddition = (bootstrapAddition + '\n\n' + eventBlock).slice(-2000);
579
+ }
580
+ }
581
+ catch (err) {
582
+ console.warn('[PiAgent] getRecentEvents failed (non-fatal):', err);
583
+ }
584
+ // 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
585
+ try {
586
+ const { readState, formatStateForPrompt } = await import('../bootstrap/project-state.js');
587
+ const state = await readState({ channelId: this.currentChannelId });
588
+ const stateText = formatStateForPrompt(state);
589
+ if (stateText) {
590
+ bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
591
+ }
592
+ }
593
+ catch (err) {
594
+ console.warn('[PiAgent] readState failed (non-fatal):', err);
595
+ }
596
+ // 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
597
+ try {
598
+ const { searchIndex } = await import('../bootstrap/vector-index.js');
599
+ const indexName = `channel-${this.currentChannelId}`;
600
+ const results = await searchIndex({
601
+ indexName,
602
+ query: userText,
603
+ topK: 3,
604
+ });
605
+ if (results.length > 0) {
606
+ const hitBlock = [
607
+ '## 相关历史片段 (top-3, TF-IDF cosine)',
608
+ ...results.map((r, i) => `- [${i + 1}] score=${r.score.toFixed(3)}: ${r.text.slice(0, 200).replace(/\n/g, ' ')}`),
609
+ ].join('\n');
610
+ bootstrapAddition = (bootstrapAddition + '\n\n' + hitBlock).slice(-3000);
611
+ }
612
+ }
613
+ catch (err) {
614
+ // 索引不存在是常见情况 (新 channel), 不打 warn
615
+ }
616
+ }
1359
617
  this.bootstrapAddition = bootstrapAddition;
1360
618
  // P2: 解析当前 permission mode (BootstrapOptions > env BOLLOON_PERM_MODE > default)
1361
619
  try {
@@ -1372,7 +630,7 @@ class PiAgentSession {
1372
630
  if (this.usePivotLoop) {
1373
631
  let pivotResult = '';
1374
632
  try {
1375
- const lr = await this.promptWithPivotLoop(input, undefined, channelId);
633
+ const lr = await this.promptWithPivotLoop(userText, undefined, channelId);
1376
634
  pivotResult = lr.response || '';
1377
635
  onStream({ type: 'done', content: '' });
1378
636
  }
@@ -1396,7 +654,7 @@ class PiAgentSession {
1396
654
  }
1397
655
  catch { }
1398
656
  }
1399
- monitorAfterReply(input, pivotResult);
657
+ monitorAfterReply(userText, pivotResult);
1400
658
  const stopStartTime = this.promptStartTime || Date.now();
1401
659
  onStop({
1402
660
  channelId: this.currentChannelId || 'unknown',
@@ -1407,6 +665,7 @@ class PiAgentSession {
1407
665
  this.currentOnStream = null;
1408
666
  this.currentSignal = null;
1409
667
  this.bootstrapAddition = '';
668
+ this.contextHintAddition = '';
1410
669
  this.promptStartTime = 0;
1411
670
  }
1412
671
  return pivotResult;
@@ -1540,37 +799,53 @@ class PiAgentSession {
1540
799
  }
1541
800
  // M2.4: persona 缓存
1542
801
  if (!this.cachedPersonaSection && this.persona) {
1543
- this.cachedPersonaSection = `
1544
- 角色描述: ${this.persona.description || '无'}
1545
- 性格特点: ${this.persona.personality || '无'}
1546
- 问候语: ${this.persona.greeting || '无'}
802
+ this.cachedPersonaSection = `
803
+ 角色描述: ${this.persona.description || '无'}
804
+ 性格特点: ${this.persona.personality || '无'}
805
+ 问候语: ${this.persona.greeting || '无'}
1547
806
  `;
1548
807
  }
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}`;
808
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${this.cachedPersonaSection}
809
+ 当前工作目录: ${this.cwd}
810
+ 当前身份: ${this.identity.name} (${this.identity.did})
811
+ ${this.currentIntentHint}
812
+
813
+ ${this.getToolDefinitions()}
814
+
815
+ 工作模式:
816
+ 1. 理解用户自然语言请求
817
+ 2. 分析需要哪些工具来完成
818
+ 3. 按顺序调用工具并观察结果
819
+ 4. 根据观察结果决定下一步
820
+ 5. 最终给出完整回答
821
+
822
+ 重要 (一次命中要求):
823
+ - 每次只调用一个工具
824
+ - 仔细分析工具返回结果
825
+ - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
826
+ - 如果需要更多信息,继续调用工具
827
+
828
+ 【工具调用格式 (严格遵守, 否则系统无法解析)】
829
+ - 你只能输出**一个**工具调用, 不要堆叠多个 invoke
830
+ - 工具调用格式: {"name":"<tool_name>","input":{"arg1":"value1"}}
831
+ - 用 markdown json code block 包裹: \`\`\`json\n{"name":"X","input":{...}}\n\`\`\`
832
+ - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
833
+ - 工具调用后必须等结果, 不要在同一个回复里继续输出
834
+ - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1568
835
  // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1569
836
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1570
837
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1571
838
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
1572
839
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
1573
- const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined);
840
+ // 2026-07-06: pivot token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
841
+ // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
842
+ // 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
843
+ // 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
844
+ // 不再撞预算. 这条路径留作技术债.
845
+ const onCompact = async () => {
846
+ // no-op (best-effort hook for future pi-sdk/pivot history sync)
847
+ };
848
+ const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
1574
849
  this.messageHistory.push({ role: 'user', content: input });
1575
850
  if (result.response) {
1576
851
  this.messageHistory.push({ role: 'assistant', content: result.response });
@@ -1614,10 +889,12 @@ ${this.getToolDefinitions()}
1614
889
  while (iteration < this.MAX_REACT_ITERATIONS) {
1615
890
  iteration++;
1616
891
  // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
1617
- if (iteration >= this.MAX_REACT_ITERATIONS) {
892
+ // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideMaxIterations 纯函数
893
+ const maxIterDecision = decideMaxIterations(iteration, this.MAX_REACT_ITERATIONS);
894
+ if (maxIterDecision.shouldExit) {
1618
895
  console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
1619
896
  onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
1620
- finalResponse = finalResponse || '(本轮 ReAct 循环达到最大步数, 强制结束)';
897
+ finalResponse = finalResponse || maxIterDecision.finalAnswer;
1621
898
  break;
1622
899
  }
1623
900
  // 停止条件 2: signal.aborted (显式 abort / 用户中断)
@@ -1631,14 +908,23 @@ ${this.getToolDefinitions()}
1631
908
  if (totalErrors >= this.MAX_TOTAL_ERRORS) {
1632
909
  console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
1633
910
  onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
1634
- finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
911
+ // 2026-06-19: 即使 LLM 一直失败, 也汇总之前成功执行的 tool result 给用户
912
+ if (this.successfulToolResults.length > 0) {
913
+ finalResponse = `✅ 之前步骤成功执行了 ${this.successfulToolResults.length} 个工具 (但 LLM 后续 ${totalErrors} 次调用失败):\n` +
914
+ this.successfulToolResults.map((r, i) => ` ${i + 1}. ${r.tool}: ${r.outputPreview}`).join('\n') +
915
+ `\n\n⚠️ (LLM 连续失败, 可能是 minimax 上游限流/网络问题, 工具已成功执行但 LLM 没能继续总结)`;
916
+ }
917
+ else {
918
+ finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
919
+ }
1635
920
  break;
1636
921
  }
1637
922
  // 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
1638
923
  // compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
924
+ // 2026-07-01 (v0.2.4 子任务 1): 触发判定走 shouldCompactBeforeIteration 纯函数
1639
925
  const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
1640
926
  const estimatedTokensBefore = this.estimateHistoryTokens();
1641
- if (estimatedTokensBefore > compactThreshold) {
927
+ if (shouldCompactBeforeIteration(estimatedTokensBefore, compactThreshold)) {
1642
928
  const tokensBeforeCompact = estimatedTokensBefore;
1643
929
  console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
1644
930
  onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
@@ -1651,11 +937,13 @@ ${this.getToolDefinitions()}
1651
937
  }
1652
938
  }
1653
939
  // 停止条件 3: context overflow (compact 后还超, 强制终止)
940
+ // 2026-07-01 (v0.2.4 子任务 1): 委托给 react-loop.decideContextOverflow 纯函数
1654
941
  const estimatedTokens = this.estimateHistoryTokens();
1655
- if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
942
+ const overflowDecision = decideContextOverflow(estimatedTokens, this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD);
943
+ if (overflowDecision.shouldExit) {
1656
944
  console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
1657
945
  onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
1658
- finalResponse = finalResponse || '(本轮 ReAct 循环因上下文溢出终止)';
946
+ finalResponse = finalResponse || overflowDecision.finalAnswer;
1659
947
  break;
1660
948
  }
1661
949
  // 调试日志:显示每次循环开始
@@ -1680,33 +968,33 @@ ${this.getToolDefinitions()}
1680
968
  }
1681
969
  // M2.4: persona section 缓存 — persona 在 loadPersona() 时一次设定, 此后不变
1682
970
  if (!this.cachedPersonaSection && this.persona) {
1683
- this.cachedPersonaSection = `
1684
- 角色描述: ${this.persona.description || '无'}
1685
- 性格特点: ${this.persona.personality || '无'}
1686
- 问候语: ${this.persona.greeting || '无'}
971
+ this.cachedPersonaSection = `
972
+ 角色描述: ${this.persona.description || '无'}
973
+ 性格特点: ${this.persona.personality || '无'}
974
+ 问候语: ${this.persona.greeting || '无'}
1687
975
  `;
1688
976
  }
1689
977
  const personaSection = this.cachedPersonaSection;
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}`;
978
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
979
+ 当前工作目录: ${this.cwd}
980
+ 当前身份: ${this.identity.name} (${this.identity.did})
981
+ ${refineContext}
982
+ ${this.currentIntentHint}
983
+
984
+ ${toolDefs}
985
+
986
+ 工作模式:
987
+ 1. 理解用户自然语言请求
988
+ 2. 分析需要哪些工具来完成
989
+ 3. 按顺序调用工具并观察结果
990
+ 4. 根据观察结果决定下一步
991
+ 5. 最终给出完整回答
992
+
993
+ 重要:
994
+ - 每次只调用一个工具
995
+ - 仔细分析工具返回结果
996
+ - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
997
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}${this.contextHintAddition}`;
1710
998
  // 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
1711
999
  // 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
1712
1000
  // 2. reactive compaction (prompt 估算超阈值, 跑压缩)
@@ -1714,18 +1002,61 @@ ${toolDefs}
1714
1002
  // 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
1715
1003
  const response = await this.callLlmWithRecovery(llm, messages, systemPrompt, signal, onStream);
1716
1004
  const reply = (response.reply || '').trim();
1717
- // 2026-06-16: 看到 [AI 服务调用失败] sentinel 不再立即 break,
1718
- // 而是设 aiFailed=true, 让外层 promptStream 自动重跑整个 loop 最多 N 次
1719
- // (LLM API 401 / 网络错 / 配额满时, pi-ai 返回这个 prefix;
1720
- // 自动 retry 兜底: 临时网络抖动可自愈, 真挂 N 次后才报失败)
1005
+ // 2026-06-30: OpenAI 协议 native tool_calls (LLM 真产了 tool_call 时, minimax/M3 会返回 id)
1006
+ const nativeToolCalls = response.toolCalls;
1007
+ // 2026-06-19 架构 fix: 不再因 [AI 服务调用失败] break
1008
+ // 旧逻辑: sentinel → aiFailed=true → break → 外层 retry 整个 loop (重置 history)
1009
+ // 新逻辑: 把错误当 tool_result push 进 history → 下一轮 LLM 看到错误能反思重试
1010
+ // 这是 dive-into 文档的"fail-open error recovery" — 错误进入 context, 不让 LLM 重复犯同样错
1011
+ // 2026-07-06: 对不可恢复的 API 错误直接终止, 不再无限重试
1721
1012
  if (reply.startsWith('[AI 服务调用失败]')) {
1722
- console.log(`[PiAgent] 收到 AI 错误 sentinel, 标记 aiFailed, 外层会自动重试整个 loop`);
1723
- aiFailed = true;
1013
+ console.log(`[PiAgent] 收到 AI 错误 sentinel`);
1014
+ console.log(`[sentinel DEBUG] 完整 reply: ${reply}`);
1015
+ console.log(`[sentinel DEBUG] 上一轮 messages 数量: ${Array.isArray(messages) ? messages.length : 'N/A'}, systemPrompt 长度: ${systemPrompt.length}`);
1724
1016
  aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
1017
+ totalErrors++;
1018
+ consecutiveErrors++;
1019
+ // 2026-07-06: 检测不可恢复的 API 错误 — 这些错误 LLM 无法通过反思修复, 重试无意义
1020
+ const isFatalApiError = reply.includes('chat content is empty') ||
1021
+ reply.includes('invalid params') ||
1022
+ reply.includes('401') ||
1023
+ reply.includes('403') ||
1024
+ reply.includes('quota') ||
1025
+ reply.includes('rate limit') ||
1026
+ reply.includes('API key') ||
1027
+ reply.includes('authentication') ||
1028
+ reply.includes('unauthorized');
1029
+ if (isFatalApiError) {
1030
+ console.log(`[PiAgent] 检测到不可恢复的 API 错误, 终止 loop: ${aiFailureReason}`);
1031
+ if (onStream) {
1032
+ onStream({ type: 'error', content: `⛔ API 错误无法恢复: ${aiFailureReason}`, tool: 'system' });
1033
+ }
1034
+ finalResponse = `❌ AI 服务调用失败: ${aiFailureReason}\n\n这是一个底层 API 错误, 不是任务本身的问题。请检查 API 配置或稍后重试。`;
1035
+ aiFailed = true;
1036
+ break;
1037
+ }
1038
+ // 连续错误过多也终止, 防止 LLM 陷入死循环
1039
+ if (consecutiveErrors >= 3) {
1040
+ console.log(`[PiAgent] 连续 ${consecutiveErrors} 次 AI 错误, 终止 loop`);
1041
+ if (onStream) {
1042
+ onStream({ type: 'error', content: `⛔ 连续 ${consecutiveErrors} 次 AI 错误, 终止循环`, tool: 'system' });
1043
+ }
1044
+ finalResponse = `❌ AI 连续调用失败 ${consecutiveErrors} 次, 已终止。\n\n失败原因: ${aiFailureReason}\n\n请检查 API 配置或简化任务后重试。`;
1045
+ aiFailed = true;
1046
+ break;
1047
+ }
1048
+ // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
1049
+ this.messageHistory.push({
1050
+ role: 'system',
1051
+ content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
1052
+ });
1725
1053
  if (onStream) {
1726
- onStream({ type: 'status', content: `⚠️ AI 调用失败, 将自动重试整个 loop`, tool: 'system' });
1054
+ onStream({ type: 'status', content: `⚠️ AI 调用失败 ${totalErrors}/${this.MAX_TOTAL_ERRORS}, push 错误到 history 让 LLM 反思`, tool: 'system' });
1727
1055
  }
1728
- break;
1056
+ // 退避 2s 后继续 — 临时 minimax 限流避开, 不让 loop 终止
1057
+ await new Promise(resolve => setTimeout(resolve, 2000));
1058
+ // 关键: 不设 aiFailed=true, 让外层不重试整个 loop (重置 history), 继续内层循环
1059
+ continue;
1729
1060
  }
1730
1061
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
1731
1062
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
@@ -1733,20 +1064,14 @@ ${toolDefs}
1733
1064
  if (onStream) {
1734
1065
  onStream({ type: 'token', content: reply.substring(0, 100) });
1735
1066
  }
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
- }
1067
+ // 2026-06-19 架构 fix: parseToolCall 优先于 isFinalResponse
1068
+ // 之前: 思考块里的 "<final gen>" 触发 isFinalResponse 提前 break, 工具从未真正执行
1069
+ // 现在: 先尝试解析 tool_call, 有就执行; 没有才检查是不是真正的 final gen
1748
1070
  const toolCall = this.parseToolCall(reply);
1071
+ // 2026-06-30 修: 给 toolCall 分配稳定 id, 让后续 tool result 能引用同一个 id
1072
+ // OpenAI 协议要求 messages 里 tool result 必须有对应的 tool_call_id, 否则 400
1749
1073
  if (toolCall) {
1074
+ toolCall.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1750
1075
  this.messageHistory.push({
1751
1076
  role: 'assistant',
1752
1077
  content: reply,
@@ -1912,7 +1237,7 @@ ${toolDefs}
1912
1237
  catch (err) {
1913
1238
  console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1914
1239
  }
1915
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
1240
+ this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
1916
1241
  this.logToHarness(toolCall.name, toolCall.args, result);
1917
1242
  // 通知前端工具执行结果
1918
1243
  if (onStream) {
@@ -1944,6 +1269,16 @@ ${toolDefs}
1944
1269
  }
1945
1270
  if (result.success) {
1946
1271
  consecutiveErrors = 0; // 重置连续错误计数
1272
+ // 2026-06-19: 记录成功结果, 用于 LLM 失败退出时汇总给用户
1273
+ if (result.output) {
1274
+ this.successfulToolResults.push({
1275
+ tool: toolCall.name,
1276
+ outputPreview: result.output.substring(0, 200) + (result.output.length > 200 ? '...' : '')
1277
+ });
1278
+ }
1279
+ else {
1280
+ this.successfulToolResults.push({ tool: toolCall.name, outputPreview: '(无输出)' });
1281
+ }
1947
1282
  // 检查工具执行质量
1948
1283
  lastQualityScore = this.estimateToolResultQuality(result);
1949
1284
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
@@ -2015,6 +1350,36 @@ ${toolDefs}
2015
1350
  if (onStream) {
2016
1351
  onStream({ type: 'token', content: reply.substring(0, 150) });
2017
1352
  }
1353
+ // 2026-06-19 架构 fix: 只有 strip <think> 后才检查 isFinalResponse
1354
+ // (parseToolCall 已先尝试, 既然没解析出 tool_call, 现在检查 final gen 是否真的在最终回答区)
1355
+ if (this.isFinalResponse(reply)) {
1356
+ // 2026-06-19 dive-into 风格修复: 如果还有 successful tool results 没汇报,
1357
+ // LLM 不能提前 final_gen — harness 自动注入"请汇报剩余工具结果" hint 再 continue
1358
+ // 这是 dive-into 文档"step 9 stop condition check" 的具体化:
1359
+ // stop condition = (有工具结果未汇报) ? continue : break
1360
+ // 检查回复是否包含工具结果内容(避免无限循环)
1361
+ const hasToolResultContent = this.successfulToolResults.some(r => reply.includes(r.tool) || reply.includes(r.outputPreview.substring(0, 50)));
1362
+ // 如果回复包含工具结果内容,清除 successfulToolResults
1363
+ if (hasToolResultContent) {
1364
+ console.log(`[PiAgent] 回复包含工具结果内容, 清除 successfulToolResults (${this.successfulToolResults.length} 个)`);
1365
+ this.successfulToolResults = [];
1366
+ }
1367
+ if (this.successfulToolResults.length > 0 && iteration < this.MAX_REACT_ITERATIONS) {
1368
+ const unreported = this.successfulToolResults.length;
1369
+ console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报, push hint 让其继续`);
1370
+ this.messageHistory.push({
1371
+ role: 'system',
1372
+ content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
1373
+ });
1374
+ if (onStream) {
1375
+ onStream({ type: 'status', content: `🔄 还有 ${unreported} 个工具结果未汇报, 让 LLM 继续总结`, tool: 'system' });
1376
+ }
1377
+ continue;
1378
+ }
1379
+ lastQualityScore = this.estimateResponseQuality(reply);
1380
+ finalResponse = this.extractFinalAnswer(reply);
1381
+ break;
1382
+ }
2018
1383
  // 检查是否需要继续循环处理
2019
1384
  // 更严格的判断:只有当回复明确表示需要更多信息时才继续
2020
1385
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
@@ -2060,12 +1425,12 @@ ${toolDefs}
2060
1425
  onStream({ type: 'status', content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2061
1426
  }
2062
1427
  const now = new Date().toISOString();
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>
1428
+ const identityPrefix = `${this.identity.name} | bolloon 智能体
1429
+ <environment_details>
1430
+ Current time: ${now}
1431
+ Working directory: ${this.cwd}
1432
+ Workspace root folder: ${this.cwd}
1433
+ </environment_details>
2069
1434
  `;
2070
1435
  finalResponse = identityPrefix + finalResponse;
2071
1436
  this.messageHistory.push({ role: 'assistant', content: finalResponse });
@@ -2157,18 +1522,45 @@ Workspace root folder: ${this.cwd}
2157
1522
  for (const m of recentMessages) {
2158
1523
  const role = m.role;
2159
1524
  let content = m.content;
2160
- // tool role: toolResult 序列化 ( buildContext 一样)
1525
+ // 2026-06-30 修: OpenAI 协议 tool role 必须带 tool_call_id, 否则 minimax (OpenAI-compatible) 400
1526
+ // bolloon 之前把所有 tool result 包成 "[工具结果] ..." 当 user/assistant role 发, minimax 严格校验失败
1527
+ // 现在: 保留 role='tool' + 加 tool_call_id 字段 (用 messageHistory 里自己生成的 id)
2161
1528
  if (role === 'tool') {
2162
1529
  const result = m.toolResult ? JSON.stringify(m.toolResult) : content;
2163
1530
  content = `[工具结果] ${result}`;
1531
+ // MiniMax 等 API 不支持 tool role, 转为 user role
1532
+ out.push({ role: 'user', content });
1533
+ continue;
2164
1534
  }
2165
1535
  // system role (router hint 等) 直接保留
2166
1536
  if (role === 'system') {
2167
1537
  out.push({ role: 'system', content });
2168
1538
  continue;
2169
1539
  }
2170
- // assistant / user / tool 直接转
2171
- if (role === 'user' || role === 'assistant' || role === 'tool') {
1540
+ // 2026-06-30 修: assistant 消息如果带 toolCall (bolloon 内部对象), emit OpenAI 协议的 tool_calls 数组
1541
+ // minimax 严格要求 assistant 消息含 tool_calls 字段, 后续 tool result 才能引用 tool_call_id
1542
+ if (role === 'assistant') {
1543
+ const tc = m.toolCall;
1544
+ if (tc && tc.id) {
1545
+ out.push({
1546
+ role: 'assistant',
1547
+ content: content || '',
1548
+ tool_calls: [{
1549
+ id: tc.id,
1550
+ type: 'function',
1551
+ function: {
1552
+ name: tc.name,
1553
+ arguments: JSON.stringify(tc.args || {}),
1554
+ },
1555
+ }],
1556
+ });
1557
+ }
1558
+ else {
1559
+ out.push({ role, content });
1560
+ }
1561
+ continue;
1562
+ }
1563
+ if (role === 'user') {
2172
1564
  out.push({ role, content });
2173
1565
  }
2174
1566
  }
@@ -2255,7 +1647,8 @@ Workspace root folder: ${this.cwd}
2255
1647
  // M3.5 (2026-06-17): 传 messages 数组 (如果 contextOrMessages 是数组) 或字符串
2256
1648
  // 数组版让 LLM 看到结构化的 user/assistant/tool role, 而不是把 history 拼成单字符串
2257
1649
  const response = await llm.chat(contextOrMessages, systemPrompt, signal);
2258
- return { reply: response.reply || '' };
1650
+ // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
1651
+ return { reply: response.reply || '', toolCalls: response.toolCalls };
2259
1652
  }
2260
1653
  catch (err) {
2261
1654
  // 用户主动 abort: 不重试, 立即抛
@@ -2381,100 +1774,47 @@ Workspace root folder: ${this.cwd}
2381
1774
  }
2382
1775
  }
2383
1776
  isFinalResponse(content) {
2384
- // 只有明确输出 <final gen> 才认为是最终回答
2385
- return content.includes('<final gen>');
1777
+ // 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
1778
+ return isFinalResponseImpl(content, this._parseCtx());
2386
1779
  }
2387
1780
  extractFinalAnswer(content) {
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;
2397
- }
2398
- else {
2399
- content = content.substring(0, markerIndex).trim();
2400
- }
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;
1781
+ // 抽取实现已挪到 ./parse-tool-call.ts (纯函数, 易测)
1782
+ return extractFinalAnswerImpl(content);
1783
+ }
1784
+ _parseCtx() {
1785
+ return {
1786
+ tools: new Set(Array.from(this.tools.keys())),
1787
+ resolveAlias: (name) => this.resolveToolName(name),
1788
+ };
2409
1789
  }
2410
1790
  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)]));
2438
- }
2439
- }
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(':') || '';
2447
- }
2448
- }
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
- }
2461
- }
2462
- }
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
- }
2471
- }
2472
- if (this.tools.has(name) || this.tools.has(name.replace(/_/g, '_'))) {
2473
- return { name, args };
2474
- }
2475
- }
2476
- }
2477
- return null;
1791
+ // 2026-06-30: 抽到 ./parse-tool-call.ts 作为纯函数 — 这里只构建 ctx 并调用
1792
+ return parseToolCallImpl(content, this._parseCtx());
1793
+ }
1794
+ // [debug-2026-06-19] 临时: 打印 parseToolCall 输入和返回
1795
+ _dbgParseToolCall(content) {
1796
+ const r = this.parseToolCall(content);
1797
+ console.log('[DBG parseToolCall] result:', JSON.stringify(r), 'content head:', JSON.stringify(content.substring(0, 200)));
1798
+ return r;
1799
+ }
1800
+ /**
1801
+ * 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
1802
+
1803
+ /**
1804
+ * 2026-06-19: 工具名大小写不敏感 + Claude Code 风格别名映射
1805
+ * LLM 实际产出 Read/Edit/Write/Bash/Grep/Glob 等大写名 (Claude Code 工具命名)
1806
+ * bolloon 注册的是 read_document / edit_file / write_file / shell_exec / list_files
1807
+ * 返回 this.tools 里的标准名, 或 null 表示未识别
1808
+ */
1809
+ /**
1810
+ * LLM 给的工具名 (可能大小写不一, 或者 Claude Code 风格的别名) 解析为
1811
+ * bolloon 注册的标准工具名.
1812
+ *
1813
+ * 2026-06-30: 委托给 ToolRegistry 模块 — alias 表在 tool-registry.ts 统一维护,
1814
+ * 这里只做 thin wrapper 保留 backward compat (private API 但其它地方可能用).
1815
+ */
1816
+ resolveToolName(name) {
1817
+ return this._toolRegistry.resolve(name);
2478
1818
  }
2479
1819
  estimateResponseQuality(response) {
2480
1820
  let score = 0.5;
@@ -2565,37 +1905,37 @@ Workspace root folder: ${this.cwd}
2565
1905
  getDefaultResponse(input) {
2566
1906
  const operationsRef = PiAgentSession.getOperationsReference();
2567
1907
  if (operationsRef) {
2568
- return `收到了: "${input}"
2569
-
2570
- 我是一个判断力处理智能体,支持自然语言交互。
2571
-
2572
- 可用操作(直接说出即可):
2573
- ${this.extractOperationsFromRef(operationsRef)}
2574
-
2575
- 示例请求:
2576
- - "读取 src/index.ts 文件"
2577
- - "总结一下 README.md"
2578
- - "查看当前连接了哪些节点"
1908
+ return `收到了: "${input}"
1909
+
1910
+ 我是一个判断力处理智能体,支持自然语言交互。
1911
+
1912
+ 可用操作(直接说出即可):
1913
+ ${this.extractOperationsFromRef(operationsRef)}
1914
+
1915
+ 示例请求:
1916
+ - "读取 src/index.ts 文件"
1917
+ - "总结一下 README.md"
1918
+ - "查看当前连接了哪些节点"
2579
1919
  - "向 QmABC... 发送测试消息"`;
2580
1920
  }
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
- - "查看当前连接了哪些节点"
1921
+ return `收到了: "${input}"
1922
+
1923
+ 我是一个判断力处理智能体,支持自然语言交互。
1924
+
1925
+ 可用操作(直接说出即可):
1926
+ - "读取 README.md" - 读取并分析文档
1927
+ - "总结文档" - 总结文档内容
1928
+ - "改进文档,按照X要求" - 改进文档
1929
+ - "查看节点" - 查看已连接的对等节点
1930
+ - "向X发送消息Y" - 向对等节点发送消息
1931
+ - "广播消息X" - 广播消息到所有节点
1932
+ - "查看身份" - 查看当前智能体身份
1933
+ - "查看日志" - 查看最近操作日志
1934
+
1935
+ 示例请求:
1936
+ - "读取 src/index.ts 文件"
1937
+ - "总结一下 README.md"
1938
+ - "查看当前连接了哪些节点"
2599
1939
  - "向 QmABC... 发送测试消息"`;
2600
1940
  }
2601
1941
  extractOperationsFromRef(ref) {
@@ -2620,13 +1960,13 @@ ${this.extractOperationsFromRef(operationsRef)}
2620
1960
  }
2621
1961
  }
2622
1962
  return operationLines.length > 0 ? operationLines.join('\n') :
2623
- ` - "读取 README.md" - 读取并分析文档
2624
- - "总结文档" - 总结文档内容
2625
- - "改进文档,按照X要求" - 改进文档
2626
- - "查看节点" - 查看已连接的对等节点
2627
- - "向X发送消息Y" - 向对等节点发送消息
2628
- - "广播消息X" - 广播消息到所有节点
2629
- - "查看身份" - 查看当前智能体身份
1963
+ ` - "读取 README.md" - 读取并分析文档
1964
+ - "总结文档" - 总结文档内容
1965
+ - "改进文档,按照X要求" - 改进文档
1966
+ - "查看节点" - 查看已连接的对等节点
1967
+ - "向X发送消息Y" - 向对等节点发送消息
1968
+ - "广播消息X" - 广播消息到所有节点
1969
+ - "查看身份" - 查看当前智能体身份
2630
1970
  - "查看日志" - 查看最近操作日志`;
2631
1971
  }
2632
1972
  async suggestRename(messages) {
@@ -2955,98 +2295,5 @@ ${this.extractOperationsFromRef(operationsRef)}
2955
2295
  return [...this.operationLog];
2956
2296
  }
2957
2297
  }
2958
- let sessionInstance = null;
2959
- let lastIdentityDid = null;
2960
- // 独立的 session 实例缓存(用于多 session 支持)
2961
- const independentSessions = new Map();
2962
- export async function createAgentSession(config, forceNew) {
2963
- const incomingDid = config.identityDoc?.did;
2964
- // 如果有独立的 peerId (包含 :),使用它作为 key
2965
- if (config.peerId && config.peerId.includes(':')) {
2966
- const key = config.peerId;
2967
- if (!forceNew && independentSessions.has(key)) {
2968
- console.log(`[createAgentSession] 找到现有独立 session, key=${key}`);
2969
- return independentSessions.get(key);
2970
- }
2971
- const session = new PiAgentSession(config);
2972
- independentSessions.set(key, session);
2973
- console.log(`[createAgentSession] 创建独立 session, key=${key}, DID=${incomingDid}`);
2974
- return session;
2975
- }
2976
- // 如果指定了 forceNew 但没有 peerId,生成带时间戳的 key
2977
- if (forceNew) {
2978
- const key = `force:${Date.now()}`;
2979
- const session = new PiAgentSession(config);
2980
- independentSessions.set(key, session);
2981
- console.log(`[createAgentSession] 创建强制新 session, key=${key}`);
2982
- return session;
2983
- }
2984
- // 如果有新的 DID,强制重建 session
2985
- if (sessionInstance && lastIdentityDid && incomingDid && lastIdentityDid !== incomingDid) {
2986
- console.log(`[createAgentSession] DID 变化 ${lastIdentityDid} -> ${incomingDid},重建 session`);
2987
- sessionInstance = null;
2988
- }
2989
- if (sessionInstance) {
2990
- // 检查是否需要更新 identity
2991
- const currentDid = sessionInstance.getIdentity().did;
2992
- if (incomingDid && currentDid !== incomingDid) {
2993
- console.log(`[createAgentSession] 更新 identity: ${currentDid} -> ${incomingDid}`);
2994
- sessionInstance.updateIdentity({
2995
- did: incomingDid,
2996
- name: config.identityDoc?.name || sessionInstance.getIdentity().name,
2997
- publicKey: config.identityDoc?.publicKey || '',
2998
- createdAt: Date.now()
2999
- });
3000
- }
3001
- return sessionInstance;
3002
- }
3003
- sessionInstance = new PiAgentSession(config);
3004
- lastIdentityDid = config.identityDoc?.did || null;
3005
- console.log(`[createAgentSession] 新建 session, DID=${lastIdentityDid}`);
3006
- return sessionInstance;
3007
- }
3008
- export function getAgentSession() {
3009
- return sessionInstance;
3010
- }
3011
- export function resetAgentSession() {
3012
- sessionInstance = null;
3013
- lastIdentityDid = null;
3014
- }
3015
- /**
3016
- * 自我改进循环: 在沙箱分支上工作, 输出结果给用户审.
3017
- *
3018
- * 不在 PiAgent 实例上的原因: 心跳回调可能没有 agent 实例, 单独函数更易复用.
3019
- *
3020
- * **关键不变量**:
3021
- * 1. AI 不能 push 到 master (shell-guard 黑名单 + git 受保护分支)
3022
- * 2. 改动必须走沙箱分支 (SELF_IMPROVE_BRANCH_PREFIX)
3023
- * 3. 6 小时冷却期 (SELF_IMPROVE_COOLDOWN_MS)
3024
- * 4. 写文件必须经过 shell_exec + 护栏检查
3025
- */
3026
- let lastSelfImproveAt = null;
3027
- export async function runSelfImproveLoop(goal) {
3028
- const cooldownMs = getCooldownMs();
3029
- // 1. 冷却期检查
3030
- if (lastSelfImproveAt && Date.now() - lastSelfImproveAt < cooldownMs) {
3031
- const waitHrs = Math.ceil((cooldownMs - (Date.now() - lastSelfImproveAt)) / 3600000);
3032
- return { success: false, error: `自改冷却中, 还需要约 ${waitHrs} 小时` };
3033
- }
3034
- // 2. 选源分支 + 新分支名
3035
- const sourceBranch = 'master';
3036
- const newBranch = `${getBranchPrefix()}${Date.now()}`;
3037
- console.log(`[self-improve] 启动自改循环, 目标: ${goal}, 新分支: ${newBranch}`);
3038
- // 3. 创建分支
3039
- const r1 = await shellExec('git', ['checkout', sourceBranch]);
3040
- if (!r1.success)
3041
- return { success: false, error: `切换到 ${sourceBranch} 失败: ${r1.error}` };
3042
- const r2 = await shellExec('git', ['checkout', '-b', newBranch]);
3043
- if (!r2.success)
3044
- return { success: false, error: `创建分支失败: ${r2.error}` };
3045
- // 4. 走 task queue: 把"自改"作为一个 task 抛回去, AI 拿到后会用 shell_exec 改
3046
- // 护栏已经阻止所有禁区改动, 这里只负责登记
3047
- lastSelfImproveAt = Date.now();
3048
- return {
3049
- success: true,
3050
- output: `✅ 自改分支已创建: ${newBranch}\n目标: ${goal}\n\n**护栏已激活**:\n - 仅允许白名单命令 (git/npm/tsc/vitest/cat/ls/...)\n - 禁止改 src/agents/pi-sdk.ts, shell-guard.ts, src/heartbeat/, src/network/, src/pi-ecosystem-judgment/, package.json, .env 等\n - 6 小时冷却期\n\nAI 接下来会用 shell_exec 工具改源码. 完成后你会在对话里看到 diff 摘要, 手动 git diff master..${newBranch} 审, 满意再 merge.`
3051
- };
3052
- }
2298
+ // createAgentSession / getAgentSession / resetAgentSession / runSelfImproveLoop
2299
+ // 已抽到 ./pi-sdk-session-factory.ts (2026-07-06), 从顶部 import 并 re-export