@bolloon/bolloon-agent 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +467 -467
  2. package/bin/bolloon-cli.cjs +183 -183
  3. package/bin/bolloon.cjs +0 -0
  4. package/bin/bolloon.js +157 -0
  5. package/dist/agents/agent-manifest-protocol.js +0 -52
  6. package/dist/agents/constraint-layer.js +19 -19
  7. package/dist/agents/pi-sdk.js +287 -1213
  8. package/dist/agents/shell-guard.js +2 -2
  9. package/dist/agents/workflow-pivot-loop.js +2 -81
  10. package/dist/bollharness/src/scripts/deploy-guard.js +8 -8
  11. package/dist/bollharness-integration/context-router-judgment.js +4 -4
  12. package/dist/bollharness-integration/context-router.js +292 -292
  13. package/dist/bollharness-integration/gate-state-machine.js +13 -26
  14. package/dist/bollharness-integration/integration.js +0 -71
  15. package/dist/bollharness-integration/llm-judgment-engine.js +134 -134
  16. package/dist/bollharness-integration/skill-adapter.js +127 -52
  17. package/dist/bootstrap/context-collector.js +3 -6
  18. package/dist/bootstrap/lifecycle-hooks.js +1 -15
  19. package/dist/cli-entry.js +35 -39
  20. package/dist/constraint-runtime/src/_archive_helper.js +9 -0
  21. package/dist/constraint-runtime/src/agent/coordinator.js +48 -0
  22. package/dist/constraint-runtime/src/agent/index.js +1 -0
  23. package/dist/constraint-runtime/src/assistant/index.js +12 -0
  24. package/dist/constraint-runtime/src/bootstrap/index.js +12 -0
  25. package/dist/constraint-runtime/src/bootstrap_graph.js +13 -0
  26. package/dist/constraint-runtime/src/bridge/index.js +12 -0
  27. package/dist/constraint-runtime/src/buddy/index.js +12 -0
  28. package/dist/constraint-runtime/src/cli/index.js +12 -0
  29. package/dist/constraint-runtime/src/command_graph.js +10 -0
  30. package/dist/constraint-runtime/src/commands.js +60 -0
  31. package/dist/constraint-runtime/src/components/index.js +12 -0
  32. package/dist/constraint-runtime/src/constants/index.js +12 -0
  33. package/dist/constraint-runtime/src/constraint/budget.js +22 -0
  34. package/dist/constraint-runtime/src/constraint/index.js +2 -0
  35. package/dist/constraint-runtime/src/constraint/permission.js +20 -0
  36. package/dist/constraint-runtime/src/context.js +30 -0
  37. package/dist/constraint-runtime/src/coordinator/index.js +12 -0
  38. package/dist/constraint-runtime/src/cost_hook.js +4 -0
  39. package/dist/constraint-runtime/src/cost_tracker.js +8 -0
  40. package/dist/constraint-runtime/src/deferred_init.js +10 -0
  41. package/dist/constraint-runtime/src/direct_modes.js +6 -0
  42. package/dist/constraint-runtime/src/dynamic-tool-loader.js +85 -0
  43. package/dist/constraint-runtime/src/entrypoints/index.js +12 -0
  44. package/dist/constraint-runtime/src/execution_registry.js +44 -0
  45. package/dist/constraint-runtime/src/history.js +9 -0
  46. package/dist/constraint-runtime/src/hooks/index.js +12 -0
  47. package/dist/constraint-runtime/src/index.js +26 -0
  48. package/dist/constraint-runtime/src/ink.js +4 -0
  49. package/dist/constraint-runtime/src/keybindings/index.js +12 -0
  50. package/dist/constraint-runtime/src/memdir/index.js +12 -0
  51. package/dist/constraint-runtime/src/migrations/index.js +12 -0
  52. package/dist/constraint-runtime/src/models.js +1 -0
  53. package/dist/constraint-runtime/src/moreright/index.js +12 -0
  54. package/dist/constraint-runtime/src/native_ts/index.js +12 -0
  55. package/dist/constraint-runtime/src/output_styles/index.js +12 -0
  56. package/dist/constraint-runtime/src/parity_audit.js +12 -0
  57. package/dist/constraint-runtime/src/plugins/index.js +12 -0
  58. package/dist/constraint-runtime/src/port_manifest.js +11 -0
  59. package/dist/constraint-runtime/src/prefetch.js +9 -0
  60. package/dist/constraint-runtime/src/query.js +1 -0
  61. package/dist/constraint-runtime/src/remote/index.js +12 -0
  62. package/dist/constraint-runtime/src/remote_runtime.js +9 -0
  63. package/dist/constraint-runtime/src/runtime/index.js +1 -0
  64. package/dist/constraint-runtime/src/runtime/session.js +35 -0
  65. package/dist/constraint-runtime/src/schemas/index.js +12 -0
  66. package/dist/constraint-runtime/src/screens/index.js +12 -0
  67. package/dist/constraint-runtime/src/server/index.js +12 -0
  68. package/dist/constraint-runtime/src/services/index.js +12 -0
  69. package/dist/constraint-runtime/src/session_store.js +22 -0
  70. package/dist/constraint-runtime/src/setup.js +30 -0
  71. package/dist/constraint-runtime/src/skills/index.js +1 -0
  72. package/dist/constraint-runtime/src/skills/skill-registry.js +28 -0
  73. package/dist/constraint-runtime/src/state/index.js +12 -0
  74. package/dist/constraint-runtime/src/system_init.js +20 -0
  75. package/dist/constraint-runtime/src/thinking/engine.js +42 -0
  76. package/dist/constraint-runtime/src/thinking/index.js +1 -0
  77. package/dist/constraint-runtime/src/tool_pool.js +8 -0
  78. package/dist/constraint-runtime/src/tools/OpenCLI/execAdapter.js +7 -0
  79. package/dist/constraint-runtime/src/tools/OpenCLI/listAdapters.js +7 -0
  80. package/dist/constraint-runtime/src/tools/OpenCLI/runCommand.js +7 -0
  81. package/dist/constraint-runtime/src/tools/SafeSDK/confirmTransaction.js +6 -0
  82. package/dist/constraint-runtime/src/tools/SafeSDK/createTransaction.js +8 -0
  83. package/dist/constraint-runtime/src/tools/SafeSDK/executeTransaction.js +6 -0
  84. package/dist/constraint-runtime/src/tools/SafeSDK/getBalance.js +6 -0
  85. package/dist/constraint-runtime/src/tools/SafeSDK/getPendingTransactions.js +6 -0
  86. package/dist/constraint-runtime/src/tools/SafeSDK/proposeTransaction.js +6 -0
  87. package/dist/constraint-runtime/src/tools.js +80 -0
  88. package/dist/constraint-runtime/src/transcript.js +19 -0
  89. package/dist/constraint-runtime/src/types/index.js +12 -0
  90. package/dist/constraint-runtime/src/upstream_proxy/index.js +12 -0
  91. package/dist/constraint-runtime/src/utils/index.js +12 -0
  92. package/dist/constraint-runtime/src/vim/index.js +12 -0
  93. package/dist/constraint-runtime/src/voice/index.js +12 -0
  94. package/dist/constraint-runtime/tests/agent.test.js +16 -0
  95. package/dist/constraint-runtime/tests/constraint.test.js +41 -0
  96. package/dist/constraint-runtime/tests/skill.test.js +19 -0
  97. package/dist/constraint-runtime/tests/thinking.test.js +22 -0
  98. package/dist/context-compaction/auto-compact.js +7 -7
  99. package/dist/electron-preload.js +1 -18
  100. package/dist/electron-preload.js.map +1 -1
  101. package/dist/electron.js +168 -3
  102. package/dist/electron.js.map +1 -1
  103. package/dist/index.js +116 -136
  104. package/dist/llm/config-store.js +8 -24
  105. package/dist/llm/llm-judgment-client.js +102 -102
  106. package/dist/llm/pi-ai.js +85 -126
  107. package/dist/llm/system-prompt/layers/channel/local.md +14 -14
  108. package/dist/llm/system-prompt/layers/channel/p2p-agent.md +18 -18
  109. package/dist/llm/system-prompt/layers/channel/p2p-visitor.md +19 -19
  110. package/dist/llm/system-prompt/layers/core/artifacts_storage.md +89 -89
  111. package/dist/llm/system-prompt/layers/core/evenhandedness.md +21 -21
  112. package/dist/llm/system-prompt/layers/core/hibs_reminders.md +15 -15
  113. package/dist/llm/system-prompt/layers/core/identity.md +37 -37
  114. package/dist/llm/system-prompt/layers/core/knowledge.md +17 -17
  115. package/dist/llm/system-prompt/layers/core/memory_system.md +12 -12
  116. package/dist/llm/system-prompt/layers/core/network_filesystem.md +28 -28
  117. package/dist/llm/system-prompt/layers/core/refusal.md +37 -37
  118. package/dist/llm/system-prompt/layers/core/tone.md +31 -31
  119. package/dist/llm/system-prompt/layers/core/tools.thin.md +13 -13
  120. package/dist/llm/system-prompt/layers/core/wellbeing.md +41 -41
  121. package/dist/llm/system-prompt/layers/role/architect.md +20 -20
  122. package/dist/llm/system-prompt/layers/role/expert.md +19 -19
  123. package/dist/llm/system-prompt/layers/role/implementer.md +15 -15
  124. package/dist/llm/system-prompt/layers/role/security.md +15 -15
  125. package/dist/llm/system-prompt/layers/tool/artifacts.md +72 -72
  126. package/dist/llm/system-prompt/layers/tool/bash.md +25 -25
  127. package/dist/llm/system-prompt/layers/tool/hibs_api.md +171 -171
  128. package/dist/llm/system-prompt/layers/tool/image_search.md +70 -70
  129. package/dist/llm/system-prompt/layers/tool/manifest.md +89 -89
  130. package/dist/llm/system-prompt/layers/tool/mcp_apps.md +53 -53
  131. package/dist/llm/system-prompt/layers/tool/web_search.md +83 -83
  132. package/dist/llm/tool-manifest/ask_user_input.js +1 -1
  133. package/dist/llm/tool-manifest/bash.js +3 -3
  134. package/dist/llm/tool-manifest/create_file.js +4 -4
  135. package/dist/llm/tool-manifest/fetch_sports_data.js +3 -3
  136. package/dist/llm/tool-manifest/image_search.js +2 -2
  137. package/dist/llm/tool-manifest/mcp.js +2 -2
  138. package/dist/llm/tool-manifest/message_compose.js +3 -3
  139. package/dist/llm/tool-manifest/places.js +2 -2
  140. package/dist/llm/tool-manifest/present_files.js +1 -1
  141. package/dist/llm/tool-manifest/recipe.js +2 -2
  142. package/dist/llm/tool-manifest/str_replace.js +5 -5
  143. package/dist/llm/tool-manifest/view.js +3 -3
  144. package/dist/llm/tool-manifest/weather.js +4 -4
  145. package/dist/llm/tool-manifest/web.js +4 -4
  146. package/dist/pi-ecosystem-colony/index.js +365 -0
  147. package/dist/pi-ecosystem-judgment/causal-judge.js +51 -51
  148. package/dist/pi-ecosystem-judgment/distill-prompt.js +42 -42
  149. package/dist/pi-ecosystem-judgment/distillation.js +14 -14
  150. package/dist/pi-ecosystem-judgment/monitor-gate.js +28 -28
  151. package/dist/pi-ecosystem-judgment/value-injection.js +102 -102
  152. package/dist/security/context-router-tool.js +15 -15
  153. package/dist/security/tool-gate.js +0 -11
  154. package/dist/social/ant-colony/AdaptiveHeartbeat.js +101 -0
  155. package/dist/social/ant-colony/PheromoneEngine.js +227 -0
  156. package/dist/social/ant-colony/index.js +6 -0
  157. package/dist/social/ant-colony/types.js +24 -0
  158. package/dist/test/ai-judgment-test.js +80 -0
  159. package/dist/test/bollharness-integration.test.js +318 -0
  160. package/dist/test/channel-agent-multi-dialogue.js +205 -0
  161. package/dist/test/channel-heartbeat-agent-test.js +201 -0
  162. package/dist/test/constraint-layer.test.js +164 -0
  163. package/dist/test/diap-identity-test.js +172 -0
  164. package/dist/test/diap-quick-test.js +62 -0
  165. package/dist/test/global-shared-context.test.js +315 -0
  166. package/dist/test/harness-judgment-injection.test.js +246 -0
  167. package/dist/test/harness-workflow-integrator-test.js +228 -0
  168. package/dist/test/human-value-store.test.js +243 -0
  169. package/dist/test/hybrid-integration-test.js +118 -0
  170. package/dist/test/hybrid-messenger-verify.js +55 -0
  171. package/dist/test/iroh-bistream-debug.js +38 -0
  172. package/dist/test/iroh-communication.test.js +66 -0
  173. package/dist/test/iroh-debug-test.js +57 -0
  174. package/dist/test/iroh-diap-test.js +71 -0
  175. package/dist/test/iroh-direct-connect.js +55 -0
  176. package/dist/test/iroh-e2e-fixed.js +89 -0
  177. package/dist/test/iroh-e2e-same-process.js +63 -0
  178. package/dist/test/iroh-e2e.js +66 -0
  179. package/dist/test/iroh-final-e2e.js +72 -0
  180. package/dist/test/iroh-relay-test.js +37 -0
  181. package/dist/test/iroh-simple-test.js +41 -0
  182. package/dist/test/iroh-transport-verify.js +54 -0
  183. package/dist/test/iroh-transport.test.js +37 -0
  184. package/dist/test/iroh-two-nodes.js +70 -0
  185. package/dist/test/iroh-verify.js +44 -0
  186. package/dist/test/judgment-decision.test.js +219 -0
  187. package/dist/test/llm-judgment-integration.test.js +220 -0
  188. package/dist/test/p2p-agent-complex-dialogue.js +385 -0
  189. package/dist/test/p2p-agent-dialogue.js +341 -0
  190. package/dist/test/p2p-agent-full-bidirectional.js +510 -0
  191. package/dist/test/p2p-agent-harness-flow.js +437 -0
  192. package/dist/test/p2p-agent-harness-single.js +143 -0
  193. package/dist/test/p2p-ai-dialogue-test.js +318 -0
  194. package/dist/test/p2p-cid-connect-test.js +195 -0
  195. package/dist/test/p2p-connect-receiver.js +69 -0
  196. package/dist/test/p2p-doc-transfer.js +110 -0
  197. package/dist/test/p2p-identity-page-test.js +77 -0
  198. package/dist/test/p2p-iroh-test.js +171 -0
  199. package/dist/test/p2p-minimal-test.js +241 -0
  200. package/dist/test/p2p-node-1.js +148 -0
  201. package/dist/test/p2p-node-2.js +148 -0
  202. package/dist/test/p2p-server.js +281 -0
  203. package/dist/test/p2p-two-nodes-test.js +438 -0
  204. package/dist/test/pi-sdk.test.js +44 -0
  205. package/dist/test/set-persona.js +40 -0
  206. package/dist/test/simple.test.js +9 -0
  207. package/dist/test/storage-integration.test.js +150 -0
  208. package/dist/test/subagent-manager.test.js +276 -0
  209. package/dist/test/test-gate-flow.test.js +81 -0
  210. package/dist/test/workflow-engine.test.js +87 -0
  211. package/dist/test/workflow-pivot-loop.test.js +246 -0
  212. package/dist/web/api-config.html +520 -520
  213. package/dist/web/client.js +2917 -3791
  214. package/dist/web/components/p2p/index.js +234 -276
  215. package/dist/web/components/wallet-viem.mjs +118 -118
  216. package/dist/web/index.html +374 -373
  217. package/dist/web/manifest.json +20 -20
  218. package/dist/web/server.js +23 -775
  219. package/dist/web/style.css +4737 -4737
  220. package/dist/web/ui/message-renderer.js +373 -451
  221. package/dist/web/ui/step-timeline.js +255 -351
  222. package/package.json +160 -157
  223. package/scripts/build-cli.js +216 -0
  224. package/scripts/build-web.ts +125 -0
  225. package/scripts/postinstall.js +153 -0
  226. package/bin/bolloon-daemon.sh +0 -207
  227. package/bin/ipfs +0 -0
  228. package/dist/agents/chat-segmenter.js +0 -298
  229. package/dist/agents/judgment-protocol.js +0 -479
  230. package/dist/agents/parse-tool-call.js +0 -304
  231. package/dist/agents/peer-manifest-loader.js +0 -210
  232. package/dist/agents/react-loop.js +0 -120
  233. package/dist/agents/session-store.js +0 -171
  234. package/dist/agents/tool-registry.js +0 -136
  235. package/dist/bootstrap/chat-archiver.js +0 -276
  236. package/dist/bootstrap/memory-compressor.js +0 -170
  237. package/dist/bootstrap/persona-loader.js +0 -94
  238. package/dist/electron/config.js +0 -16
  239. package/dist/electron/config.js.map +0 -1
  240. package/dist/electron/dialogs.js +0 -71
  241. package/dist/electron/dialogs.js.map +0 -1
  242. package/dist/electron/first-run.js +0 -129
  243. package/dist/electron/first-run.js.map +0 -1
  244. package/dist/electron/ipc.js +0 -16
  245. package/dist/electron/ipc.js.map +0 -1
  246. package/dist/electron/logger.js +0 -77
  247. package/dist/electron/logger.js.map +0 -1
  248. package/dist/electron/main.js +0 -60
  249. package/dist/electron/main.js.map +0 -1
  250. package/dist/electron/menu.js +0 -140
  251. package/dist/electron/menu.js.map +0 -1
  252. package/dist/electron/paths.js +0 -33
  253. package/dist/electron/paths.js.map +0 -1
  254. package/dist/electron/server.js +0 -74
  255. package/dist/electron/server.js.map +0 -1
  256. package/dist/electron/tray.js +0 -73
  257. package/dist/electron/tray.js.map +0 -1
  258. package/dist/electron/window.js +0 -69
  259. package/dist/electron/window.js.map +0 -1
  260. package/dist/network/local-inbox-bus.js +0 -73
  261. package/dist/network/p2p-outbox.js +0 -161
  262. package/dist/network/peer-fs.js +0 -420
  263. package/dist/network/peer-resource-bridge.js +0 -216
  264. package/dist/web/components/p2p/P2PModal.js +0 -188
  265. package/dist/web/components/p2p/p2p-modal.js +0 -664
  266. package/dist/web/components/p2p/p2p-tools.js +0 -248
  267. package/dist/web/input-validator.js +0 -103
  268. package/dist/web/util/safe-name.js +0 -32
@@ -1,304 +0,0 @@
1
- /**
2
- * parseToolCall — 从 LLM 输出 (minimax / Hermes / Qwen / GLM / Anthropic 等)
3
- * 中提取 tool_call { name, args }.
4
- *
5
- * 2026-06-30 抽出: 原先作为 PiAgentSession 的 private 方法藏在 4522 行类里,
6
- * 测试只能复刻逻辑不能 import 真源 = 测试通过 ≠ 实际工作. 现在抽出为纯函数,
7
- * 接受 ctx.tools (可用工具名集合) + 可选 ctx.resolveAlias (别名 → 真实工具名).
8
- *
9
- * 用法:
10
- * import { parseToolCall } from './parse-tool-call';
11
- * const r = parseToolCall(content, { tools: new Set(['shell_exec', 'read_file']) });
12
- * if (r) tool.execute(r.args);
13
- */
14
- const SHELL_KEYWORDS = ['git', 'npx', 'npm', 'tsx', 'tsc', 'vitest', 'node', 'mkdir', 'touch', 'ls', 'echo', 'cat', 'head', 'tail', 'wc', 'pwd', 'date'];
15
- /**
16
- * 默认的 alias resolver — Claude Code 风格 → bolloon 工具名
17
- * 当 ctx.resolveAlias 缺失时使用.
18
- */
19
- function defaultResolveAlias(name, tools) {
20
- if (tools.has(name))
21
- return name;
22
- const lower = name.toLowerCase();
23
- const aliasMap = {
24
- read: 'read_file',
25
- edit: 'edit_file',
26
- write: 'write_file',
27
- rm: 'delete_file',
28
- mv: 'move_file',
29
- bash: 'shell_exec',
30
- shell: 'shell_exec',
31
- sh: 'shell_exec',
32
- cat: 'read_file',
33
- test: 'vitest_run',
34
- vitest: 'vitest_run',
35
- typecheck: 'tsc_check',
36
- tsc: 'tsc_check',
37
- log: 'git_log',
38
- show: 'git_show',
39
- diff: 'git_diff',
40
- commit: 'git_commit',
41
- push: 'git_push',
42
- branch: 'git_branch',
43
- checkout: 'git_branch',
44
- stash: 'git_stash',
45
- todo_write: 'create_task',
46
- todowrite: 'create_task',
47
- task: 'create_task',
48
- };
49
- const aliased = aliasMap[lower];
50
- if (aliased && tools.has(aliased))
51
- return aliased;
52
- if (tools.has(lower))
53
- return lower;
54
- return null;
55
- }
56
- /** 拆分 args.command: "git status" → { command: "git", args: "status" } */
57
- function autoSplitCommand(args) {
58
- if (typeof args.command === 'string' && args.command.includes(' ') && !args.args) {
59
- const parts = args.command.split(/\s+/);
60
- args.command = parts[0];
61
- args.args = parts.slice(1).join(' ');
62
- }
63
- }
64
- /** 用 resolver 解析工具名 — 接受任意 resolver (默认或自定义). */
65
- function resolve(ctx, name) {
66
- if (ctx.resolveAlias)
67
- return ctx.resolveAlias(name);
68
- return defaultResolveAlias(name, ctx.tools);
69
- }
70
- export function parseToolCall(content, ctx) {
71
- if (!content)
72
- return null;
73
- // === 0. 剥离 思考块 — 必须先做, 否则旧 regex `<(\w+)>...</\1>` 会先匹配 想想标签 ===
74
- const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, '');
75
- // === 1. JSON function-call (OpenAI / Anthropic / Minimax-style) ===
76
- const jsonPatterns = [
77
- // markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
78
- /(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/,
79
- ];
80
- for (const p of jsonPatterns) {
81
- const m = content.match(p);
82
- if (m) {
83
- const name = m[1];
84
- let args = {};
85
- try {
86
- const parsed = JSON.parse(m[2]);
87
- if (parsed && typeof parsed === 'object') {
88
- args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
89
- }
90
- }
91
- catch {
92
- /* 解析失败保持 args={} */
93
- }
94
- autoSplitCommand(args);
95
- const resolved = resolve(ctx, name);
96
- if (resolved) {
97
- return { name: resolved, args };
98
- }
99
- }
100
- }
101
- // === 2. <tools:call name="X">...</tools:call> 嵌套格式 ===
102
- const toolsCallMatch = content.match(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/);
103
- if (toolsCallMatch) {
104
- const name = toolsCallMatch[1];
105
- const inner = toolsCallMatch[2];
106
- const args = {};
107
- const argTags = inner.matchAll(/<tools:call\s+name=["'](\w+)["']>([\s\S]*?)<\/tools:call>/g);
108
- for (const m of argTags) {
109
- args[m[1]] = m[2].trim();
110
- }
111
- const resolved = resolve(ctx, name);
112
- if (resolved) {
113
- return { name: resolved, args };
114
- }
115
- // 兜底: 外层 tool name 不在 tools, 但内层有 command, 推断 shell_exec
116
- if (args.command) {
117
- const cmdFirst = args.command.split(/\s+/)[0];
118
- if (SHELL_KEYWORDS.includes(cmdFirst)) {
119
- return { name: 'shell_exec', args };
120
- }
121
- }
122
- }
123
- // === 3. XML 格式 (陆续检测多个 pattern) ===
124
- const patterns = [
125
- // minimax/Hermes 自闭合 XML 格式 <invoke name="X">...</invoke>
126
- new RegExp('<invoke\\s+name=["\']([\\w]+)["\']>([\\s\\S]*?)</invoke>'),
127
- // <function_calls> 包裹
128
- new RegExp('<function_calls>[\\s\\S]*?<invoke\\s+name=["\']([\\w]+)["\']>([\\s\\S]*?)</invoke>[\\s\\S]*?</function_calls>'),
129
- // <function_calls><tool name="X"><param name="Y">value</param></tool></function_calls>
130
- new RegExp('<function_calls>[\\s\\S]*?<tool\\s+name=["\']([\\w]+)["\']>([\\s\\S]*?)</tool>[\\s\\S]*?</function_calls>'),
131
- /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
132
- /使用工具[::]\s*(\w+)\s*\(([^)]*)\)/,
133
- /tool[_\w]*[::]\s*(\w+)\s*\(([^)]*)\)/i,
134
- /(\w+)\s*\(\s*([^)]*)\s*\)/,
135
- // 对象字面量格式 {tool => "get_identity", args => {...}}
136
- /\{\s*tool\s*=>\s*["'](\w+)["']\s*(?:,\s*args\s*=>\s*(\{[\s\S]*?\}))?\s*\}/,
137
- // tool => "get_identity" (无 args 包裹)
138
- /\btool\s*=>\s*["'](\w+)["']/,
139
- // [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
140
- /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
141
- // "tool_name {json_args}" 形式 (单行, 无 name 字段)
142
- /(?:^|\n)(\w+)\s+(\{[\s\S]*?\})(?=\n|$)/,
143
- // XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
144
- /<(\w+)>([\s\S]*?)<\/\1>/,
145
- ];
146
- for (const pattern of patterns) {
147
- const match = strippedContent.match(pattern);
148
- if (!match)
149
- continue;
150
- const name = match[1];
151
- let args = {};
152
- const rawArgs = match[2] || '';
153
- if (rawArgs && rawArgs.trim().startsWith('{')) {
154
- // JSON 形式, 尝试解析
155
- try {
156
- const parsed = JSON.parse(rawArgs);
157
- if (parsed && typeof parsed === 'object') {
158
- args = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
159
- }
160
- }
161
- catch {
162
- // 解析失败就当字符串处理
163
- const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
164
- for (const pair of argPairs) {
165
- const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
166
- if (key)
167
- args[key] = valueParts.join(':') || '';
168
- }
169
- }
170
- }
171
- else if (rawArgs && /<[\w]/.test(rawArgs) && /<\/\w+>/.test(rawArgs)) {
172
- // 2026-06-30 修: 检测 XML 子标签 (含带属性的 <parameter name="X"> 形式)
173
- // 原文用 /<\w+>[\s\S]*<\/\w+>/ 检测, 但是 <parameter name="command"> 形式
174
- // 首标签是 <parameter, 不是简单的 <command 所以原检测会 false,
175
- // 路由错误走 key:value 路径. 这里放宽到任何 <...>...content...</...>
176
- const paramRe = /<parameter\s+name=["'](\w+)["']>([\s\S]*?)<\/parameter>/g;
177
- const paramReShort = /<param\s+name=["'](\w+)["']>([\s\S]*?)<\/param>/g;
178
- let pMatch;
179
- while ((pMatch = paramRe.exec(rawArgs)) !== null) {
180
- const argName = pMatch[1];
181
- const argValue = pMatch[2].trim();
182
- if (argName && argValue) {
183
- args[argName] = argValue;
184
- }
185
- }
186
- if (Object.keys(args).length === 0) {
187
- let sMatch;
188
- paramReShort.lastIndex = 0;
189
- while ((sMatch = paramReShort.exec(rawArgs)) !== null) {
190
- const argName = sMatch[1];
191
- const argValue = sMatch[2].trim().replace(/^["']|["']$/g, '');
192
- if (argName && argValue) {
193
- args[argName] = argValue;
194
- }
195
- }
196
- }
197
- if (Object.keys(args).length === 0) {
198
- const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
199
- let xmlMatch;
200
- while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
201
- const argName = xmlMatch[1];
202
- const argValue = xmlMatch[2].trim();
203
- if (argName && argValue) {
204
- args[argName] = argValue;
205
- }
206
- }
207
- }
208
- }
209
- else if (rawArgs) {
210
- // 形参串 key: value
211
- const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
212
- for (const pair of argPairs) {
213
- const [key, ...valueParts] = pair.split(':').map(s => s.trim().replace(/['"]/g, ''));
214
- if (key)
215
- args[key] = valueParts.join(':') || '';
216
- }
217
- }
218
- // 名字 resolve: 优先 this.tools.has, 然后 alias resolver
219
- const resolved = ctx.tools.has(name) ? name : resolve(ctx, name);
220
- if (resolved) {
221
- autoSplitCommand(args);
222
- return { name: resolved, args };
223
- }
224
- // 名字未识别但 rawArgs 含 <command> 子标签 → 跳出, 走 fallback 按 command 推断 shell_exec
225
- if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(strippedContent)) {
226
- break;
227
- }
228
- }
229
- // === 4. Fallback: 任意 <tag><command>...</command></tag> 按 cmdFirst 推断 shell_exec ===
230
- const xmlTagMatch = strippedContent.match(/<(\w+)>([\s\S]*?)<\/\1>/);
231
- if (xmlTagMatch) {
232
- const outerTag = xmlTagMatch[1];
233
- const inner = xmlTagMatch[2];
234
- if (!resolve(ctx, outerTag)) {
235
- const cmdMatch = inner.match(/<command>([\s\S]*?)<\/command>/);
236
- if (cmdMatch) {
237
- const cmd = cmdMatch[1].trim();
238
- const cmdFirst = cmd.split(/\s+/)[0];
239
- if (SHELL_KEYWORDS.includes(cmdFirst)) {
240
- const args = {};
241
- const remaining = cmd.slice(cmdFirst.length).trim();
242
- if (remaining) {
243
- args.command = cmdFirst;
244
- args.args = remaining;
245
- }
246
- else {
247
- args.command = cmd;
248
- }
249
- const argsM = inner.match(/<args>([\s\S]*?)<\/args>/);
250
- if (argsM && argsM[1].trim()) {
251
- args.args = argsM[1].trim();
252
- }
253
- return { name: 'shell_exec', args };
254
- }
255
- }
256
- }
257
- }
258
- return null;
259
- }
260
- /**
261
- * isFinalResponse — 临时判断 LLM 是否已完结 (含 <final gen>)
262
- * 2026-06-19/06-30 修: 先剥离 think, 避免思考块中提及 <final gen> 误杀
263
- * 含可解析 tool_call 则优先执行工具, 不算 final
264
- */
265
- export function isFinalResponse(content, ctx) {
266
- const stripped = content.replace(/<think[\s\S]*?<\/think/g, '');
267
- if (!stripped.includes('<final gen>'))
268
- return false;
269
- // 用默认 ctx (无需 tool 名解析, 只看能否 parse 出来) - 但 isFinalResponse 不需要 tools 列表上下文.
270
- // 如果没有传 ctx, 使用 empty set (只解析不 resolve).
271
- if (ctx && parseToolCall(stripped, ctx))
272
- return false;
273
- if (!ctx) {
274
- // 试探性 parse (不 resolve name) — 如果至少能提取 name+args 模式, 就不是 final.
275
- // 这里粗略检测: stripped 中含 json "name": 或 <invoke name= 或 <function_calls> 任一
276
- if (/\{[\s\S]*?"name"\s*:/.test(stripped))
277
- return false;
278
- if (/<invoke\s+name=/.test(stripped))
279
- return false;
280
- if (/<function_calls>/.test(stripped))
281
- return false;
282
- }
283
- return true;
284
- }
285
- /** extractFinalAnswer: 去除 <final gen> 标记 + tool call 噪声. */
286
- export function extractFinalAnswer(content) {
287
- const marker = '<final gen>';
288
- const markerIndex = content.indexOf(marker);
289
- if (markerIndex !== -1) {
290
- const after = content.substring(markerIndex + marker.length).trim();
291
- if (after) {
292
- content = after;
293
- }
294
- else {
295
- content = content.substring(0, markerIndex).trim();
296
- }
297
- }
298
- let cleaned = content
299
- .replace(/调用工具[::]\s*\w+\s*\([^)]*\)/g, '')
300
- .replace(/使用工具[::]\s*\w+\s*\([^)]*\)/g, '')
301
- .replace(/tool[_\w]*[::]\s*\w+\s*\([^)]*\)/gi, '')
302
- .trim();
303
- return cleaned;
304
- }
@@ -1,210 +0,0 @@
1
- /**
2
- * peer-manifest-loader.ts — 智能体相互了解的懒加载触发器
3
- *
4
- * 设计目的 (2026-07-05):
5
- * 远程 P2P 节点的 manifest 不默认进 LLM prompt, 只在需要时拉取.
6
- * 3 个触发点:
7
- * ① @-mention 远端 channel → 立即拉对方 manifest + 对应 agent 详细描述
8
- * ② 本地 agent 连续失败 → 兜底拉对方 manifest (对方可能有解)
9
- * ③ 关键词触发 → 用户说 "持续协助" / "cooperate" 时加载
10
- *
11
- * 加载结果拼成 prompt 附加块 (≤2000 字符), 拼到 system prompt 尾部.
12
- * 不入持久化 prompt, 不污染主对话.
13
- */
14
- import * as fs from 'fs/promises';
15
- import * as peerFs from '../network/peer-fs.js';
16
- // ============== 主入口 ==============
17
- /**
18
- * 加载对方的 manifest + (可选) 详细 agent 描述, 拼成 prompt 块.
19
- * 行为:
20
- * 1) 先看本地 ~/.bolloon/peers/<pk>/capability-index.md 缓存 — 命中且新 (≤1h) → 直接用
21
- * 2) 否则调 RPC 拉 manifest → 落盘 → 读 capability-index.md
22
- * 3) 如果 triggerValue 是 agent id 或 capability 名, 再发 RPC 拉详细描述
23
- */
24
- export async function loadPeerManifest(ctx, opts) {
25
- const t0 = Date.now();
26
- const pk = ctx.remotePublicKey;
27
- if (!pk || pk === opts.p2p.getPublicKey())
28
- return null; // 自己
29
- let rpcTriggered = false;
30
- let idx = await peerFs.readPeerIndex(pk);
31
- let capabilityIndex = await peerFs.readCapabilityIndex(pk);
32
- // 缓存判断: capability-index 存在 + 索引在 TTL 内 → 跳过 RPC
33
- const ttl = opts.cacheTtlMs ?? 60 * 60 * 1000;
34
- if (!idx || !capabilityIndex || (idx.updatedAt && (Date.now() - new Date(idx.updatedAt).getTime()) > ttl)) {
35
- // 缓存过期或不完整, 发 RPC 拉新
36
- const since = idx?.manifestTs || 0;
37
- const req = JSON.stringify({
38
- v: 3, op: 'agent.manifest.exchange',
39
- payload: { since, fromPublicKey: opts.p2p.getPublicKey() }
40
- });
41
- const r = await opts.p2p.sendToWithWait(pk, req, 3000);
42
- if (r === 'SENT') {
43
- rpcTriggered = true;
44
- // 等异步处理落盘 — 这里 sleep 短时间, 不阻塞主线程太久
45
- await new Promise(r => setTimeout(r, 500));
46
- idx = await peerFs.readPeerIndex(pk);
47
- capabilityIndex = await peerFs.readCapabilityIndex(pk);
48
- }
49
- }
50
- if (!idx) {
51
- return {
52
- publicKey: pk,
53
- capabilityIndex: '',
54
- promptBlock: `[远端 peer ${pk.slice(0, 12)}…] 暂无 manifest 缓存 (对方未响应或本地首次连接).`,
55
- agentDescriptions: [],
56
- rpcTriggered,
57
- durationMs: Date.now() - t0,
58
- };
59
- }
60
- // 按 triggerValue 决定要不要拉详细 agent 描述
61
- const agentDescriptions = [];
62
- const lowerVal = ctx.triggerValue.toLowerCase();
63
- // 1) 如果 triggerValue 看起来是 agent id (有特殊前缀/格式) → 直接拉
64
- // 2) 如果是 capability 名 (编程/翻译/...) → 在 idx 里找匹配 capability 的 agent → 拉
65
- const candidates = [];
66
- if (ctx.triggerValue.match(/^(agent_|agt_|bot_|assist_|task_)/)) {
67
- candidates.push(ctx.triggerValue);
68
- }
69
- else {
70
- for (const a of idx.agents) {
71
- if (a.capabilities.some(c => c.toLowerCase().includes(lowerVal))) {
72
- candidates.push(a.id);
73
- }
74
- }
75
- }
76
- // 拉详细描述 (限 3 个, 避免 prompt 爆炸)
77
- for (const agentId of candidates.slice(0, 3)) {
78
- const localFile = peerFs.getPeerAgentMdPath(pk, agentId);
79
- let body = '';
80
- try {
81
- body = await fs.readFile(localFile, 'utf-8');
82
- }
83
- catch { }
84
- if (!body) {
85
- // 本地没缓存, 拉 RPC
86
- const req = JSON.stringify({
87
- v: 3, op: 'agent.resource.get',
88
- payload: { agentId, fromPublicKey: opts.p2p.getPublicKey() }
89
- });
90
- const r = await opts.p2p.sendToWithWait(pk, req, 3000);
91
- if (r === 'SENT') {
92
- rpcTriggered = true;
93
- // 异步处理, 等 300ms
94
- await new Promise(r => setTimeout(r, 300));
95
- try {
96
- body = await fs.readFile(localFile, 'utf-8');
97
- }
98
- catch { }
99
- }
100
- }
101
- if (body)
102
- agentDescriptions.push({ agentId, body: body.slice(0, 1500) });
103
- }
104
- // 拼 prompt 附加块
105
- const block = buildPromptBlock(ctx, capabilityIndex || '(无索引)', agentDescriptions, idx);
106
- return {
107
- publicKey: pk,
108
- capabilityIndex: capabilityIndex || '',
109
- promptBlock: block,
110
- agentDescriptions,
111
- rpcTriggered,
112
- durationMs: Date.now() - t0,
113
- };
114
- }
115
- /**
116
- * 把加载结果拼成可注入 system prompt 的文本块 (≤2000 字)
117
- */
118
- function buildPromptBlock(ctx, capabilityIndex, agentDescriptions, idx) {
119
- const lines = [];
120
- lines.push(`[远端 peer 临时上下文] ${ctx.reason} → ${ctx.triggerValue}`);
121
- lines.push(`远端节点: ${idx.ownerName || idx.publicKey.slice(0, 12)}… (${idx.agents.length} agents)`);
122
- lines.push('');
123
- lines.push('## 对方能力索引');
124
- lines.push(capabilityIndex);
125
- lines.push('');
126
- if (agentDescriptions.length > 0) {
127
- lines.push('## 触发相关的 agent 详细描述');
128
- for (const a of agentDescriptions) {
129
- lines.push(`### ${a.agentId}`);
130
- lines.push(a.body);
131
- lines.push('');
132
- }
133
- }
134
- const text = lines.join('\n').trim();
135
- return text.length > 2000 ? text.slice(0, 1997) + '…' : text;
136
- }
137
- /**
138
- * 检测 /message 处理后是否需要加载远端 peer manifest.
139
- * 输入:
140
- * - text (用户原始输入)
141
- * - channelId (当前 channel)
142
- * - remoteChannels (远端 channel 列表 [{ id, name, _ownerPublicKey }])
143
- * - consecutiveFailures (连续 LLM 失败次数, 可选)
144
- */
145
- export function detectLoadTrigger(opts) {
146
- // 触发 ①: @-mention 远端 channel
147
- const mentionRe = /@([一-龥A-Za-z0-9_\-]{1,30})/g;
148
- for (const m of opts.text.matchAll(mentionRe)) {
149
- const name = m[1];
150
- const rc = opts.remoteChannels.find(c => c.name === name);
151
- if (rc && rc._ownerPublicKey) {
152
- return {
153
- shouldLoad: true,
154
- reason: 'mention-remote',
155
- remotePublicKey: rc._ownerPublicKey,
156
- triggerValue: name,
157
- };
158
- }
159
- }
160
- // 触发 ③: 关键词
161
- const text = opts.text.toLowerCase();
162
- const keywords = ['持续协助', '一起合作', 'cooperate', '协作', '对方能干', 'remote help', 'find peer'];
163
- for (const k of keywords) {
164
- if (text.includes(k.toLowerCase())) {
165
- // 关键词触发 — 用第一个 known remote channel 的 owner
166
- if (opts.remoteChannels.length > 0 && opts.remoteChannels[0]._ownerPublicKey) {
167
- return {
168
- shouldLoad: true,
169
- reason: 'cooperate-keyword',
170
- remotePublicKey: opts.remoteChannels[0]._ownerPublicKey,
171
- triggerValue: k,
172
- };
173
- }
174
- }
175
- }
176
- // 触发 ②: 连续失败 (3+ 次)
177
- const fails = opts.consecutiveFailures || 0;
178
- if (fails >= 3) {
179
- // 用最近一次 @ 的远端 channel; 没有则取 remoteChannels[0]
180
- const lastMention = [...opts.text.matchAll(mentionRe)].pop();
181
- let rc = opts.remoteChannels[0];
182
- if (lastMention) {
183
- const found = opts.remoteChannels.find(c => c.name === lastMention[1]);
184
- if (found)
185
- rc = found;
186
- }
187
- if (rc && rc._ownerPublicKey) {
188
- return {
189
- shouldLoad: true,
190
- reason: 'consecutive-failure',
191
- remotePublicKey: rc._ownerPublicKey,
192
- triggerValue: rc.name,
193
- };
194
- }
195
- }
196
- return { shouldLoad: false };
197
- }
198
- // ============== 失败计数器 (内存) ==============
199
- const failureCounters = new Map();
200
- export function recordFailure(channelId) {
201
- const cur = (failureCounters.get(channelId) || 0) + 1;
202
- failureCounters.set(channelId, cur);
203
- return cur;
204
- }
205
- export function clearFailure(channelId) {
206
- failureCounters.delete(channelId);
207
- }
208
- export function getFailures(channelId) {
209
- return failureCounters.get(channelId) || 0;
210
- }
@@ -1,120 +0,0 @@
1
- /**
2
- * ReAct Loop 决策层 (decideNext + runStep)
3
- *
4
- * 2026-06-30 抽出:
5
- * - 原 PiAgentSession.runReActLoop 是 1100+ 行的 private 巨石函数, 既管 LLM IO 又管状态机.
6
- * - 现在抽出"决策下一步"为纯函数, claude code / 外部 harness 可独立消融验证.
7
- *
8
- * 决策表 (apply 到 LLM 回复 + 当前 state):
9
- * 1. reply 是 AI failure sentinel → continue (push error 进 history, 让 LLM 反思)
10
- * 2. reply 可 parse 出 tool call → execute-tool (调 tool, push result 进 history)
11
- * 3. reply 含 <final gen> 且无 tool_call → final (LLM 显式终止)
12
- * 4. reply 无 <final gen>, 无 tool_call → continue (LLM 可能再想想, 让下一轮重新生成)
13
- *
14
- * 这个模块不调 LLM, 只接受 reply/state 返回下一步决策. LLM 调用 + 真正状态变更
15
- * 仍由 PiAgentSession 协调. 这样 claude code 可以独立 unit-test 决策边界.
16
- */
17
- import { parseToolCall, isFinalResponse } from './parse-tool-call.js';
18
- /** 把 LLM 回复归到一种 next action — 纯函数. */
19
- export function decideNext(ctx) {
20
- if (ctx.signalAborted) {
21
- return { kind: 'continue', reason: 'signal-aborted-upstream' };
22
- }
23
- if (ctx.atMaxIterations) {
24
- return { kind: 'final', answer: '(max iterations reached, fail-safe)', reason: 'max-iterations' };
25
- }
26
- // AI failure sentinel 优先 (matches pi-sdk.ts 现有顺序 L2558)
27
- // LLM 偶尔在 retry 中夹杂 sentinel + 工具块, sentinel 是必须先看的"停止信号"
28
- if (isAiFailureSentinel(ctx.reply)) {
29
- return { kind: 'continue', reason: 'ai-failure-sentinel' };
30
- }
31
- // 工具调用优先 (per 2026-06-19 fix) — 在 sentinel 之后
32
- const tc = parseToolCall(ctx.reply, { tools: ctx.knownToolNames });
33
- if (tc) {
34
- if (!ctx.knownToolNames.has(tc.name)) {
35
- // 名字解析出来但不在已知集合 — LLM 幻觉高频场景
36
- return { kind: 'parse-error', reason: `unknown-tool:${tc.name}` };
37
- }
38
- return {
39
- kind: 'execute-tool',
40
- name: tc.name,
41
- args: tc.args,
42
- reason: 'parse-tool-call',
43
- };
44
- }
45
- // final gen 终止
46
- if (isFinalResponse(ctx.reply)) {
47
- return { kind: 'final', answer: ctx.reply, reason: 'final-gen-marker' };
48
- }
49
- // 默认 — 普通 text, 让 LLM 继续想
50
- return { kind: 'continue', reason: 'no-tool-no-final' };
51
- }
52
- /**
53
- * isAiFailureSentinel — 检测 LLM 上游失败哨兵.
54
- * bolloon 用 [AI 服务调用失败] 前缀作为哨兵 — runReActLoop 收到时
55
- * 把错误当 tool result push 进 history, 让 LLM 下轮能反思.
56
- */
57
- export function isAiFailureSentinel(reply) {
58
- if (!reply)
59
- return false;
60
- const trimmed = reply.trim();
61
- return trimmed.startsWith('[AI 服务调用失败]') ||
62
- trimmed.startsWith('[AI 调用失败]') ||
63
- trimmed.startsWith('[错误:');
64
- }
65
- /** extractFinalText — 抽 LLM 输出里能呈现给用户的最终文字 (已是 Message 状态). */
66
- export function extractFinalText(reply, marker = '<final gen>') {
67
- const idx = reply.indexOf(marker);
68
- if (idx !== -1) {
69
- const after = reply.substring(idx + marker.length).trim();
70
- return after || reply.substring(0, idx).trim();
71
- }
72
- return reply;
73
- }
74
- /**
75
- * 错误累计判定 — 当 totalErrors 达阈值时, 给 final 答案 + 汇总.
76
- * 这是独立的小函数, 因为 runReActLoop 用它决定是否继续.
77
- */
78
- export function shouldForceExit(totalErrors, maxTotalErrors) {
79
- return totalErrors >= maxTotalErrors;
80
- }
81
- /**
82
- * 2026-07-01 (v0.2.4 子任务 1): 迭代上限判定 — 纯函数.
83
- * 之前 runReActLoop 在循环顶部硬编码 `if (iteration >= MAX_REACT_ITERATIONS)`,
84
- * 抽出后 claude code 可独立单测 + 替代.
85
- */
86
- export function decideMaxIterations(iteration, maxIterations) {
87
- return {
88
- shouldExit: iteration >= maxIterations,
89
- finalAnswer: iteration >= maxIterations
90
- ? '(本轮 ReAct 循环达到最大步数, 强制结束)'
91
- : '',
92
- };
93
- }
94
- /**
95
- * 2026-07-01 (v0.2.4 子任务 1): context overflow 判定 — 纯函数.
96
- * 之前 runReActLoop 紧接 decideMaxIterations 之后硬编码 token 阈值检查.
97
- * 抽出后行为一致 + 可测.
98
- */
99
- export function decideContextOverflow(estimatedTokens, threshold) {
100
- return {
101
- shouldExit: estimatedTokens > threshold,
102
- finalAnswer: estimatedTokens > threshold
103
- ? `(本轮 ReAct 循环因上下文溢出终止)`
104
- : '',
105
- };
106
- }
107
- /**
108
- * 2026-07-01 (v0.2.4 子任务 1): compact 触发判定 — 纯函数.
109
- * 之前 runReActLoop 入口处: estimatedTokens > compactThreshold → 跑 maybeAutoCompact.
110
- * 抽出纯函数后, claude code 能 dry-run "token X 时该 compact 吗".
111
- */
112
- export function shouldCompactBeforeIteration(estimatedTokens, compactThreshold) {
113
- return estimatedTokens > compactThreshold;
114
- }
115
- /**
116
- * 同一工具连续失败 N 次, 提示 LLM 不要再用同一个工具 (force final).
117
- */
118
- export function shouldHintToStopSameTool(consecutiveFails, threshold) {
119
- return consecutiveFails >= threshold;
120
- }