micro-models-agent 2.11.0 → 2.13.1

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 (356) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/app.js +22 -2
  3. package/dist/assets/index.html +39 -9
  4. package/dist/assets/js/dom.js +15 -0
  5. package/dist/assets/js/feed.js +129 -10
  6. package/dist/assets/js/sidebar.js +512 -0
  7. package/dist/assets/js/tabs.js +30 -5
  8. package/dist/assets/setup.js +267 -8
  9. package/dist/assets/styles.css +168 -3
  10. package/dist/cli/cache-line.js +1 -0
  11. package/dist/cli/command-suggest.js +5 -4
  12. package/dist/cli/commands.js +231 -122
  13. package/dist/cli/completer.js +132 -31
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-payload.js +4 -4
  16. package/dist/cli/main.js +166 -61
  17. package/dist/cli/onboarding-commands.js +126 -0
  18. package/dist/cli/plugin-commands.js +2 -1
  19. package/dist/cli/relaunch.js +16 -13
  20. package/dist/cli/repl-commands-lsp.js +75 -0
  21. package/dist/cli/repl-commands-skills.js +105 -0
  22. package/dist/cli/repl-commands.js +589 -266
  23. package/dist/cli/repl.js +436 -177
  24. package/dist/cli/run-result.js +5 -6
  25. package/dist/cli/security-commands.js +37 -28
  26. package/dist/cli/setup-order.js +4 -55
  27. package/dist/cli/setup-prompt.js +3 -2
  28. package/dist/cli/setup.js +27 -107
  29. package/dist/cli/web-command.js +98 -0
  30. package/dist/config/budget.js +5 -5
  31. package/dist/config/config.js +103 -96
  32. package/dist/config/defaults.js +38 -8
  33. package/dist/config/domains.js +24 -27
  34. package/dist/config/env.js +46 -0
  35. package/dist/config/experts.js +2 -0
  36. package/dist/config/index.js +1 -0
  37. package/dist/config/paths.js +56 -0
  38. package/dist/config/redact.js +44 -0
  39. package/dist/config/registry-extra.js +334 -0
  40. package/dist/config/registry.js +584 -0
  41. package/dist/config/security.js +35 -36
  42. package/dist/config/timeouts.js +74 -0
  43. package/dist/config/utils.js +16 -15
  44. package/dist/core/agent/artifact-progress.js +59 -0
  45. package/dist/core/agent/artifact-resolve.js +79 -0
  46. package/dist/core/agent/audit-gate.js +7 -3
  47. package/dist/core/agent/compaction.js +2 -2
  48. package/dist/core/agent/constants.js +33 -4
  49. package/dist/core/agent/context-log.js +454 -0
  50. package/dist/core/agent/context-renderer.js +5 -0
  51. package/dist/core/agent/critic-gate.js +132 -0
  52. package/dist/core/agent/hallucination-gate.js +148 -38
  53. package/dist/core/agent/loop-state.js +15 -0
  54. package/dist/core/agent/prefix-monitor.js +1 -1
  55. package/dist/core/agent/reasoning-resolver.js +10 -1
  56. package/dist/core/agent/token-tracker.js +7 -4
  57. package/dist/core/agent/tool-batch.js +93 -18
  58. package/dist/core/agent/tool-images.js +18 -0
  59. package/dist/core/agent/tool-output.js +65 -11
  60. package/dist/core/agent/tool-pairing.js +60 -0
  61. package/dist/core/agent-moe.js +25 -24
  62. package/dist/core/agent.js +429 -108
  63. package/dist/core/bootstrap/agent-phase.js +66 -0
  64. package/dist/core/bootstrap/config-phase.js +86 -0
  65. package/dist/core/bootstrap/modules-phase.js +269 -0
  66. package/dist/core/bootstrap/provider-phase.js +183 -0
  67. package/dist/core/bootstrap/session-phase.js +204 -0
  68. package/dist/core/bootstrap/state.js +94 -0
  69. package/dist/core/bootstrap/workspace-phase.js +115 -0
  70. package/dist/core/bootstrap.js +92 -660
  71. package/dist/core/crash-handler.js +17 -7
  72. package/dist/core/environment.js +37 -12
  73. package/dist/core/host-plan.js +43 -0
  74. package/dist/core/host.js +23 -0
  75. package/dist/core/index.js +1 -0
  76. package/dist/core/prompt-builder.js +17 -1
  77. package/dist/core/prompt-overflow.js +42 -43
  78. package/dist/core/session-logger.js +122 -30
  79. package/dist/core/system-prompt.js +117 -0
  80. package/dist/core/version.js +6 -4
  81. package/dist/core/web-host.js +34 -0
  82. package/dist/core/workspace.js +19 -15
  83. package/dist/i18n/en.json +89 -3
  84. package/dist/i18n/index.js +37 -3
  85. package/dist/i18n/ru.json +89 -3
  86. package/dist/index.js +7 -3
  87. package/dist/llm/image-utils.js +55 -64
  88. package/dist/llm/llm-errors.js +9 -0
  89. package/dist/llm/model-loader.js +10 -32
  90. package/dist/llm/models-dev.js +121 -0
  91. package/dist/llm/openai-compat.js +146 -73
  92. package/dist/llm/orchestrator.js +33 -11
  93. package/dist/llm/provider-budget.js +2 -2
  94. package/dist/llm/provider-context.js +233 -0
  95. package/dist/llm/provider.js +1 -1
  96. package/dist/llm/response.js +5 -0
  97. package/dist/llm/token-counter.js +27 -11
  98. package/dist/logger/app-logger.js +48 -22
  99. package/dist/logger/file-log.js +28 -19
  100. package/dist/logger/index.js +1 -0
  101. package/dist/main.js +888 -139
  102. package/dist/migration/backup.js +14 -3
  103. package/dist/migration/detect.js +12 -3
  104. package/dist/migration/index.js +1 -0
  105. package/dist/modules/artifacts/store.js +9 -8
  106. package/dist/modules/browser/actions.js +6 -0
  107. package/dist/modules/browser/bridge-client.js +14 -12
  108. package/dist/modules/browser/bridge-path.js +4 -4
  109. package/dist/modules/browser/console-buffer.js +37 -0
  110. package/dist/modules/browser/cookie-store.js +5 -0
  111. package/dist/modules/browser/driver-types.js +1 -0
  112. package/dist/modules/browser/driver.js +19 -11
  113. package/dist/modules/browser/index.js +3 -1
  114. package/dist/modules/browser/module.js +2 -1
  115. package/dist/modules/browser/session.js +10 -30
  116. package/dist/modules/browser/snapshot.js +19 -13
  117. package/dist/modules/browser/types.js +1 -0
  118. package/dist/modules/certification/cli.js +28 -20
  119. package/dist/modules/certification/fact-checker.js +9 -1
  120. package/dist/modules/certification/loader.js +9 -2
  121. package/dist/modules/certification/manifest.js +83 -21
  122. package/dist/modules/certification/runner.js +23 -13
  123. package/dist/modules/certification/scenarios.js +20 -20
  124. package/dist/modules/certification/syntax-scenarios.js +1 -2
  125. package/dist/modules/context/chunk-query.js +11 -9
  126. package/dist/modules/context/fact-extractor.js +37 -39
  127. package/dist/modules/context/index.js +1 -0
  128. package/dist/modules/context/manager.js +106 -80
  129. package/dist/modules/critic/client.js +176 -0
  130. package/dist/modules/critic/evidence.js +81 -0
  131. package/dist/modules/critic/provider.js +45 -0
  132. package/dist/modules/critic/tools.js +78 -0
  133. package/dist/modules/critic/types.js +1 -0
  134. package/dist/modules/execution/audit-runners.js +28 -34
  135. package/dist/modules/execution/auditor.js +53 -49
  136. package/dist/modules/execution/execution-plugin.js +142 -102
  137. package/dist/modules/execution/index.js +1 -1
  138. package/dist/modules/execution/input-from.js +37 -13
  139. package/dist/modules/execution/module.js +368 -191
  140. package/dist/modules/execution/moe-executor.js +53 -26
  141. package/dist/modules/execution/plan-actions.js +320 -202
  142. package/dist/modules/execution/plan-checks.js +35 -0
  143. package/dist/modules/execution/plan-coverage.js +36 -33
  144. package/dist/modules/execution/plan-steps.js +11 -3
  145. package/dist/modules/execution/plan-store.js +83 -17
  146. package/dist/modules/execution/plan-tool.js +89 -35
  147. package/dist/modules/execution/plan-validator.js +30 -10
  148. package/dist/modules/execution/planner.js +22 -13
  149. package/dist/modules/execution/stuck-detector.js +196 -93
  150. package/dist/modules/execution/tracker.js +22 -8
  151. package/dist/modules/execution/verifier.js +69 -48
  152. package/dist/modules/execution/windows-commands.js +12 -12
  153. package/dist/modules/hallucination/artifact-claim.js +93 -0
  154. package/dist/modules/hallucination/confidence.js +13 -11
  155. package/dist/modules/hallucination/consistency.js +10 -2
  156. package/dist/modules/hallucination/detector.js +10 -5
  157. package/dist/modules/hallucination/factual.js +43 -26
  158. package/dist/modules/hallucination/index.js +1 -0
  159. package/dist/modules/hallucination/js-identifiers.js +44 -45
  160. package/dist/modules/hallucination/llm-judge.js +17 -12
  161. package/dist/modules/hallucination/success-claim.js +59 -0
  162. package/dist/modules/indexer/cache.js +52 -4
  163. package/dist/modules/indexer/index.js +1 -0
  164. package/dist/modules/indexer/map-command.js +2 -4
  165. package/dist/modules/indexer/map-select.js +16 -16
  166. package/dist/modules/indexer/module.js +47 -12
  167. package/dist/modules/indexer/project-profile.js +8 -7
  168. package/dist/modules/indexer/symbols.js +15 -15
  169. package/dist/modules/indexer/walker.js +159 -29
  170. package/dist/modules/lsp/check-tool.js +7 -5
  171. package/dist/modules/lsp/client.js +39 -39
  172. package/dist/modules/lsp/command.js +17 -24
  173. package/dist/modules/lsp/config.js +12 -11
  174. package/dist/modules/lsp/index.js +1 -0
  175. package/dist/modules/lsp/module.js +48 -46
  176. package/dist/modules/lsp/probe.js +13 -14
  177. package/dist/modules/lsp/project-root.js +14 -17
  178. package/dist/modules/lsp/startup-check.js +18 -16
  179. package/dist/modules/mcp/client.js +59 -62
  180. package/dist/modules/mcp/index.js +1 -0
  181. package/dist/modules/mcp/module.js +66 -37
  182. package/dist/modules/mcp/registry.js +5 -0
  183. package/dist/modules/memory/index.js +1 -0
  184. package/dist/modules/memory/module.js +12 -8
  185. package/dist/modules/memory/search.js +4 -1
  186. package/dist/modules/memory/store.js +40 -2
  187. package/dist/modules/onboarding/init.js +47 -0
  188. package/dist/modules/onboarding/meet.js +30 -0
  189. package/dist/modules/onboarding/mode.js +30 -0
  190. package/dist/modules/onboarding/prompts.js +43 -0
  191. package/dist/modules/pipelines/engine.js +10 -0
  192. package/dist/modules/pipelines/index.js +1 -0
  193. package/dist/modules/pipelines/parser.js +4 -1
  194. package/dist/modules/pipelines/template.js +4 -0
  195. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  196. package/dist/modules/plugins/builtin/notify.js +2 -0
  197. package/dist/modules/plugins/index.js +1 -0
  198. package/dist/modules/plugins/loader.js +18 -3
  199. package/dist/modules/plugins/manager.js +37 -17
  200. package/dist/modules/pricing/index.js +10 -11
  201. package/dist/modules/pricing/prices.js +12 -12
  202. package/dist/modules/processes/index.js +1 -0
  203. package/dist/modules/processes/kill-tree.js +27 -31
  204. package/dist/modules/processes/registry.js +44 -25
  205. package/dist/modules/processes/runner.js +4 -4
  206. package/dist/modules/providers/capabilities.js +28 -0
  207. package/dist/modules/providers/create.js +3 -4
  208. package/dist/modules/providers/factory.js +7 -7
  209. package/dist/modules/providers/fallback.js +12 -12
  210. package/dist/modules/providers/health.js +4 -4
  211. package/dist/modules/providers/manager.js +50 -19
  212. package/dist/modules/providers/naming.js +15 -0
  213. package/dist/modules/providers/preflight.js +71 -0
  214. package/dist/modules/providers/presets.js +39 -8
  215. package/dist/modules/providers/registry.js +7 -2
  216. package/dist/modules/reasoning/policy.js +8 -6
  217. package/dist/modules/reasoning/probe.js +30 -25
  218. package/dist/modules/security/audit-log.js +19 -36
  219. package/dist/modules/security/audit-notifier.js +35 -75
  220. package/dist/modules/security/command-validator.js +99 -51
  221. package/dist/modules/security/content-scanner.js +8 -8
  222. package/dist/modules/security/data-sanitizer.js +9 -16
  223. package/dist/modules/security/encryption.js +32 -52
  224. package/dist/modules/security/index.js +2 -1
  225. package/dist/modules/security/network-validator.js +120 -43
  226. package/dist/modules/security/path-validator.js +61 -46
  227. package/dist/modules/security/protected-paths.js +73 -0
  228. package/dist/modules/security/rate-limiter.js +17 -49
  229. package/dist/modules/security/security-policies.js +25 -57
  230. package/dist/modules/security/session-encryption.js +32 -71
  231. package/dist/modules/security/session-isolation.js +15 -28
  232. package/dist/modules/session/auto-name.js +63 -0
  233. package/dist/modules/session/export.js +98 -0
  234. package/dist/modules/session/index.js +1 -0
  235. package/dist/modules/session/manager.js +79 -28
  236. package/dist/modules/session/module.js +4 -3
  237. package/dist/modules/session/store.js +57 -20
  238. package/dist/modules/setup/answers.js +35 -0
  239. package/dist/modules/setup/apply.js +79 -0
  240. package/dist/modules/setup/discovery.js +72 -0
  241. package/dist/modules/skills/index.js +1 -0
  242. package/dist/modules/skills/loader.js +9 -1
  243. package/dist/modules/skills/module.js +11 -0
  244. package/dist/modules/updater/changelog-reader.js +15 -16
  245. package/dist/modules/updater/checker.js +9 -4
  246. package/dist/modules/updater/dev-detect.js +7 -6
  247. package/dist/modules/updater/index.js +1 -0
  248. package/dist/modules/updater/module.js +17 -15
  249. package/dist/modules/user-profile/compressor.js +2 -0
  250. package/dist/modules/user-profile/index.js +1 -0
  251. package/dist/modules/user-profile/profile.js +10 -1
  252. package/dist/modules/webui/answer-api.js +29 -0
  253. package/dist/modules/webui/assets.js +50 -0
  254. package/dist/modules/webui/body.js +21 -0
  255. package/dist/modules/webui/commands-settings.js +209 -0
  256. package/dist/modules/webui/commands-shared.js +60 -0
  257. package/dist/modules/webui/commands.js +174 -0
  258. package/dist/modules/webui/config-api.js +134 -0
  259. package/dist/modules/webui/file-api.js +202 -0
  260. package/dist/modules/webui/forward.js +159 -0
  261. package/dist/modules/webui/history-api.js +112 -0
  262. package/dist/modules/webui/host-state.js +60 -0
  263. package/dist/modules/webui/i18n-api.js +9 -0
  264. package/dist/modules/webui/limits.js +6 -0
  265. package/dist/modules/webui/log-forward.js +20 -0
  266. package/dist/modules/webui/memory-commands.js +64 -0
  267. package/dist/modules/webui/model-cache.js +18 -0
  268. package/dist/modules/webui/module.js +148 -0
  269. package/dist/modules/webui/persist.js +24 -0
  270. package/dist/modules/webui/prompt-io.js +97 -0
  271. package/dist/modules/webui/providers-api.js +337 -0
  272. package/dist/modules/webui/providers-view.js +56 -0
  273. package/dist/modules/webui/reasoning-view.js +15 -0
  274. package/dist/modules/webui/run-agent.js +81 -0
  275. package/dist/modules/webui/server.js +284 -0
  276. package/dist/modules/webui/sessions-api.js +178 -0
  277. package/dist/modules/webui/settings-api.js +561 -0
  278. package/dist/modules/webui/setup-api.js +201 -0
  279. package/dist/modules/webui/state.js +74 -0
  280. package/dist/modules/webui/util.js +261 -0
  281. package/dist/output/bus.js +7 -2
  282. package/dist/output/channel.js +80 -28
  283. package/dist/output/format.js +2 -1
  284. package/dist/output/index.js +1 -0
  285. package/dist/output/json-sink.js +5 -3
  286. package/dist/output/machine.js +4 -3
  287. package/dist/output/session-sink.js +4 -4
  288. package/dist/tools/approve.js +4 -0
  289. package/dist/tools/attach-image.js +5 -4
  290. package/dist/tools/auto-fixer.js +26 -44
  291. package/dist/tools/auto-tags.js +83 -0
  292. package/dist/tools/bash.js +193 -116
  293. package/dist/tools/browser.js +76 -31
  294. package/dist/tools/chunk-query.js +14 -6
  295. package/dist/tools/create-dir.js +9 -5
  296. package/dist/tools/delete-file.js +9 -5
  297. package/dist/tools/download-file.js +32 -7
  298. package/dist/tools/edit-file.js +52 -14
  299. package/dist/tools/enable-tools.js +6 -6
  300. package/dist/tools/executor.js +132 -27
  301. package/dist/tools/file-info.js +3 -1
  302. package/dist/tools/filter-tools.js +3 -3
  303. package/dist/tools/glob-tool.js +4 -1
  304. package/dist/tools/grep-tool.js +13 -10
  305. package/dist/tools/hidden-tools-block.js +6 -6
  306. package/dist/tools/index.js +16 -8
  307. package/dist/tools/list-dir.js +4 -2
  308. package/dist/tools/load-skill.js +7 -0
  309. package/dist/tools/mcp-call.js +15 -4
  310. package/dist/tools/meet-done.js +14 -0
  311. package/dist/tools/move-file.js +14 -7
  312. package/dist/tools/path-utils.js +29 -29
  313. package/dist/tools/pipeline-run.js +29 -7
  314. package/dist/tools/preview.js +1 -1
  315. package/dist/tools/process-ids.js +31 -0
  316. package/dist/tools/process-kill.js +9 -16
  317. package/dist/tools/process-list.js +1 -0
  318. package/dist/tools/process-log.js +11 -16
  319. package/dist/tools/process-wait.js +63 -0
  320. package/dist/tools/prompt-io.js +4 -3
  321. package/dist/tools/question.js +172 -25
  322. package/dist/tools/read-file.js +55 -34
  323. package/dist/tools/recall.js +14 -8
  324. package/dist/tools/registry.js +6 -5
  325. package/dist/tools/remember.js +8 -4
  326. package/dist/tools/scope-request.js +11 -6
  327. package/dist/tools/search-history.js +5 -2
  328. package/dist/tools/session-info.js +2 -0
  329. package/dist/tools/set-thinking.js +8 -4
  330. package/dist/tools/subagent.js +59 -39
  331. package/dist/tools/syntax-validator.js +292 -90
  332. package/dist/tools/user-input.js +22 -18
  333. package/dist/tools/web-browse.js +9 -3
  334. package/dist/tools/web-fetch.js +12 -23
  335. package/dist/tools/web-search.js +7 -5
  336. package/dist/tools/write-file.js +45 -15
  337. package/dist/ui/action-log.js +13 -0
  338. package/dist/ui/box.js +4 -3
  339. package/dist/ui/colors.js +2 -0
  340. package/dist/ui/diff.js +6 -5
  341. package/dist/ui/index.js +1 -0
  342. package/dist/ui/line-editor.js +230 -122
  343. package/dist/ui/line-math.js +10 -2
  344. package/dist/ui/md-formatter.js +57 -37
  345. package/dist/ui/output.js +8 -3
  346. package/dist/ui/plan-view.js +21 -10
  347. package/dist/ui/renderer.js +393 -67
  348. package/dist/ui/spinner.js +16 -5
  349. package/dist/ui/table.js +15 -17
  350. package/dist/ui/theme.js +143 -0
  351. package/dist/utils/child-run.js +20 -0
  352. package/dist/utils/error.js +1 -1
  353. package/dist/utils/index.js +2 -0
  354. package/dist/utils/retry.js +2 -2
  355. package/dist/utils/truncate.js +1 -1
  356. package/package.json +1 -1
@@ -1,23 +1,38 @@
1
+ /** Тул вопросов юзеру: меню, memory pre-check, батч-cap, сессионные ответы. */
1
2
  import { t } from "../i18n/index";
2
3
  import { askUser } from "./user-input";
3
- const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
4
- 1. Gather user preferences or requirements
5
- 2. Clarify ambiguous instructions
6
- 3. Get decisions on implementation choices as you work
7
- 4. Offer choices to the user about what direction to take.
8
-
9
- Usage notes:
10
- - When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
11
- - Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
12
- - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
13
- - Execution blocks until the user answers every question
14
- - Omit "options" for a free-text question`;
4
+ import { MemoryStore } from "../modules/memory/store";
5
+ import { globalMemoryDir } from "../config/paths";
6
+ const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
7
+ 1. Gather user preferences or requirements
8
+ 2. Clarify ambiguous instructions
9
+ 3. Get decisions on implementation choices as you work
10
+ 4. Offer choices to the user about what direction to take.
11
+
12
+ Usage notes:
13
+ - When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
14
+ - Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
15
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
16
+ - Execution blocks until the user answers every question
17
+ - Omit "options" for a free-text question
18
+ - Never ask choice questions as a plain-text list in your reply — always call
19
+ this tool (it renders interactive buttons and blocks until answered).
20
+ When the user asks for a Q&A session with options, use this tool for every
21
+ question, not chat text
22
+ - Before asking, the tool checks agent memory itself: questions memory
23
+ already answers are skipped (reported back, not asked) — never ask what
24
+ is already known
25
+ - Answers live only in the current session; nothing is written to memory.
26
+ Call "remember" separately to persist something, and only when asked to
27
+ - At most 3 unknown questions are asked per call; keep batches small`;
28
+ /** Проверяет форму опции (label + description — строки). */
15
29
  function isOption(val) {
16
30
  if (!val || typeof val !== "object")
17
31
  return false;
18
32
  const opt = val;
19
33
  return typeof opt.label === "string" && typeof opt.description === "string";
20
34
  }
35
+ /** Нормализует args в спеки (массив + legacy `{ question }`). Битые — пропуск. */
21
36
  function normalizeQuestions(args) {
22
37
  const raw = args.questions;
23
38
  if (Array.isArray(raw)) {
@@ -29,8 +44,8 @@ function normalizeQuestions(args) {
29
44
  if (typeof q.question !== "string" || !q.question.trim())
30
45
  continue;
31
46
  const options = Array.isArray(q.options) ? q.options.filter(isOption) : undefined;
32
- // An explicitly provided options array with no valid entries is a
33
- // malformed call — skip the question instead of blocking on stdin.
47
+ // Явно данный массив опций без валидных записей — malformed вызов:
48
+ // вопрос пропустить, а не висеть на stdin.
34
49
  if (Array.isArray(q.options) && options.length === 0)
35
50
  continue;
36
51
  specs.push({
@@ -43,17 +58,120 @@ function normalizeQuestions(args) {
43
58
  }
44
59
  return specs;
45
60
  }
46
- // Legacy format: { question: "..." } — plain free-text question.
61
+ // Legacy-формат: { question: "..." } — plain free-text вопрос.
47
62
  if (typeof args.question === "string" && args.question.trim()) {
48
63
  return [{ question: args.question }];
49
64
  }
50
65
  return [];
51
66
  }
67
+ /** Максимум незнакомых вопросов за вызов — большие батчи малые модели теряют. */
68
+ export const MAX_QUESTIONS_PER_CALL = 3;
69
+ /** Токенный bound memory pre-check: хиты репортятся повопросно. */
70
+ const MAX_MEMORY_HITS = 2;
71
+ const MAX_HIT_CHARS = 200;
72
+ /**
73
+ * Делит спеки на известные памяти (репорт, не спрашивать) и незнакомые.
74
+ * Чистая через инжектируемый lookup — тесты не трогают реальный memory dir.
75
+ */
76
+ export function splitByMemory(specs, lookup) {
77
+ const known = [];
78
+ const unknown = [];
79
+ for (const spec of specs) {
80
+ const hits = lookup(spec);
81
+ if (hits.length > 0)
82
+ known.push({ spec, hits });
83
+ else
84
+ unknown.push(spec);
85
+ }
86
+ return { known, unknown };
87
+ }
88
+ /**
89
+ * Значимые слова текста: lowercase, unicode-разбивка, длина ≥ 4. Короче
90
+ * ("тип", "для", "что") — шум для overlap-мэтчинга.
91
+ */
92
+ export function extractKeywords(...texts) {
93
+ const out = new Set();
94
+ for (const text of texts) {
95
+ if (!text)
96
+ continue;
97
+ for (const word of text.toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
98
+ if (word.length >= 4)
99
+ out.add(word);
100
+ }
101
+ }
102
+ return [...out];
103
+ }
104
+ /**
105
+ * Строка памяти считается ответом, только если делит со вопросом не меньше
106
+ * стольких distinct keywords. Одно общее слово ("какой", "город") — обычно
107
+ * совпадение: пропуск на нём ответил бы из stale-данных вместо вопроса.
108
+ * Precision важнее recall: промах просто спросит.
109
+ */
110
+ const MIN_KEYWORD_OVERLAP = 2;
111
+ /**
112
+ * Keyword-overlap поиск по памяти через инжектируемый покейвордный поиск.
113
+ * Чистая — прод-обвязка (`lookupMemory`) передаёт `MemoryStore.search`.
114
+ */
115
+ export function collectMemoryHits(searchOne, spec) {
116
+ const keywords = extractKeywords(spec.question, spec.header, ...(spec.options ?? []).flatMap((o) => [o.label, o.description]));
117
+ if (keywords.length === 0)
118
+ return [];
119
+ const lines = new Map();
120
+ for (const kw of keywords) {
121
+ let hits;
122
+ try {
123
+ hits = searchOne(kw);
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ for (const r of hits) {
129
+ const key = `${r.file}\n${r.match}`;
130
+ let entry = lines.get(key);
131
+ if (!entry) {
132
+ entry = { text: `[${r.file}] ${r.match}`, matched: new Set() };
133
+ lines.set(key, entry);
134
+ }
135
+ entry.matched.add(kw);
136
+ }
137
+ }
138
+ return [...lines.values()]
139
+ .filter((e) => e.matched.size >= MIN_KEYWORD_OVERLAP)
140
+ .slice(0, MAX_MEMORY_HITS)
141
+ .map((e) => e.text.slice(0, MAX_HIT_CHARS));
142
+ }
143
+ /**
144
+ * Fail-open поиск по памяти: нечитаемая/отсутствующая память вопрос не блокирует.
145
+ * Возврат — короткие `file: match`-сниппеты, токенно ограниченные.
146
+ */
147
+ function lookupMemory(spec) {
148
+ try {
149
+ const store = new MemoryStore(globalMemoryDir());
150
+ return collectMemoryHits((kw) => store.search(kw), spec);
151
+ }
152
+ catch {
153
+ return [];
154
+ }
155
+ }
156
+ /**
157
+ * Тестовый шов: `memorySearch` на контексте стабит memory pre-check (тот же
158
+ * override-паттерн, что `ToolContext.agentFactory` — моки Bun-модулей глобальны
159
+ * на процесс, plain vi.mock тут не годится).
160
+ */
161
+ function resolveSearch(ctx) {
162
+ const stub = ctx
163
+ .memorySearch;
164
+ return typeof stub === "function" ? stub : lookupMemory;
165
+ }
166
+ /** Тул вопросов: выбор/мультивыбор/free-text, memory pre-check, батч-cap 3. */
52
167
  export const questionTool = {
53
168
  name: "question",
54
169
  icon: "❓",
55
170
  description: DESCRIPTION,
56
171
  tags: ["core"],
172
+ // Интерактивные тулы переживают сужение тегов: спросить модель должна уметь
173
+ // всегда, при любом типе задачи (P5 auto-narrowing).
174
+ alwaysOn: true,
57
175
  interactive: true,
58
176
  parameters: {
59
177
  type: "object",
@@ -113,10 +231,18 @@ export const questionTool = {
113
231
  if (questions.length === 0) {
114
232
  return { success: false, output: t("tool.question.no_questions") };
115
233
  }
234
+ // Memory pre-check: известное памяти не спрашивать. Известные вопросы —
235
+ // репорт, не опрос. В память ничего не пишется: ответы живут только в сессии
236
+ // (persist — отдельным "remember" по просьбе).
237
+ const { known, unknown } = splitByMemory(questions, resolveSearch(ctx));
238
+ // Батч-cap: не больше MAX_QUESTIONS_PER_CALL за раз, остаток — пометкой,
239
+ // чтобы модель довызвала, а не потеряла вопросы молча.
240
+ const truncated = unknown.length - MAX_QUESTIONS_PER_CALL;
241
+ const toAsk = truncated > 0 ? unknown.slice(0, MAX_QUESTIONS_PER_CALL) : unknown;
116
242
  const answers = [];
117
- for (let i = 0; i < questions.length; i++) {
118
- const q = questions[i];
119
- const progress = questions.length > 1
243
+ for (let i = 0; i < toAsk.length; i++) {
244
+ const q = toAsk[i];
245
+ const progress = toAsk.length > 1
120
246
  ? t("tool.question.progress", {
121
247
  current: i + 1,
122
248
  total: questions.length,
@@ -130,12 +256,33 @@ export const questionTool = {
130
256
  progress,
131
257
  }, ctx.promptIO));
132
258
  }
133
- const formatted = questions
134
- .map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
135
- .join(", ");
136
- return {
137
- success: true,
138
- output: t("tool.question.answered", { formatted }),
139
- };
259
+ const parts = [];
260
+ if (toAsk.length > 0) {
261
+ const formatted = toAsk
262
+ .map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
263
+ .join(", ");
264
+ parts.push(t("tool.question.answered", { formatted }));
265
+ }
266
+ else {
267
+ parts.push(t("tool.question.memory_only"));
268
+ }
269
+ if (known.length > 0) {
270
+ const formatted = known
271
+ .map((k) => `"${k.spec.question}"="${k.hits.join("; ")}"`)
272
+ .join(", ");
273
+ parts.push(t("tool.question.memory_known", { formatted }));
274
+ }
275
+ parts.push(t("tool.question.session_only"));
276
+ if (truncated > 0) {
277
+ parts.push(t("tool.question.batch_truncated", {
278
+ shown: MAX_QUESTIONS_PER_CALL,
279
+ total: unknown.length,
280
+ }));
281
+ }
282
+ // display:"" гасит UI-эхо (tool-batch предпочитает display и пропускает
283
+ // пустые): модели — полный output в role:"tool", чату — только блок вопроса
284
+ // с ↳-ответами. Текст output — model-facing директивы (сессия/память/батч),
285
+ // не контент юзера: и verbose идёт через tool_end, не meta.
286
+ return { success: true, output: parts.join(" "), display: "" };
140
287
  },
141
288
  };
@@ -1,3 +1,4 @@
1
+ /** Тул чтения файла: постранично, со стримингом больших файлов. */
1
2
  import { readFileSync, existsSync, statSync, openSync, readSync, closeSync } from "fs";
2
3
  import { extname } from "path";
3
4
  import { t } from "../i18n/index";
@@ -5,27 +6,27 @@ import { isPathInScope } from "../modules/security/path-validator";
5
6
  import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
7
  import { logSecurityBlock } from "../modules/security/audit-log";
7
8
  import { safeResolvePath } from "./path-utils";
9
+ import { errMsg } from "../utils";
8
10
  /**
9
- * Default lines per read_file call. Deliberately much larger than
10
- * MAX_PREVIEW_LINES (15): a 15-line default forces the model to issue 10-17
11
- * calls per file, which floods the context and drives compaction every 15
12
- * iterations (observed: 96 read_file calls, App.tsx read 23x, then 11
13
- * compactions in ~20 min that deleted the user's task mid-turn). 300 lines
14
- * covers typical component files in one call while still capping output.
11
+ * Строк на вызов read_file по умолчанию. Намеренно больше MAX_PREVIEW_LINES
12
+ * (15): дефолт 15 заставлял модель делать 10–17 вызовов на файл, что заливало
13
+ * контекст и дёргало компакцию каждые 15 итераций (наблюдено: 96 вызовов
14
+ * read_file, 11 компакций за ~20 мин со сносом задачи). 300 строк покрывают
15
+ * типичный файл за один вызов, вывод всё равно ограничен.
15
16
  */
16
17
  const DEFAULT_LIMIT = 300;
18
+ /** Жёсткий потолок явного `limit` (тул исключён из усечения по бюджету). */
19
+ const MAX_READ_LIMIT = 2000;
17
20
  /**
18
- * Files larger than this are read through a bounded streaming path instead of
19
- * readFileSync (which loads the whole file into memory just to slice a few
20
- * hundred lines).
21
+ * Файлы крупнее — через ограниченный стриминг вместо readFileSync (который
22
+ * грузит весь файл в память ради пары сотен строк).
21
23
  */
22
24
  const LARGE_FILE_BYTES = 1024 * 1024;
23
25
  const STREAM_CHUNK = 256 * 1024;
24
26
  /**
25
- * Bounded line-slice read for large files: streams the file in chunks,
26
- * collects only the requested [offset, offset+limit) window, then counts the
27
- * remaining newlines raw (no decode) to produce the total. Memory stays
28
- * bounded regardless of file size.
27
+ * Ограниченное построчное чтение больших файлов: стримит чанками, собирает
28
+ * только окно [offset, offset+limit), остаток переводов строк считает сырым
29
+ * (без декода) для total. Память ограничена при любом размере файла.
29
30
  */
30
31
  function readLineSlice(path, offset, limit) {
31
32
  const fd = openSync(path, "r");
@@ -52,7 +53,7 @@ function readLineSlice(path, offset, limit) {
52
53
  lineNumber++;
53
54
  }
54
55
  if (lineNumber >= endLine && collected.length >= limit) {
55
- // Window complete — count remaining newlines without decoding.
56
+ // Окно собрано — остаток переводов строк считаем без декодирования.
56
57
  const rest = [];
57
58
  while (true) {
58
59
  const n = readSync(fd, buf, 0, STREAM_CHUNK, pos);
@@ -65,7 +66,10 @@ function readLineSlice(path, offset, limit) {
65
66
  for (let i = 0; i < tail.length; i++)
66
67
  if (tail[i] === 0x0a)
67
68
  total++;
68
- total++; // last line has no trailing newline
69
+ // Плюс финальная строка без перевода — только если файл им не
70
+ // кончается, иначе посчитаем фантомную пустую строку.
71
+ if (tail.length > 0 && tail[tail.length - 1] !== 0x0a)
72
+ total++;
69
73
  return { selected: collected.join("\n"), total };
70
74
  }
71
75
  }
@@ -80,12 +84,14 @@ function readLineSlice(path, offset, limit) {
80
84
  closeSync(fd);
81
85
  }
82
86
  }
87
+ /** Читает файл постранично (offset/limit); кешируемый, с cap вывода. */
83
88
  export const readFileTool = {
84
89
  name: "read_file",
85
90
  icon: "👀",
86
91
  description: `Read a file from the filesystem. Reads up to ${DEFAULT_LIMIT} lines at a time by default; use offset to page through large files.`,
87
92
  tags: ["file", "code"],
88
93
  boundedOutput: true,
94
+ cacheable: true,
89
95
  parameters: {
90
96
  type: "object",
91
97
  properties: {
@@ -107,8 +113,7 @@ export const readFileTool = {
107
113
  const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
108
114
  const scopeCheck = isPathInScope(ctx.baseDir, resolved, ctx.scope, securityPaths);
109
115
  if (!scopeCheck.allowed) {
110
- const pathStr = path;
111
- logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
116
+ logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", path);
112
117
  return {
113
118
  success: false,
114
119
  output: t("file.path_not_allowed", {
@@ -117,8 +122,8 @@ export const readFileTool = {
117
122
  };
118
123
  }
119
124
  if (!existsSync(resolved)) {
120
- // For relative paths, tell the model WHERE the path resolved — the
121
- // "not found" alone is confusing (it cannot tell baseDir from cwd).
125
+ // Для относительных путей показываем, КУДА резолвилось: голое
126
+ // "not found" не даёт модели отличить baseDir от cwd.
122
127
  const output = resolved !== path
123
128
  ? t("file.notfound_resolved", {
124
129
  path,
@@ -127,22 +132,39 @@ export const readFileTool = {
127
132
  : t("file.notfound", { path });
128
133
  return { success: false, output };
129
134
  }
130
- const offset = args.offset || 1;
131
- const limit = args.limit || DEFAULT_LIMIT;
135
+ // Кламп обоих: `offset: 0` давал slice(-1, end) с ПОСЛЕДНЕЙ строкой, а
136
+ // безграничный `limit` (тул вне усечения по бюджету) тянул бы целый
137
+ // большой файл в контекст.
138
+ const offset = Math.max(1, typeof args.offset === "number" ? Math.floor(args.offset) : 1);
139
+ const limit = Math.min(MAX_READ_LIMIT, Math.max(1, typeof args.limit === "number" ? Math.floor(args.limit) : DEFAULT_LIMIT));
132
140
  let lines;
133
141
  let total;
134
142
  let selected;
135
- if (statSync(resolved).size > LARGE_FILE_BYTES) {
136
- const slice = readLineSlice(resolved, offset, limit);
137
- total = slice.total;
138
- selected = slice.selected;
143
+ try {
144
+ if (statSync(resolved).size > LARGE_FILE_BYTES) {
145
+ const slice = readLineSlice(resolved, offset, limit);
146
+ total = slice.total;
147
+ selected = slice.selected;
148
+ }
149
+ else {
150
+ const content = readFileSync(resolved, "utf-8");
151
+ lines = content.split("\n");
152
+ // Файл с финальным переводом даёт фантомный пустой элемент — снять,
153
+ // чтобы счёт строк совпал со стриминг-путем.
154
+ if (lines.length > 1 && lines[lines.length - 1] === "")
155
+ lines.pop();
156
+ total = lines.length;
157
+ const end = Math.min(offset - 1 + limit, total);
158
+ selected = lines.slice(offset - 1, end).join("\n");
159
+ }
139
160
  }
140
- else {
141
- const content = readFileSync(resolved, "utf-8");
142
- lines = content.split("\n");
143
- total = lines.length;
144
- const end = Math.min(offset - 1 + limit, total);
145
- selected = lines.slice(offset - 1, end).join("\n");
161
+ catch (err) {
162
+ // Файл мог исчезнуть между existsSync и чтением (TOCTOU) или оказаться
163
+ // директорией — возвращаем понятную ошибку, а не голый throw.
164
+ return {
165
+ success: false,
166
+ output: t("file.read_failed", { path, error: errMsg(err) }),
167
+ };
146
168
  }
147
169
  const end = Math.min(offset - 1 + limit, total);
148
170
  const ext = extname(resolved) || "(no extension)";
@@ -159,9 +181,8 @@ export const readFileTool = {
159
181
  next: String(end + 1),
160
182
  })
161
183
  : "";
162
- // Compact user-facing display: the tool header already shows the path,
163
- // so only a partial read needs extra info (the range within the file).
164
- // A full read adds nothing — keeps the card to two lines.
184
+ // Компактный display: заголовок уже показывает путь, доп. инфо (диапазон)
185
+ // нужна только частичному чтению. Полное чтение — пусто, карточка в 2 строки.
165
186
  const fullRead = offset === 1 && end >= total;
166
187
  const display = fullRead
167
188
  ? ""
@@ -1,8 +1,10 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
1
+ /** Тул чтения межсессионной памяти (поиск, категории, всё целиком). */
3
2
  import { t } from "../i18n/index";
4
3
  import { MemoryStore } from "../modules/memory/store";
4
+ import { globalMemoryDir } from "../config/paths";
5
+ /** Категории памяти. */
5
6
  const CATEGORIES = ["preferences", "conventions", "decisions", "errors", "facts"];
7
+ /** Читает/ищет по памяти. Параметры: query (full-text), category (опционально). */
6
8
  export const recallTool = {
7
9
  name: "recall",
8
10
  icon: "💾",
@@ -25,18 +27,18 @@ export const recallTool = {
25
27
  handler: async (_ctx, args) => {
26
28
  const query = args.query ? String(args.query) : "";
27
29
  const category = args.category ? String(args.category) : "";
28
- const memoryDir = join(homedir(), ".mma", "memory");
30
+ const memoryDir = globalMemoryDir();
29
31
  const store = new MemoryStore(memoryDir);
30
32
  try {
31
- // No query, no category → show everything
33
+ // Без запроса и категории — показать всё.
32
34
  if (!query && !category) {
33
35
  return { success: true, output: formatAll(store) };
34
36
  }
35
- // Category only → show that category
37
+ // Только категория — показать её.
36
38
  if (!query && category) {
37
39
  return { success: true, output: formatCategory(store, category) };
38
40
  }
39
- // Query with optional category → search
41
+ // Запрос с опциональной категорией — поиск.
40
42
  if (category) {
41
43
  const results = searchCategory(store, category, query);
42
44
  if (results.length === 0) {
@@ -47,7 +49,7 @@ export const recallTool = {
47
49
  output: t("tool.recall.search_results", { category, results: results.join("\n") }),
48
50
  };
49
51
  }
50
- // Query across all
52
+ // Запрос по всем категориям.
51
53
  const results = store.search(query);
52
54
  if (results.length === 0) {
53
55
  return { success: true, output: t("tool.recall.empty", { query }) };
@@ -59,10 +61,12 @@ export const recallTool = {
59
61
  };
60
62
  }
61
63
  catch (err) {
62
- return { success: true, output: t("tool.memory_error", { error: String(err) }) };
64
+ // Чтение отсутствующей/битой категории — ошибка, а не пустой успех.
65
+ return { success: false, output: t("tool.memory_error", { error: String(err) }) };
63
66
  }
64
67
  },
65
68
  };
69
+ /** Форматирует всю память (преференсы + последние 5 записей категорий). */
66
70
  function formatAll(store) {
67
71
  const parts = [];
68
72
  const prefs = store.getPreferences();
@@ -84,6 +88,7 @@ function formatAll(store) {
84
88
  }
85
89
  return parts.length > 0 ? parts.join("\n") : t("tool.recall.no_memory");
86
90
  }
91
+ /** Форматирует одну категорию. */
87
92
  function formatCategory(store, category) {
88
93
  if (category === "preferences") {
89
94
  const prefs = store.getPreferences();
@@ -101,6 +106,7 @@ function formatCategory(store, category) {
101
106
  return t("tool.recall.no_memory");
102
107
  return `${category.charAt(0).toUpperCase() + category.slice(1)} (${entries.length} entries):\n${entries.join("\n")}`;
103
108
  }
109
+ /** Ищет запрос внутри категории (регистронезависимо). */
104
110
  function searchCategory(store, category, query) {
105
111
  if (category === "preferences") {
106
112
  const prefs = store.getPreferences();
@@ -12,12 +12,15 @@ export class ToolRegistry {
12
12
  has(name) {
13
13
  return this.tools.has(name);
14
14
  }
15
+ /** Тулы по именам (отсортированы). */
15
16
  list() {
16
17
  return Array.from(this.tools.keys()).sort();
17
18
  }
19
+ /** Все зарегистрированные определения. */
18
20
  getAll() {
19
21
  return Array.from(this.tools.values());
20
22
  }
23
+ /** Подмножество с любым из тегов (без alwaysOn). */
21
24
  getByTags(tags) {
22
25
  return this.getAll().filter((t) => {
23
26
  if (!t.tags || t.tags.length === 0)
@@ -26,17 +29,15 @@ export class ToolRegistry {
26
29
  });
27
30
  }
28
31
  /**
29
- * Tools to expose to the LLM. Always includes `alwaysOn` tools, then tools
30
- * matching the given tags. When no tags are given, returns every registered
31
- * tool (backward-compatible). Duplicates are impossible because the
32
- * alwaysOn + tagged sets are unioned by name.
32
+ * Тулы для LLM: всегда `alwaysOn`, плюс совпавшие по тегам. Без тегов —
33
+ * все (backward-compatible). Дубликаты исключены объединением по имени.
33
34
  */
34
35
  getAllForLLM(tags) {
35
36
  const all = this.getAll();
36
37
  const tagSet = new Set(tags ?? []);
37
38
  const selected = tagSet.size === 0
38
39
  ? all
39
- : all.filter((t) => t.alwaysOn || (t.tags && t.tags.some((tag) => tagSet.has(tag))));
40
+ : all.filter((t) => t.alwaysOn || (t.tags?.some((tag) => tagSet.has(tag))));
40
41
  return selected.map((t) => ({
41
42
  name: t.name,
42
43
  description: t.description,
@@ -1,8 +1,10 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
1
+ /** Тул записи в межсессионную память (preferences, факты, решения, ошибки). */
3
2
  import { t } from "../i18n/index";
4
3
  import { MemoryStore } from "../modules/memory/store";
4
+ import { globalMemoryDir } from "../config/paths";
5
+ /** Категории памяти. */
5
6
  const CATEGORIES = ["preferences", "conventions", "decisions", "errors", "facts"];
7
+ /** Пишет факт/преференс в глобальную память. Параметры: category, key/value или entry. */
6
8
  export const rememberTool = {
7
9
  name: "remember",
8
10
  icon: "🧠",
@@ -36,7 +38,7 @@ export const rememberTool = {
36
38
  if (!CATEGORIES.includes(category)) {
37
39
  return { success: false, output: t("tool.invalid_params") };
38
40
  }
39
- const memoryDir = join(homedir(), ".mma", "memory");
41
+ const memoryDir = globalMemoryDir();
40
42
  const store = new MemoryStore(memoryDir);
41
43
  try {
42
44
  if (category === "preferences") {
@@ -62,7 +64,9 @@ export const rememberTool = {
62
64
  };
63
65
  }
64
66
  catch (err) {
65
- return { success: true, output: t("tool.memory_error", { error: String(err) }) };
67
+ // Упавшая запись — провал: отчёт об успехе заставлял агента (и учёт
68
+ // падений) верить, что память сохранена.
69
+ return { success: false, output: t("tool.memory_error", { error: String(err) }) };
66
70
  }
67
71
  },
68
72
  };
@@ -1,3 +1,4 @@
1
+ /** Тулы записи (для классификации отказа как write). */
1
2
  const WRITE_TOOLS = new Set([
2
3
  "write_file",
3
4
  "edit_file",
@@ -7,8 +8,8 @@ const WRITE_TOOLS = new Set([
7
8
  "download_file",
8
9
  ]);
9
10
  /**
10
- * Scope denials funnel through i18n keys file.path_not_allowed(_short)
11
- * ("Path not allowed" / "Путь недопустим") and chunk-query's "[SCOPE]" prefix.
11
+ * Отказы скоупа идут через i18n-ключи file.path_not_allowed(_short)
12
+ * ("Path not allowed" / "Путь недопустим") и "[SCOPE]"-префикс chunk-query.
12
13
  */
13
14
  export function isPathDenialOutput(output) {
14
15
  if (!output)
@@ -17,6 +18,7 @@ export function isPathDenialOutput(output) {
17
18
  output.includes("Путь недопустим") ||
18
19
  output.startsWith("[SCOPE]"));
19
20
  }
21
+ /** Извлекает путь из аргументов tool call (по именам ключей, иначе по виду). */
20
22
  function pathArgOf(args) {
21
23
  if (!args)
22
24
  return "";
@@ -31,30 +33,31 @@ function pathArgOf(args) {
31
33
  }
32
34
  return "";
33
35
  }
34
- /** Observes the sub-agent's tool results and records scope-denied paths. */
36
+ /** Наблюдает результаты тулов субагента, копит scope-отказы по путям. */
35
37
  export class ScopeDenialCollector {
36
38
  denied = new Map();
37
39
  plugin = {
38
40
  name: "scope-denial-collector",
39
41
  isBuiltin: true,
40
42
  onAfterTool: (_ctx, call, result) => {
41
- const name = call?.name;
43
+ const name = call.name;
42
44
  if (!name || !result || result.success)
43
45
  return;
44
46
  if (!isPathDenialOutput(typeof result.output === "string" ? result.output : undefined)) {
45
47
  return;
46
48
  }
47
- const path = pathArgOf(call?.arguments);
49
+ const path = pathArgOf(call.arguments);
48
50
  if (!path)
49
51
  return;
50
52
  const write = WRITE_TOOLS.has(name);
51
53
  this.denied.set(path, { path, write });
52
54
  },
53
55
  };
56
+ /** Все отказы дедуплицированно. */
54
57
  getDenied() {
55
58
  return Array.from(this.denied.values());
56
59
  }
57
- /** read_only=true only when every denial was a pure read denial. */
60
+ /** read_only=true — только если все отказы были чисто read. */
58
61
  toScopeRequest(reason = "sub-agent was denied access to files needed for the task") {
59
62
  const all = this.getDenied();
60
63
  if (all.length === 0)
@@ -66,9 +69,11 @@ export class ScopeDenialCollector {
66
69
  };
67
70
  }
68
71
  }
72
+ /** Сериализует запрос в `<scope-request>`-блок результата. */
69
73
  export function formatScopeRequest(req) {
70
74
  return `<scope-request>${JSON.stringify(req)}</scope-request>`;
71
75
  }
76
+ /** Парсит `<scope-request>`-блок из текста. Битый/пустой — null. */
72
77
  export function parseScopeRequest(text) {
73
78
  if (!text)
74
79
  return null;