micro-models-agent 2.11.0 → 2.12.0

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 (347) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/js/tabs.js +22 -3
  3. package/dist/cli/cache-line.js +1 -0
  4. package/dist/cli/command-suggest.js +5 -4
  5. package/dist/cli/commands.js +231 -122
  6. package/dist/cli/completer.js +132 -31
  7. package/dist/cli/index.js +1 -0
  8. package/dist/cli/json-payload.js +4 -4
  9. package/dist/cli/main.js +166 -61
  10. package/dist/cli/onboarding-commands.js +126 -0
  11. package/dist/cli/plugin-commands.js +2 -1
  12. package/dist/cli/relaunch.js +16 -13
  13. package/dist/cli/repl-commands-lsp.js +75 -0
  14. package/dist/cli/repl-commands-skills.js +105 -0
  15. package/dist/cli/repl-commands.js +589 -266
  16. package/dist/cli/repl.js +436 -177
  17. package/dist/cli/run-result.js +5 -6
  18. package/dist/cli/security-commands.js +37 -28
  19. package/dist/cli/setup-order.js +4 -55
  20. package/dist/cli/setup-prompt.js +3 -2
  21. package/dist/cli/setup.js +27 -107
  22. package/dist/cli/web-command.js +98 -0
  23. package/dist/config/budget.js +5 -5
  24. package/dist/config/config.js +103 -96
  25. package/dist/config/defaults.js +38 -8
  26. package/dist/config/domains.js +24 -27
  27. package/dist/config/env.js +46 -0
  28. package/dist/config/experts.js +2 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/paths.js +56 -0
  31. package/dist/config/redact.js +44 -0
  32. package/dist/config/registry-extra.js +334 -0
  33. package/dist/config/registry.js +584 -0
  34. package/dist/config/security.js +35 -36
  35. package/dist/config/timeouts.js +74 -0
  36. package/dist/config/utils.js +16 -15
  37. package/dist/core/agent/artifact-progress.js +59 -0
  38. package/dist/core/agent/artifact-resolve.js +79 -0
  39. package/dist/core/agent/audit-gate.js +7 -3
  40. package/dist/core/agent/compaction.js +2 -2
  41. package/dist/core/agent/constants.js +33 -4
  42. package/dist/core/agent/context-log.js +454 -0
  43. package/dist/core/agent/context-renderer.js +5 -0
  44. package/dist/core/agent/critic-gate.js +132 -0
  45. package/dist/core/agent/hallucination-gate.js +148 -38
  46. package/dist/core/agent/loop-state.js +15 -0
  47. package/dist/core/agent/prefix-monitor.js +1 -1
  48. package/dist/core/agent/reasoning-resolver.js +10 -1
  49. package/dist/core/agent/token-tracker.js +7 -4
  50. package/dist/core/agent/tool-batch.js +93 -18
  51. package/dist/core/agent/tool-images.js +18 -0
  52. package/dist/core/agent/tool-output.js +65 -11
  53. package/dist/core/agent/tool-pairing.js +60 -0
  54. package/dist/core/agent-moe.js +25 -24
  55. package/dist/core/agent.js +429 -108
  56. package/dist/core/bootstrap/agent-phase.js +66 -0
  57. package/dist/core/bootstrap/config-phase.js +86 -0
  58. package/dist/core/bootstrap/modules-phase.js +269 -0
  59. package/dist/core/bootstrap/provider-phase.js +183 -0
  60. package/dist/core/bootstrap/session-phase.js +204 -0
  61. package/dist/core/bootstrap/state.js +94 -0
  62. package/dist/core/bootstrap/workspace-phase.js +115 -0
  63. package/dist/core/bootstrap.js +92 -660
  64. package/dist/core/crash-handler.js +17 -7
  65. package/dist/core/environment.js +37 -12
  66. package/dist/core/host-plan.js +43 -0
  67. package/dist/core/host.js +23 -0
  68. package/dist/core/index.js +1 -0
  69. package/dist/core/prompt-builder.js +17 -1
  70. package/dist/core/prompt-overflow.js +42 -43
  71. package/dist/core/session-logger.js +122 -30
  72. package/dist/core/system-prompt.js +117 -0
  73. package/dist/core/version.js +6 -4
  74. package/dist/core/web-host.js +34 -0
  75. package/dist/core/workspace.js +19 -15
  76. package/dist/i18n/index.js +37 -3
  77. package/dist/index.js +7 -3
  78. package/dist/llm/image-utils.js +55 -64
  79. package/dist/llm/llm-errors.js +9 -0
  80. package/dist/llm/model-loader.js +10 -32
  81. package/dist/llm/models-dev.js +121 -0
  82. package/dist/llm/openai-compat.js +146 -73
  83. package/dist/llm/orchestrator.js +33 -11
  84. package/dist/llm/provider-budget.js +2 -2
  85. package/dist/llm/provider-context.js +233 -0
  86. package/dist/llm/provider.js +1 -1
  87. package/dist/llm/response.js +5 -0
  88. package/dist/llm/token-counter.js +27 -11
  89. package/dist/logger/app-logger.js +48 -22
  90. package/dist/logger/file-log.js +28 -19
  91. package/dist/logger/index.js +1 -0
  92. package/dist/main.js +273 -47
  93. package/dist/migration/backup.js +14 -3
  94. package/dist/migration/detect.js +12 -3
  95. package/dist/migration/index.js +1 -0
  96. package/dist/modules/artifacts/store.js +9 -8
  97. package/dist/modules/browser/actions.js +6 -0
  98. package/dist/modules/browser/bridge-client.js +14 -12
  99. package/dist/modules/browser/bridge-path.js +4 -4
  100. package/dist/modules/browser/console-buffer.js +37 -0
  101. package/dist/modules/browser/cookie-store.js +5 -0
  102. package/dist/modules/browser/driver-types.js +1 -0
  103. package/dist/modules/browser/driver.js +19 -11
  104. package/dist/modules/browser/index.js +3 -1
  105. package/dist/modules/browser/module.js +2 -1
  106. package/dist/modules/browser/session.js +10 -30
  107. package/dist/modules/browser/snapshot.js +19 -13
  108. package/dist/modules/browser/types.js +1 -0
  109. package/dist/modules/certification/cli.js +28 -20
  110. package/dist/modules/certification/fact-checker.js +9 -1
  111. package/dist/modules/certification/loader.js +9 -2
  112. package/dist/modules/certification/manifest.js +83 -21
  113. package/dist/modules/certification/runner.js +23 -13
  114. package/dist/modules/certification/scenarios.js +20 -20
  115. package/dist/modules/certification/syntax-scenarios.js +1 -2
  116. package/dist/modules/context/chunk-query.js +11 -9
  117. package/dist/modules/context/fact-extractor.js +37 -39
  118. package/dist/modules/context/index.js +1 -0
  119. package/dist/modules/context/manager.js +106 -80
  120. package/dist/modules/critic/client.js +176 -0
  121. package/dist/modules/critic/evidence.js +81 -0
  122. package/dist/modules/critic/provider.js +45 -0
  123. package/dist/modules/critic/tools.js +78 -0
  124. package/dist/modules/critic/types.js +1 -0
  125. package/dist/modules/execution/audit-runners.js +28 -34
  126. package/dist/modules/execution/auditor.js +53 -49
  127. package/dist/modules/execution/execution-plugin.js +142 -102
  128. package/dist/modules/execution/index.js +1 -1
  129. package/dist/modules/execution/input-from.js +37 -13
  130. package/dist/modules/execution/module.js +368 -191
  131. package/dist/modules/execution/moe-executor.js +53 -26
  132. package/dist/modules/execution/plan-actions.js +320 -202
  133. package/dist/modules/execution/plan-checks.js +35 -0
  134. package/dist/modules/execution/plan-coverage.js +36 -33
  135. package/dist/modules/execution/plan-steps.js +11 -3
  136. package/dist/modules/execution/plan-store.js +83 -17
  137. package/dist/modules/execution/plan-tool.js +89 -35
  138. package/dist/modules/execution/plan-validator.js +30 -10
  139. package/dist/modules/execution/planner.js +22 -13
  140. package/dist/modules/execution/stuck-detector.js +196 -93
  141. package/dist/modules/execution/tracker.js +22 -8
  142. package/dist/modules/execution/verifier.js +69 -48
  143. package/dist/modules/execution/windows-commands.js +12 -12
  144. package/dist/modules/hallucination/artifact-claim.js +93 -0
  145. package/dist/modules/hallucination/confidence.js +13 -11
  146. package/dist/modules/hallucination/consistency.js +10 -2
  147. package/dist/modules/hallucination/detector.js +10 -5
  148. package/dist/modules/hallucination/factual.js +43 -26
  149. package/dist/modules/hallucination/index.js +1 -0
  150. package/dist/modules/hallucination/js-identifiers.js +44 -45
  151. package/dist/modules/hallucination/llm-judge.js +17 -12
  152. package/dist/modules/hallucination/success-claim.js +59 -0
  153. package/dist/modules/indexer/cache.js +52 -4
  154. package/dist/modules/indexer/index.js +1 -0
  155. package/dist/modules/indexer/map-command.js +2 -4
  156. package/dist/modules/indexer/map-select.js +16 -16
  157. package/dist/modules/indexer/module.js +47 -12
  158. package/dist/modules/indexer/project-profile.js +8 -7
  159. package/dist/modules/indexer/symbols.js +15 -15
  160. package/dist/modules/indexer/walker.js +159 -29
  161. package/dist/modules/lsp/check-tool.js +7 -5
  162. package/dist/modules/lsp/client.js +39 -39
  163. package/dist/modules/lsp/command.js +17 -24
  164. package/dist/modules/lsp/config.js +12 -11
  165. package/dist/modules/lsp/index.js +1 -0
  166. package/dist/modules/lsp/module.js +48 -46
  167. package/dist/modules/lsp/probe.js +13 -14
  168. package/dist/modules/lsp/project-root.js +14 -17
  169. package/dist/modules/lsp/startup-check.js +18 -16
  170. package/dist/modules/mcp/client.js +59 -62
  171. package/dist/modules/mcp/index.js +1 -0
  172. package/dist/modules/mcp/module.js +66 -37
  173. package/dist/modules/mcp/registry.js +5 -0
  174. package/dist/modules/memory/index.js +1 -0
  175. package/dist/modules/memory/module.js +12 -8
  176. package/dist/modules/memory/search.js +4 -1
  177. package/dist/modules/memory/store.js +40 -2
  178. package/dist/modules/onboarding/init.js +47 -0
  179. package/dist/modules/onboarding/meet.js +30 -0
  180. package/dist/modules/onboarding/mode.js +30 -0
  181. package/dist/modules/onboarding/prompts.js +43 -0
  182. package/dist/modules/pipelines/engine.js +10 -0
  183. package/dist/modules/pipelines/index.js +1 -0
  184. package/dist/modules/pipelines/parser.js +4 -1
  185. package/dist/modules/pipelines/template.js +4 -0
  186. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  187. package/dist/modules/plugins/builtin/notify.js +2 -0
  188. package/dist/modules/plugins/index.js +1 -0
  189. package/dist/modules/plugins/loader.js +18 -3
  190. package/dist/modules/plugins/manager.js +37 -17
  191. package/dist/modules/pricing/index.js +10 -11
  192. package/dist/modules/pricing/prices.js +12 -12
  193. package/dist/modules/processes/index.js +1 -0
  194. package/dist/modules/processes/kill-tree.js +27 -31
  195. package/dist/modules/processes/registry.js +44 -25
  196. package/dist/modules/processes/runner.js +4 -4
  197. package/dist/modules/providers/capabilities.js +28 -0
  198. package/dist/modules/providers/create.js +3 -4
  199. package/dist/modules/providers/factory.js +7 -7
  200. package/dist/modules/providers/fallback.js +12 -12
  201. package/dist/modules/providers/health.js +4 -4
  202. package/dist/modules/providers/manager.js +50 -19
  203. package/dist/modules/providers/naming.js +15 -0
  204. package/dist/modules/providers/preflight.js +71 -0
  205. package/dist/modules/providers/presets.js +39 -8
  206. package/dist/modules/providers/registry.js +7 -2
  207. package/dist/modules/reasoning/policy.js +8 -6
  208. package/dist/modules/reasoning/probe.js +30 -25
  209. package/dist/modules/security/audit-log.js +19 -36
  210. package/dist/modules/security/audit-notifier.js +35 -75
  211. package/dist/modules/security/command-validator.js +99 -51
  212. package/dist/modules/security/content-scanner.js +8 -8
  213. package/dist/modules/security/data-sanitizer.js +9 -16
  214. package/dist/modules/security/encryption.js +32 -52
  215. package/dist/modules/security/index.js +2 -1
  216. package/dist/modules/security/network-validator.js +120 -43
  217. package/dist/modules/security/path-validator.js +61 -46
  218. package/dist/modules/security/protected-paths.js +73 -0
  219. package/dist/modules/security/rate-limiter.js +17 -49
  220. package/dist/modules/security/security-policies.js +25 -57
  221. package/dist/modules/security/session-encryption.js +32 -71
  222. package/dist/modules/security/session-isolation.js +15 -28
  223. package/dist/modules/session/auto-name.js +63 -0
  224. package/dist/modules/session/export.js +98 -0
  225. package/dist/modules/session/index.js +1 -0
  226. package/dist/modules/session/manager.js +79 -28
  227. package/dist/modules/session/module.js +4 -3
  228. package/dist/modules/session/store.js +57 -20
  229. package/dist/modules/setup/answers.js +35 -0
  230. package/dist/modules/setup/apply.js +79 -0
  231. package/dist/modules/setup/discovery.js +72 -0
  232. package/dist/modules/skills/index.js +1 -0
  233. package/dist/modules/skills/loader.js +9 -1
  234. package/dist/modules/skills/module.js +11 -0
  235. package/dist/modules/updater/changelog-reader.js +15 -16
  236. package/dist/modules/updater/checker.js +9 -4
  237. package/dist/modules/updater/dev-detect.js +7 -6
  238. package/dist/modules/updater/index.js +1 -0
  239. package/dist/modules/updater/module.js +17 -15
  240. package/dist/modules/user-profile/compressor.js +2 -0
  241. package/dist/modules/user-profile/index.js +1 -0
  242. package/dist/modules/user-profile/profile.js +10 -1
  243. package/dist/modules/webui/answer-api.js +29 -0
  244. package/dist/modules/webui/assets.js +50 -0
  245. package/dist/modules/webui/body.js +21 -0
  246. package/dist/modules/webui/commands-settings.js +209 -0
  247. package/dist/modules/webui/commands-shared.js +60 -0
  248. package/dist/modules/webui/commands.js +174 -0
  249. package/dist/modules/webui/config-api.js +134 -0
  250. package/dist/modules/webui/file-api.js +202 -0
  251. package/dist/modules/webui/forward.js +159 -0
  252. package/dist/modules/webui/history-api.js +112 -0
  253. package/dist/modules/webui/host-state.js +60 -0
  254. package/dist/modules/webui/i18n-api.js +9 -0
  255. package/dist/modules/webui/limits.js +6 -0
  256. package/dist/modules/webui/log-forward.js +20 -0
  257. package/dist/modules/webui/memory-commands.js +64 -0
  258. package/dist/modules/webui/model-cache.js +18 -0
  259. package/dist/modules/webui/module.js +148 -0
  260. package/dist/modules/webui/persist.js +24 -0
  261. package/dist/modules/webui/prompt-io.js +97 -0
  262. package/dist/modules/webui/providers-api.js +337 -0
  263. package/dist/modules/webui/providers-view.js +56 -0
  264. package/dist/modules/webui/reasoning-view.js +15 -0
  265. package/dist/modules/webui/run-agent.js +81 -0
  266. package/dist/modules/webui/server.js +284 -0
  267. package/dist/modules/webui/sessions-api.js +178 -0
  268. package/dist/modules/webui/settings-api.js +561 -0
  269. package/dist/modules/webui/setup-api.js +201 -0
  270. package/dist/modules/webui/state.js +74 -0
  271. package/dist/modules/webui/util.js +261 -0
  272. package/dist/output/bus.js +7 -2
  273. package/dist/output/channel.js +80 -28
  274. package/dist/output/format.js +2 -1
  275. package/dist/output/index.js +1 -0
  276. package/dist/output/json-sink.js +5 -3
  277. package/dist/output/machine.js +4 -3
  278. package/dist/output/session-sink.js +4 -4
  279. package/dist/tools/approve.js +4 -0
  280. package/dist/tools/attach-image.js +5 -4
  281. package/dist/tools/auto-fixer.js +26 -44
  282. package/dist/tools/auto-tags.js +83 -0
  283. package/dist/tools/bash.js +193 -116
  284. package/dist/tools/browser.js +76 -31
  285. package/dist/tools/chunk-query.js +14 -6
  286. package/dist/tools/create-dir.js +9 -5
  287. package/dist/tools/delete-file.js +9 -5
  288. package/dist/tools/download-file.js +32 -7
  289. package/dist/tools/edit-file.js +52 -14
  290. package/dist/tools/enable-tools.js +6 -6
  291. package/dist/tools/executor.js +132 -27
  292. package/dist/tools/file-info.js +3 -1
  293. package/dist/tools/filter-tools.js +3 -3
  294. package/dist/tools/glob-tool.js +4 -1
  295. package/dist/tools/grep-tool.js +13 -10
  296. package/dist/tools/hidden-tools-block.js +6 -6
  297. package/dist/tools/index.js +16 -8
  298. package/dist/tools/list-dir.js +4 -2
  299. package/dist/tools/load-skill.js +7 -0
  300. package/dist/tools/mcp-call.js +15 -4
  301. package/dist/tools/meet-done.js +14 -0
  302. package/dist/tools/move-file.js +14 -7
  303. package/dist/tools/path-utils.js +29 -29
  304. package/dist/tools/pipeline-run.js +29 -7
  305. package/dist/tools/preview.js +1 -1
  306. package/dist/tools/process-ids.js +31 -0
  307. package/dist/tools/process-kill.js +9 -16
  308. package/dist/tools/process-list.js +1 -0
  309. package/dist/tools/process-log.js +11 -16
  310. package/dist/tools/process-wait.js +63 -0
  311. package/dist/tools/prompt-io.js +4 -3
  312. package/dist/tools/question.js +172 -25
  313. package/dist/tools/read-file.js +55 -34
  314. package/dist/tools/recall.js +14 -8
  315. package/dist/tools/registry.js +6 -5
  316. package/dist/tools/remember.js +8 -4
  317. package/dist/tools/scope-request.js +11 -6
  318. package/dist/tools/search-history.js +5 -2
  319. package/dist/tools/session-info.js +2 -0
  320. package/dist/tools/set-thinking.js +8 -4
  321. package/dist/tools/subagent.js +59 -39
  322. package/dist/tools/syntax-validator.js +292 -90
  323. package/dist/tools/user-input.js +22 -18
  324. package/dist/tools/web-browse.js +9 -3
  325. package/dist/tools/web-fetch.js +12 -23
  326. package/dist/tools/web-search.js +7 -5
  327. package/dist/tools/write-file.js +45 -15
  328. package/dist/ui/action-log.js +13 -0
  329. package/dist/ui/box.js +4 -3
  330. package/dist/ui/colors.js +2 -0
  331. package/dist/ui/diff.js +6 -5
  332. package/dist/ui/index.js +1 -0
  333. package/dist/ui/line-editor.js +230 -122
  334. package/dist/ui/line-math.js +10 -2
  335. package/dist/ui/md-formatter.js +57 -37
  336. package/dist/ui/output.js +8 -3
  337. package/dist/ui/plan-view.js +21 -10
  338. package/dist/ui/renderer.js +393 -67
  339. package/dist/ui/spinner.js +16 -5
  340. package/dist/ui/table.js +15 -17
  341. package/dist/ui/theme.js +143 -0
  342. package/dist/utils/child-run.js +20 -0
  343. package/dist/utils/error.js +1 -1
  344. package/dist/utils/index.js +2 -0
  345. package/dist/utils/retry.js +2 -2
  346. package/dist/utils/truncate.js +1 -1
  347. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
- import { ProviderManager } from "../modules/providers/manager";
1
+ import { ProviderManager, activeProviderEndpoint, reasoningStrategyForProvider } from "../modules/providers/manager";
2
2
  import { buildActiveProvider } from "../modules/providers/factory";
3
3
  import { t } from "../i18n/index";
4
- import { pc } from "../ui/colors";
4
+ import { errMsg } from "../utils";
5
+ import { resolveVerbosity } from "../ui/theme";
5
6
  import { join } from "path";
7
+ import { projectMmaDir } from "../config/paths";
6
8
  import { PromptBuilder } from "./prompt-builder";
7
9
  import { processRegistry } from "../modules/processes";
8
10
  import { SessionLogger } from "./session-logger";
@@ -15,20 +17,29 @@ import { createLoopState, clearIterationSignals } from "./agent/loop-state";
15
17
  import { CompactionService } from "./agent/compaction";
16
18
  import { TokenTracker } from "./agent/token-tracker";
17
19
  import { buildPromptSnapshot, diffPrompt } from "./agent/prefix-monitor";
20
+ import { recordArtifactWrite, testChurnFile } from "./agent/artifact-progress";
18
21
  import { ReasoningEffortResolver } from "./agent/reasoning-resolver";
22
+ import { repairToolCallPairing } from "./agent/tool-pairing";
19
23
  import { ContextRenderer } from "./agent/context-renderer";
20
24
  import { ToolBatchExecutor } from "./agent/tool-batch";
21
25
  import { HallucinationGate } from "./agent/hallucination-gate";
26
+ import { resolveArtifactStates } from "./agent/artifact-resolve";
22
27
  import { AuditGate } from "./agent/audit-gate";
28
+ import { CriticGate, gatherTurnToolEvents } from "./agent/critic-gate";
29
+ import { seedToolTags } from "../tools/auto-tags";
30
+ import { CriticClient } from "../modules/critic/client";
31
+ import { buildCriticProvider } from "../modules/critic/provider";
32
+ import { CRITIC_READONLY_TOOLS } from "../modules/critic/tools";
33
+ import { buildEvidencePack, isWorkMutation } from "../modules/critic/evidence";
34
+ import { deriveSessionName, generateSessionTitle, } from "../modules/session/auto-name";
23
35
  import { resolvePromptOverflow, overflowHint, HINT_BLOCK_TOKENS, } from "./prompt-overflow";
24
36
  import { MAX_EMPTY_RESPONSE_RETRIES, MAX_LLM_ERROR_RETRIES, MAX_REPEATED_TOOL_CALLS, MAX_REPEATED_TOOL_CALLS_INTERACTIVE, MUTATION_CYCLE_NUDGE_THRESHOLD, } from "./agent/constants";
25
37
  export { toolOutputCharLimit, compactToolError } from "./agent/tool-output";
26
38
  /**
27
- * Read-only introspection tools. Repeating one with identical arguments can
28
- * never make progress, so the interactive loop hard-stops after a few repeats
29
- * (observed: `session_info` called in a tight loop after the task was already
30
- * complete). Mutating tools stay nudge-only so a legitimate edit/build/read
31
- * cycle is never cut short.
39
+ * Read-only тулты-интроспекции. Повтор одного с теми же аргументами прогресса
40
+ * не даст никогда, поэтому интерактивный цикл жёстко стопает после нескольких
41
+ * повторов (наблюдалось: `session_info` в тугом цикле после готовой задачи).
42
+ * Мутирующие — только nudge, чтобы легитимный edit/build/read-цикл не резать.
32
43
  */
33
44
  const LOOP_PRONE_STATUS_TOOLS = new Set([
34
45
  "session_info",
@@ -37,6 +48,13 @@ const LOOP_PRONE_STATUS_TOOLS = new Set([
37
48
  "file_info",
38
49
  "project_map",
39
50
  ]);
51
+ function probeIdentityOf(config) {
52
+ return {
53
+ model: config.model,
54
+ baseUrl: activeProviderEndpoint(config.provider).baseUrl,
55
+ active: config.provider?.active,
56
+ };
57
+ }
40
58
  /**
41
59
  * Ключ целевого объекта мутации для детекции «перфекционизм-цикла».
42
60
  * edit_file/write_file → путь файла; bash → нормализованная команда.
@@ -65,14 +83,23 @@ export function mutationTargetKey(name, rawArgs) {
65
83
  const cmd = a.command.replace(/\s+/g, " ").trim().slice(0, 80).toLowerCase();
66
84
  return cmd ? `bash:${cmd}` : null;
67
85
  }
68
- // Bookkeeping churn: plan/todo repeated over the same action with slightly
69
- // different args burned 10+ iterations in ses_mthn3c2a after an audit
70
- // rejection (the model "fixed" the plan instead of the work).
86
+ // Bookkeeping-churn: повторы plan/todo над тем же действием с чуть разными
87
+ // аргументами сожгли 10+ итераций в ses_mthn3c2a после отклонения аудита
88
+ // (модель «чинила» план вместо работы).
71
89
  if ((name === "plan" || name === "todo") && typeof a.action === "string") {
72
90
  return `book:${name}:${a.action.toLowerCase()}`;
73
91
  }
92
+ // У plan_step нет action — ключ по status, чтобы повторные апдейты
93
+ // били в тот же гард подряд-дубликатов.
94
+ if (name === "plan_step" && typeof a.status === "string") {
95
+ return `book:${name}:${a.status.toLowerCase()}`;
96
+ }
74
97
  return null;
75
98
  }
99
+ /**
100
+ * Агент: владеет циклом, конфигом, стоимостью, гейтами. Состояние хода —
101
+ * в LoopState, шаги — в extracted-модулях.
102
+ */
76
103
  export class Agent {
77
104
  deps;
78
105
  systemPromptAdded = false;
@@ -83,18 +110,22 @@ export class Agent {
83
110
  * переживает несколько запусков цикла — хранит lastCompactionShown. */
84
111
  contextRenderer;
85
112
  policyState;
86
- /** Reasoning probe result (B2): starts from deps, refreshed lazily in run(). */
113
+ /** Итог reasoning-пробы (B2): старт из deps, лениво обновляется в run(). */
87
114
  probePassed;
88
- /** Shared mutable reasoning state (owned by agent, shared with set_thinking tool). */
115
+ /** Общее мутабельное reasoning-состояние (владелец — агент, делит с set_thinking). */
89
116
  reasoningState;
90
- /** Current iteration counter (exposed for tool cooldown checks). */
117
+ /** Счётчик текущей итерации (для кулдаунов тулов). */
91
118
  _currentIteration = 0;
92
- /** Kind → replacement block for instructions/project-map blocks that
93
- * exceeded the system budget (resolved once before the first LLM call). */
119
+ /** Kind → блок-замена для instructions/project-map сверх бюджета (резолв раз до первого LLM-вызова). */
94
120
  promptOverrides = new Map();
95
- /** Small always-included block explaining the compressed instructions. */
121
+ /** Идентичность, против которой гонялась проба. Сравнение ПО ЗНАЧЕНИЮ: все
122
+ * write-пути мутят живой конфиг in place и передают ТОТ ЖЕ объект в
123
+ * `reconfigure` — сравнение объектов изменения бы не увидело. Сидируется
124
+ * в конструкторе (строки до мутаций). */
125
+ lastProbeIdentity;
126
+ /** Малый всегда-включённый блок про сжатые инструкции. */
96
127
  overflowHintBlock = null;
97
- /** Guards the one-time async overflow resolution in run(). */
128
+ /** Гард одноразового асинхронного резолва переполнения в run(). */
98
129
  overflowResolved = false;
99
130
  constructor(deps) {
100
131
  this.deps = deps;
@@ -102,18 +133,19 @@ export class Agent {
102
133
  this.contextRenderer = new ContextRenderer();
103
134
  this.policyState = createPolicyState();
104
135
  this.probePassed = deps.reasoningProbePassed ?? false;
136
+ this.lastProbeIdentity = probeIdentityOf(deps.config);
105
137
  const initialLevel = (deps.config.reasoning?.mode !== "auto" ? deps.config.reasoning?.mode : undefined) ?? "medium";
106
138
  this.reasoningState = { level: initialLevel, overrideIteration: -100 };
107
139
  }
108
- /** Expose context manager for REPL image attachment and other direct access. */
140
+ /** Менеджер контекста наружу (аттач картинок в REPL и др. прямой доступ). */
109
141
  get contextManager() {
110
142
  return this.deps.contextManager;
111
143
  }
112
- /** Current effective reasoning level. */
144
+ /** Текущий эффективный уровень reasoning. */
113
145
  get reasoningLevel() {
114
146
  return this.reasoningState.level;
115
147
  }
116
- /** Set reasoning level (used by REPL and CLI). "auto" clears a manual override. */
148
+ /** Ставит уровень reasoning (REPL/CLI). "auto" снимает ручной override. */
117
149
  setReasoningLevel(level) {
118
150
  if (level === "auto") {
119
151
  this.reasoningState.manual = false;
@@ -122,15 +154,24 @@ export class Agent {
122
154
  this.reasoningState.level = level;
123
155
  this.reasoningState.manual = true;
124
156
  }
125
- /** Current iteration number (for tool cooldown checks). */
157
+ /** Номер текущей итерации (для кулдаунов тулов). */
126
158
  get currentIteration() {
127
159
  return this._currentIteration;
128
160
  }
161
+ /**
162
+ * Провенанс вызывающего: живой провайдер и модель (для стоимости/логов).
163
+ * @returns {provider, model} реально обслужившие вызов.
164
+ */
129
165
  callerProvenance() {
166
+ // Сначала живое имя: sticky failover переключает активную запись
167
+ // менеджера, не трогая config.provider.active.
168
+ const live = this.deps.getProviderName?.();
169
+ if (live)
170
+ return { provider: live, model: this.deps.config.model };
130
171
  const p = this.deps.config.provider;
131
172
  let providerName = p?.type || "unknown";
132
- // When provider.entries exist, use the active entry's label for
133
- // distinguishable cost provenance (e.g. "lmstudio" vs "ollama").
173
+ // При записях провайдера — метка активной, чтобы провенанс стоимости
174
+ // различался (напр. "lmstudio" vs "ollama").
134
175
  if (p?.entries && p.entries.length > 0) {
135
176
  const entry = p.active
136
177
  ? p.entries.find((e) => e.label === p.active || e.type === p.active)
@@ -140,7 +181,7 @@ export class Agent {
140
181
  }
141
182
  return { provider: providerName, model: this.deps.config.model };
142
183
  }
143
- /** Typed module accessor for CLI/REPL commands (e.g. /lsp). */
184
+ /** Типизированный доступ к модулям для CLI/REPL (напр. /lsp). */
144
185
  getModule(name) {
145
186
  return this.deps.moduleRegistry?.get(name);
146
187
  }
@@ -157,18 +198,46 @@ export class Agent {
157
198
  }
158
199
  }
159
200
  /**
160
- * Install (or clear) the terminal-aware prompt surface used by interactive
161
- * tools. Delegates to the tool executor so the shared ToolContext — the one
162
- * tools actually receive — carries the same instance.
201
+ * Ставит (или снимает) терминальную prompt-поверхность для интерактивных
202
+ * тулов. Делегирует исполнителю, чтобы общий ToolContext — тот, что реально
203
+ * получают тулы — нёс тот же инстанс.
163
204
  */
164
205
  setPromptIO(io) {
165
206
  this.deps.toolExecutor.setPromptIO(io);
166
207
  }
208
+ /**
209
+ * Дописывает системный блок на текущий прогон (гайд онбординга).
210
+ * Удаление по идентичности через `removePromptBlock`. Блоки читаются при
211
+ * каждой `buildSystemPrompt`, эффект — со следующего хода.
212
+ */
213
+ pushPromptBlock(block) {
214
+ this.deps.promptBlocks.push(block);
215
+ }
216
+ /** Убирает ранее запушенный блок (по ссылке). */
217
+ removePromptBlock(block) {
218
+ const i = this.deps.promptBlocks.indexOf(block);
219
+ if (i >= 0)
220
+ this.deps.promptBlocks.splice(i, 1);
221
+ }
222
+ /** Тестовый вид текущих блоков промпта. */
223
+ getPromptBlocksForTest() {
224
+ return this.deps.promptBlocks;
225
+ }
226
+ /** Общий ToolContext (бросает, если bootstrap его не дал). */
227
+ get toolContext() {
228
+ if (!this.deps.toolCtx)
229
+ throw new Error("toolCtx not provided to Agent");
230
+ return this.deps.toolCtx;
231
+ }
232
+ /** Небросающийся вариант `toolContext` для опциональных/lifecycle-проверок. */
233
+ tryToolContext() {
234
+ return this.deps.toolCtx;
235
+ }
167
236
  buildSystemPrompt() {
168
237
  const systemBudget = Math.floor(this.deps.config.contextWindow * this.deps.config.contextBudget.systemPrompt);
169
238
  const builder = new PromptBuilder(systemBudget);
170
- // Overflow replacements (compressed instructions / project map) take
171
- // precedence over the original oversized blocks.
239
+ // Замены переполнения (сжатые инструкции/карта) приоритетнее исходных
240
+ // негабаритных блоков.
172
241
  const applyOverrides = (blocks) => blocks.map((b) => (b.kind && this.promptOverrides.has(b.kind) ? this.promptOverrides.get(b.kind) : b));
173
242
  builder.addBlocks(applyOverrides(this.deps.promptBlocks));
174
243
  const dynamic = this.deps.getDynamicPromptBlocks?.() ?? [];
@@ -200,11 +269,9 @@ export class Agent {
200
269
  };
201
270
  }
202
271
  /**
203
- * Log the current context state to the session log. Called every iteration
204
- * so the session.jsonl shows a full timeline of how the context grows,
205
- * compacts, and what it consists of. The `start` snapshot additionally
206
- * carries the system-prompt block breakdown (which blocks, priorities,
207
- * token estimates, what was excluded by the budget).
272
+ * Пишет состояние контекста в лог сессии. Каждую итерацию — полный таймлайн
273
+ * роста/компакции/состава; `start`-снимок дополнительно несёт разбивку
274
+ * системных блоков (что, приоритеты, оценки, что срезал бюджет).
208
275
  */
209
276
  logContextStat(kind, iteration, slog, blocks) {
210
277
  const cm = this.deps.contextManager;
@@ -232,16 +299,19 @@ export class Agent {
232
299
  blocks,
233
300
  });
234
301
  }
302
+ /**
303
+ * Текущий системный промпт с подсчётом токенов (для /sysprompt, диагностики).
304
+ * @returns текст, число токенов, исключённые блоки.
305
+ */
235
306
  getSystemPromptInfo() {
236
307
  const { prompt, excluded } = this.buildSystemPrompt();
237
308
  const tokenCount = this.deps.llmProvider.countTokens(prompt);
238
309
  return { text: prompt, tokenCount, excluded };
239
310
  }
240
311
  /**
241
- * One-time (first run) handling of instructions/project-map blocks that did
242
- * not fit the system-prompt budget: they are summarized by the LLM into the
243
- * remaining budget (disk-cached) or truncated, and a small hint block tells
244
- * the model what happened and how to read the full source.
312
+ * Одноразовая (первый прогон) обработка instructions/project-map сверх
313
+ * бюджета: LLM суммаризирует в остаток (диск-кэш) или усекает, а малый
314
+ * hint-блок говорит модели что случилось и как читать полный источник.
245
315
  */
246
316
  async resolvePromptOverflowOnce() {
247
317
  const cfg = this.deps.config;
@@ -255,7 +325,7 @@ export class Agent {
255
325
  includedTokens: dry.blocks.filter((b) => b.included).reduce((s, b) => s + b.tokens, 0),
256
326
  systemBudget,
257
327
  provider: cfg.instructions?.summarize === false ? null : this.deps.llmProvider,
258
- cacheDir: join(this.deps.baseDir, ".mma", "cache", "prompt-summaries"),
328
+ cacheDir: join(projectMmaDir(this.deps.baseDir), "cache", "prompt-summaries"),
259
329
  logger: this.deps.logger,
260
330
  });
261
331
  for (const r of res.replacements) {
@@ -294,26 +364,42 @@ export class Agent {
294
364
  }, phase);
295
365
  onPhase?.(phase);
296
366
  }
297
- async run(input, onChunk, onMeta, onTool, onPhase) {
298
- // Reset shutdown flag from previous interrupt
367
+ /**
368
+ * Прогоняет ход агента: MoE-путь или одиночный цикл, с автонеймингом сессии.
369
+ * @param input сообщение пользователя; @param onChunk/onMeta/onTool/onPhase/onReasoning колбэки вывода;
370
+ * @returns итог прогона.
371
+ */
372
+ async run(input, onChunk, onMeta, onTool, onPhase, onReasoning) {
373
+ // Сброс флага shutdown от прошлого прерывания
299
374
  this.shutdownRequested = false;
300
- // Fresh controller per turn — the MoE path relies on it for Esc handling
301
- // (executeSingleAgentLoop re-creates it again for the single-agent path).
375
+ // Свежий контроллер на ход — MoE-путь полагается на него для Esc
376
+ // (executeSingleAgentLoop пересоздаёт его же для одиночного пути).
302
377
  this.abortController = new AbortController();
303
378
  this.setScope();
304
- const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, baseDir, } = this.deps;
379
+ // P5 автосужение: детерминированно пред-посеваем теги тулов из сырого
380
+ // сообщения ДО первого LLM-вызова (системный промпт + схемы тулов).
381
+ // Только добавление — mid-session гранты enable_tools не снимаются.
382
+ // Пропуск при выключенном сужении (enableOnDemand: false → все тулы).
383
+ if (this.deps.config.tools?.enableOnDemand !== false && this.deps.toolTags) {
384
+ const added = seedToolTags(this.deps.toolTags, input);
385
+ if (added.length > 0) {
386
+ this.deps.logger.debug(`Auto-narrowing: pre-seeded tool tags [${added.join(", ")}]`);
387
+ }
388
+ }
389
+ const { config, llmProvider, toolExecutor, pluginManager, contextManager, logger, sessionManager, } = this.deps;
305
390
  const slog = new SessionLogger(sessionManager, logger, () => this.callerProvenance());
306
391
  if (sessionManager && !sessionManager.getActive()) {
307
392
  sessionManager.create();
308
393
  logger.debug(`Session started: ${sessionManager.getActive()}`);
309
394
  }
395
+ const activeMeta = sessionManager?.getActiveMeta();
396
+ const isFirstMessage = !!activeMeta && activeMeta.messageCount === 0;
310
397
  if (!this.systemPromptAdded &&
311
398
  !contextManager.getActiveHistory().some((m) => m.role === "system")) {
312
- // Log session startup diagnostics (environment, plugins, skills) as the
313
- // first entry in session.jsonl so the file is self-describing for
314
- // post-mortem analysis. LSP probe is logged separately from repl.ts
315
- // since it may complete after this point. Dedup: logSessionStart
316
- // checks for existing session_start entry to avoid double-logging.
399
+ // Стартовую диагностику сессии (окружение, плагины, скиллы) — первой
400
+ // записью в session.jsonl, чтобы файл был самоописываем для пост-анализа.
401
+ // LSP-проба логируется отдельно из repl.ts (может завершиться позже).
402
+ // Дедуп: logSessionStart проверяет существующий session_start от дублей.
317
403
  slog.logSessionStart({
318
404
  environment: this.deps.envReport,
319
405
  });
@@ -321,9 +407,8 @@ export class Agent {
321
407
  if (lazy.length > 0) {
322
408
  this.deps.promptBlocks.push(...lazy);
323
409
  }
324
- // One-time resolution of instructions/project-map blocks that exceed
325
- // the system-prompt budget: summarize (cached) or truncate them BEFORE
326
- // the first real build so the model still receives the instructions.
410
+ // Одноразовый резолв instructions/project-map сверх бюджета: суммаризация
411
+ // (кэш) или усечение ДО первой сборки, чтобы модель инструкции получила.
327
412
  if (!this.overflowResolved) {
328
413
  this.overflowResolved = true;
329
414
  try {
@@ -333,9 +418,8 @@ export class Agent {
333
418
  this.deps.logger.warn(t("prompt.overflow.failed", { error: String(err?.message ?? err) }));
334
419
  }
335
420
  }
336
- // B2: await the background reasoning probe next to the lazy prompt
337
- // blocks so the FIRST LLM call already knows whether the backend
338
- // respects the reasoning mechanism (bootstrap no longer blocks on it).
421
+ // B2: ждём фоновую reasoning-пробу рядом с ленивыми блоками, чтобы ПЕРВЫЙ
422
+ // LLM-вызов уже знал, держит ли бэкенд механизм (bootstrap больше не ждёт).
339
423
  if (this.deps.reasoningProbe) {
340
424
  this.probePassed = await this.deps.reasoningProbe();
341
425
  }
@@ -352,17 +436,42 @@ export class Agent {
352
436
  contextManager,
353
437
  });
354
438
  }
355
- // A new user message starts a fresh compaction interval — iterations
356
- // from the previous turn must not trigger a compaction on iteration 1
357
- // of this one (observed: turn 2 compacted at iteration 8, deleting the
358
- // just-sent task along with 40+ old turns).
439
+ // Новое сообщение — новый интервал компакции: итерации прошлого хода не
440
+ // должны триггерить компакцию на итерации 1 этого (наблюдалось: ход 2
441
+ // скомпактился на итерации 8, снеся только что присланную задачу с 40+ ходами).
359
442
  if (typeof contextManager.resetUserTurn === "function") {
360
443
  contextManager.resetUserTurn();
361
444
  }
362
445
  contextManager.addMessage({ role: "user", content: input });
363
446
  slog.logUser(input);
364
- // Turn lifecycle for plugins: count tool events without touching the loop,
365
- // and dispatch onTurnEnd exactly once — success, error or interrupt.
447
+ if (isFirstMessage && activeMeta && sessionManager) {
448
+ const meta = sessionManager.getActiveMeta();
449
+ if (meta && (meta.nameSource ?? "default") !== "manual") {
450
+ sessionManager.rename(meta.id, deriveSessionName(input), "auto");
451
+ if (config.session.autoName !== false) {
452
+ const chat = async (prompt, signal) => {
453
+ let text = "";
454
+ for await (const chunk of llmProvider.chat([{ role: "user", content: prompt }], undefined, signal, { maxTokens: 48, reasoningEffort: "none" })) {
455
+ if (chunk.type === "text" && chunk.content)
456
+ text += chunk.content;
457
+ }
458
+ return text;
459
+ };
460
+ void generateSessionTitle(input, chat)
461
+ .then((title) => {
462
+ if (!title)
463
+ return;
464
+ const cur = sessionManager.getActiveMeta();
465
+ if (cur && cur.id === meta.id && (cur.nameSource ?? "default") === "auto") {
466
+ sessionManager.rename(meta.id, title, "auto");
467
+ }
468
+ })
469
+ .catch(() => { });
470
+ }
471
+ }
472
+ }
473
+ // Жизненный цикл хода для плагинов: считаем тул-события вне цикла,
474
+ // onTurnEnd — ровно один раз (успех, ошибка или прерывание).
366
475
  const startedAt = Date.now();
367
476
  let toolCalls = 0;
368
477
  const countTool = (ev) => {
@@ -394,7 +503,7 @@ export class Agent {
394
503
  toolExecutor,
395
504
  logger,
396
505
  baseDir: this.deps.baseDir,
397
- }, input, () => this.executeSingleAgentLoop(input, slog, onChunk, onMeta, countTool, onPhase), {
506
+ }, input, () => this.executeSingleAgentLoop(input, slog, onChunk, onMeta, countTool, onPhase, onReasoning), {
398
507
  onMeta,
399
508
  onTool: countTool,
400
509
  onPhase,
@@ -402,11 +511,11 @@ export class Agent {
402
511
  onEvent: (event) => slog.logMoE(event.type, event.data),
403
512
  sessionId: this.deps.sessionManager?.getActiveMeta()?.id,
404
513
  })
405
- : await this.executeSingleAgentLoop(input, slog, onChunk, onMeta, countTool, onPhase);
406
- // In MoE mode the single-agent loop (the only writer of assistant turns)
407
- // never runs — without this the history ends user→user across turns and
408
- // session.jsonl is missing the assistant answer. Write the result once,
409
- // only when MoE actually handled the turn (a fallback already wrote it).
514
+ : await this.executeSingleAgentLoop(input, slog, onChunk, onMeta, countTool, onPhase, onReasoning);
515
+ // В MoE-режиме одиночный цикл (единственный писатель ходов ассистента)
516
+ // не бежит — без этого история схлопнется user→user между ходами, а в
517
+ // session.jsonl не будет ответа. Пишем итог один раз и только когда MoE
518
+ // реально вёл ход (фолбэк уже записал сам).
410
519
  if (moeEnabled && result.moeHandled) {
411
520
  if (result.text) {
412
521
  contextManager.addMessage({ role: "assistant", content: result.text });
@@ -431,7 +540,7 @@ export class Agent {
431
540
  throw err;
432
541
  }
433
542
  }
434
- async executeSingleAgentLoop(input, slog, onChunk, onMeta, onTool, onPhase) {
543
+ async executeSingleAgentLoop(input, slog, onChunk, onMeta, onTool, onPhase, onReasoning) {
435
544
  const { config, llmProvider, toolExecutor, pluginManager, contextManager, hallucinationDetector, logger, } = this.deps;
436
545
  this.abortController = new AbortController();
437
546
  // Единый контейнер состояния цикла — заменяет ~24 разрозненных `let`.
@@ -444,7 +553,7 @@ export class Agent {
444
553
  slog,
445
554
  hallucinationDetector,
446
555
  });
447
- const tokenTracker = new TokenTracker(this.costTracker, () => this.callerProvenance().provider);
556
+ const tokenTracker = new TokenTracker(this.costTracker, () => this.callerProvenance());
448
557
  const reasoningResolver = new ReasoningEffortResolver();
449
558
  const toolBatch = new ToolBatchExecutor({
450
559
  config,
@@ -463,11 +572,56 @@ export class Agent {
463
572
  });
464
573
  const hallucinationGate = new HallucinationGate(hallucinationDetector);
465
574
  const auditGate = new AuditGate(this.deps.finalAudit);
466
- // Account for tool definitions in context budget (they're sent via body.tools, not messages)
467
- // Tool definitions are recomputed each iteration so `enable_tools`
468
- // (which mutates the shared activeToolTags array) can grow the
469
- // LLM-visible tool set mid-run. Bound per-iteration to keep the budget
470
- // estimate and boundedOutput set in sync with what is actually sent.
575
+ // Защита: частичные/легаси конфиги (тесты, старый config.json) могут не иметь
576
+ // домена `critic` — считаем выключенным, а не роняем цикл.
577
+ const criticConfig = {
578
+ enabled: config.critic?.enabled ?? false,
579
+ tools: config.critic?.tools ?? true,
580
+ maxCalls: config.critic?.maxCalls ?? 4,
581
+ maxRepairCycles: config.critic?.maxRepairCycles ?? 1,
582
+ timeoutMs: config.critic?.timeoutMs ?? 15000,
583
+ };
584
+ // Phase 2: выделенные провайдер/модель, если настроены, иначе активный
585
+ // провайдер (v1). Битый оверрайд — warn + фолбэк, никогда не молча.
586
+ const criticProvider = buildCriticProvider(config.critic, config, this.deps.llmProvider, {
587
+ logger,
588
+ getSessionId: () => this.deps.sessionManager?.getActiveMeta()?.id,
589
+ });
590
+ const criticClient = new CriticClient({
591
+ provider: criticProvider,
592
+ timeoutMs: criticConfig.timeoutMs,
593
+ });
594
+ const criticGate = new CriticGate({
595
+ enabled: criticConfig.enabled,
596
+ maxCalls: criticConfig.maxCalls,
597
+ maxRepairCycles: criticConfig.maxRepairCycles,
598
+ verify: this.deps.criticOverride?.verify ?? ((pack, signal) => criticClient.verify(pack, signal, {
599
+ // Phase 2 read-only цикл: проверки бегут последовательно здесь (гейт
600
+ // await'ится между пакетами тулов), через общий executor с read-only
601
+ // allowlist — записи/shell недостижимы.
602
+ runCheck: criticConfig.tools && this.deps.toolCtx
603
+ ? async (tool, args) => {
604
+ const res = await toolExecutor.executeByName(tool, args, {
605
+ ...this.deps.toolCtx,
606
+ toolAllowlist: [...CRITIC_READONLY_TOOLS],
607
+ });
608
+ return { ok: res.success, output: res.output ?? "" };
609
+ }
610
+ : undefined,
611
+ })),
612
+ buildPack: (d) => buildEvidencePack({
613
+ task: d.task,
614
+ planSummary: d.planSummary,
615
+ toolEvents: d.toolEvents,
616
+ baseDir: this.deps.baseDir,
617
+ }),
618
+ signal: this.abortController?.signal,
619
+ });
620
+ // Определения тулов учитываем в бюджете контекста (идут через body.tools, не messages).
621
+ // Пересчитываем каждую итерацию, чтобы `enable_tools` (мутит общий
622
+ // activeToolTags) мог растить видимый модели набор прямо в прогоне.
623
+ // Привязка к итерации держит оценку бюджета и boundedOutput в синхроне
624
+ // с реально отправляемым.
471
625
  let allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
472
626
  let boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
473
627
  let toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + estimateTokens(t.description + JSON.stringify(t.parameters)), 0);
@@ -476,7 +630,7 @@ export class Agent {
476
630
  state.iteration++;
477
631
  this._currentIteration = state.iteration;
478
632
  contextManager.noteIteration();
479
- // Re-read the mutable tag set in case enable_tools was called.
633
+ // Перечитываем мутабельный набор тегов — вдруг вызывали enable_tools.
480
634
  allToolsForBudget = toolExecutor.getToolDefinitions(this.deps.toolTags);
481
635
  boundedToolNames = new Set(allToolsForBudget.filter((t) => t.boundedOutput).map((t) => t.name));
482
636
  toolTokenEstimate = allToolsForBudget.reduce((sum, t) => sum + Math.ceil((t.description.length + JSON.stringify(t.parameters).length) / 4), 0);
@@ -494,13 +648,34 @@ export class Agent {
494
648
  // Единая точка компакции: интервал → качество (с cooldown) → переполнение.
495
649
  compactionService.compactIfNeeded(state);
496
650
  this.refreshSystemPrompt();
497
- const history = contextManager.getActiveHistory();
651
+ // Позиционная нормализация истории перед отправкой: вклинившийся user
652
+ // (step-gate/auto-advance плана) или осиротевший tool ломает контракт
653
+ // OpenAI и даёт 400 у строгих провайдеров. Новые прогоны порядок не
654
+ // портят (pendingMessages), проход лечит уже испорченные resume-истории.
655
+ const history = repairToolCallPairing(contextManager.getActiveHistory());
498
656
  // Монитор стабильности префикса: считаем до запроса, пишем в llm_usage
499
657
  // после. Char-оценка (chars/4) достаточна — важна динамика ratio и
500
658
  // причина расхождения, а не абсолютная точность токенов.
501
659
  const promptSnapshot = buildPromptSnapshot(history, allToolsForBudget);
502
660
  state.prefixDelta = diffPrompt(state.prevPrompt, promptSnapshot, estimateTokens);
503
661
  state.prevPrompt = promptSnapshot;
662
+ // Opt-in git-style context log: snapshot отправляемого контекста.
663
+ if (this.deps.contextLogger) {
664
+ try {
665
+ this.deps.contextLogger.record(history, allToolsForBudget, {
666
+ iteration: state.iteration,
667
+ ts: new Date().toISOString(),
668
+ systemTokens: estimateTokens(promptSnapshot.system),
669
+ toolTokens: estimateTokens(promptSnapshot.tools),
670
+ historyTokens: estimateTokens(promptSnapshot.history),
671
+ totalTokens: contextManager.getEstimatedTokens(),
672
+ contextWindow: config.contextWindow,
673
+ });
674
+ }
675
+ catch {
676
+ // Логирование контекста не должно ронять агент.
677
+ }
678
+ }
504
679
  if (state.prefixDelta.cause === "system" ||
505
680
  state.prefixDelta.cause === "tools" ||
506
681
  state.prefixDelta.cause === "history" ||
@@ -520,6 +695,7 @@ export class Agent {
520
695
  const toolCalls = [];
521
696
  let sawToolCall = false;
522
697
  let emittedReasoning = false;
698
+ let malformedToolArgs = 0;
523
699
  this.emitPhase(state.iteration, "thinking", onPhase);
524
700
  const llmStart = Date.now();
525
701
  const baseline = tokenTracker.beginIteration(state, contextManager.getEstimatedTokens());
@@ -554,7 +730,7 @@ export class Agent {
554
730
  break;
555
731
  if (chunk.type === "text" && chunk.content) {
556
732
  if (emittedReasoning && !textContent) {
557
- onMeta?.("\n\n");
733
+ onReasoning?.("\n\n");
558
734
  }
559
735
  textContent += chunk.content;
560
736
  const textOut = pluginManager.runOnText({ iteration: state.iteration, logger, contextManager }, chunk.content);
@@ -565,7 +741,7 @@ export class Agent {
565
741
  if (config.showReasoning) {
566
742
  const metaOut = pluginManager.runOnMeta({ iteration: state.iteration, logger, contextManager }, chunk.content);
567
743
  if (metaOut) {
568
- onMeta?.(pc.dim(metaOut));
744
+ onReasoning?.(metaOut);
569
745
  }
570
746
  emittedReasoning = true;
571
747
  }
@@ -577,7 +753,13 @@ export class Agent {
577
753
  parsedArgs = JSON.parse(chunk.toolCall.arguments);
578
754
  }
579
755
  catch {
756
+ // Битый JSON аргументов раньше молча превращался в `{}` —
757
+ // модель звала тул с пустыми параметрами и не понимала причины.
758
+ // Логируем и сообщаем модели, чтобы она повторила вызов.
580
759
  parsedArgs = {};
760
+ malformedToolArgs++;
761
+ logger.diagnostic("warn", `Invalid JSON in tool_call arguments for "${chunk.toolCall.name}" (${chunk.toolCall.arguments.length} chars) — passing {}`);
762
+ slog.logError(`tool_args_parse_error: tool="${chunk.toolCall.name}" tool_call_id="${chunk.toolCall.id}" chars=${chunk.toolCall.arguments.length}`);
581
763
  }
582
764
  toolCalls.push({
583
765
  id: chunk.toolCall.id,
@@ -593,18 +775,17 @@ export class Agent {
593
775
  tokenTracker.recordApiUsage(state, baseline, chunk.usage);
594
776
  }
595
777
  if (chunk.type === "warning" && chunk.content) {
596
- // Non-fatal provider notice (e.g. answer cut by the completion
597
- // cap). Rendered through the text stream so it lands after the
598
- // partial line without disturbing markdown formatting; it is NOT
599
- // added to the assistant context/history.
600
- logger.warn(`LLM warning: ${chunk.content}`);
778
+ // Нефатальное уведомление провайдера (напр., ответ срезан cap'ом).
779
+ // Рендерим через текстовый стрим, чтобы легло после частичной строки,
780
+ // не ломая markdown; в контекст/историю ассистента НЕ добавляем.
781
+ logger.diagnostic("warn", `LLM warning: ${chunk.content}`);
601
782
  onChunk?.(`\n\n> ⚠️ ${chunk.content}\n`);
602
783
  }
603
784
  }
604
785
  }
605
786
  catch (err) {
606
787
  if (this.shutdownRequested || err?.name === "AbortError") {
607
- logger.info("LLM call aborted (interrupt)");
788
+ logger.diagnostic("info", "LLM call aborted (interrupt)");
608
789
  break;
609
790
  }
610
791
  // Восстановимые ошибки LLM (например, токен-лимит на середине
@@ -614,7 +795,7 @@ export class Agent {
614
795
  if (err instanceof LlmError && err.recoverable && state.llmErrorRetries < MAX_LLM_ERROR_RETRIES) {
615
796
  state.iterRecoverableLlmError = true;
616
797
  state.llmErrorRetries++;
617
- logger.warn(`Recoverable LLM error, feeding back (${state.llmErrorRetries}/${MAX_LLM_ERROR_RETRIES}): ${err.message}`);
798
+ logger.diagnostic("warn", `Recoverable LLM error, feeding back (${state.llmErrorRetries}/${MAX_LLM_ERROR_RETRIES}): ${err.message}`);
618
799
  slog.logError(err.message);
619
800
  pluginManager.runOnError({ iteration: state.iteration, logger, contextManager }, err);
620
801
  contextManager.addMessage({
@@ -649,6 +830,14 @@ export class Agent {
649
830
  if (this.shutdownRequested) {
650
831
  break;
651
832
  }
833
+ // Сообщаем модели, что её аргументы тула были невалидным JSON: иначе она
834
+ // видит лишь «пустой» вызов и не знает, что нужно повторить корректно.
835
+ if (malformedToolArgs > 0) {
836
+ contextManager.addMessage({
837
+ role: "user",
838
+ content: `<system-summary>${malformedToolArgs} tool call(s) in your last response had invalid JSON arguments and were executed with empty parameters. Re-issue them with valid JSON.</system-summary>`,
839
+ });
840
+ }
652
841
  let llmResponse = null;
653
842
  if (sawToolCall) {
654
843
  llmResponse = { type: "tool_call", calls: toolCalls };
@@ -660,6 +849,22 @@ export class Agent {
660
849
  llmResponse = { type: "reasoning", content: reasoningContent };
661
850
  }
662
851
  pluginManager.runOnAfterThink({ iteration: state.iteration, logger, contextManager }, llmResponse);
852
+ // Аннотация вывода модели в context-log (reasoning/текст/tool_calls).
853
+ // НЕ входит в отправляемый контекст — пишется отдельным [OUTPUT]-блоком.
854
+ if (this.deps.contextLogger && (reasoningContent || textContent || toolCalls.length > 0)) {
855
+ try {
856
+ this.deps.contextLogger.recordOutput({
857
+ iteration: state.iteration,
858
+ ts: new Date().toISOString(),
859
+ reasoning: reasoningContent || undefined,
860
+ text: textContent || undefined,
861
+ toolCalls: toolCalls.map((tc) => ({ name: tc.name, arguments: tc.arguments })),
862
+ });
863
+ }
864
+ catch {
865
+ // Логирование контекста не должно ронять агент.
866
+ }
867
+ }
663
868
  // Детекция повторных одинаковых тулов (структурная, не по ключевым
664
869
  // словам). Сигнал питает reasoning policy для понижения effort на рутине.
665
870
  if (sawToolCall) {
@@ -668,27 +873,31 @@ export class Agent {
668
873
  .join("|");
669
874
  if (signature && signature === state.lastToolSignature) {
670
875
  state.iterRepetitive = true;
671
- // Nudge fires in ALL modes: an identical (tool,args) repeat is a
672
- // wasted iteration — observed with qwen3.5-9b re-running the same
673
- // verification command back-to-back after the answer was already
674
- // in the result. Mutating tools stay nudge-only in interactive
675
- // sessions (the user keeps control); status tools, which can never
676
- // make progress by repeating, hard-stop after a few identical calls.
677
- logger.info(`Repeated identical tool call detected (iteration ${state.iteration}) — nudging model to finish or change approach`);
876
+ // Nudge во ВСЕХ режимах: идентичный (тул,аргументы) повтор — пустая
877
+ // итерация (наблюдалось: qwen3.5-9b гонял ту же verify-команду подряд,
878
+ // когда ответ уже был в результате). Мутирующие в интерактиве —
879
+ // только nudge (контроль у пользователя); status-тулы, которым повтор
880
+ // прогресса не даст никогда, — жёсткий стоп после нескольких повторов.
881
+ logger.diagnostic("info", `Repeated identical tool call detected (iteration ${state.iteration}) — nudging model to finish or change approach`);
678
882
  contextManager.addMessage({
679
883
  role: "user",
680
884
  content: `<system-summary>You just called the same tool with identical arguments. If the task is done, answer with a final text response NOW. If the command failed, try a different approach.</system-summary>`,
681
885
  });
682
886
  state.repeatedToolCount++;
887
+ // Жёсткий стоп — только для status-тулов, которые не могут
888
+ // прогрессировать от повтора. Мутирующие/побочные тулы (например,
889
+ // bash) остаются nudge-only: пользователь мог явно попросить
890
+ // выполнить команду несколько раз, и обрыв дал бы пустой ответ.
891
+ const allStatusTools = toolCalls.every((tc) => LOOP_PRONE_STATUS_TOOLS.has(tc.name));
683
892
  if (this.deps.exitOnComplete) {
684
- if (state.repeatedToolCount >= MAX_REPEATED_TOOL_CALLS) {
685
- logger.debug("Exit-on-complete: repeated identical tool call, stopping");
893
+ if (state.repeatedToolCount >= MAX_REPEATED_TOOL_CALLS && allStatusTools) {
894
+ logger.debug("Exit-on-complete: repeated identical status tool call, stopping");
686
895
  break;
687
896
  }
688
897
  }
689
898
  else if (state.repeatedToolCount >= MAX_REPEATED_TOOL_CALLS_INTERACTIVE &&
690
- toolCalls.every((tc) => LOOP_PRONE_STATUS_TOOLS.has(tc.name))) {
691
- logger.warn(`Repeated identical status tool ${state.repeatedToolCount}× (iteration ${state.iteration}) — stopping to avoid an infinite loop`);
899
+ allStatusTools) {
900
+ logger.diagnostic("warn", `Repeated identical status tool ${state.repeatedToolCount}× (iteration ${state.iteration}) — stopping to avoid an infinite loop`);
692
901
  break;
693
902
  }
694
903
  }
@@ -705,6 +914,11 @@ export class Agent {
705
914
  const cycleNudges = [];
706
915
  if (sawToolCall) {
707
916
  for (const tc of toolCalls) {
917
+ // Прогресс тест/спек-артефактов (обобщённо, по пути файла).
918
+ if ((tc.name === "write_file" || tc.name === "edit_file") &&
919
+ typeof tc.arguments.path === "string") {
920
+ recordArtifactWrite(state.artifactChurn, tc.arguments.path);
921
+ }
708
922
  const target = mutationTargetKey(tc.name, tc.arguments);
709
923
  if (!target)
710
924
  continue;
@@ -716,6 +930,10 @@ export class Agent {
716
930
  cycleNudges.push(target);
717
931
  }
718
932
  }
933
+ for (const tc of toolCalls) {
934
+ if (isWorkMutation(tc.name))
935
+ state.mutationsSinceCritic++;
936
+ }
719
937
  }
720
938
  if (sawToolCall) {
721
939
  const batch = await toolBatch.execute(state, toolCalls, boundedToolNames, textContent, reasoningContent, input, onTool, onMeta, slog);
@@ -723,12 +941,23 @@ export class Agent {
723
941
  break;
724
942
  }
725
943
  if (cycleNudges.length > 0) {
726
- logger.info(`Perfectionism cycle detected (${cycleNudges.length} target(s) at ${MUTATION_CYCLE_NUDGE_THRESHOLD}+ mutations) — nudging model to finalize`);
944
+ logger.diagnostic("info", `Perfectionism cycle detected (${cycleNudges.length} target(s) at ${MUTATION_CYCLE_NUDGE_THRESHOLD}+ mutations) — nudging model to finalize`);
727
945
  contextManager.addMessage({
728
946
  role: "user",
729
947
  content: `<system-summary>You have already modified or re-run the same target many times (e.g. ${cycleNudges[0]}). If the latest output is correct, STOP polishing and answer with a final text response. More identical edits will not change the result.</system-summary>`,
730
948
  });
731
949
  }
950
+ // Тест/спек-churn: серия правок тестов без единой production-правки
951
+ // (обобщённо — см. artifact-progress.ts).
952
+ const churnFile = testChurnFile(state.artifactChurn);
953
+ if (churnFile) {
954
+ state.artifactChurn.nudged = true;
955
+ logger.diagnostic("info", `Test/spec artifact churn: "${churnFile}" repeatedly edited with no production changes — nudging to implement or change approach`);
956
+ contextManager.addMessage({
957
+ role: "user",
958
+ content: `<system-summary>${t("exec.test_churn_nudge", { file: churnFile })}</system-summary>`,
959
+ });
960
+ }
732
961
  this.contextRenderer.render(this.deps.config.ui, contextManager, onMeta);
733
962
  // Тулы означают, что модель продолжила работать, а не переотвечает —
734
963
  // послабление на повтор после аудита больше не действует.
@@ -745,6 +974,11 @@ export class Agent {
745
974
  contextManager,
746
975
  onMeta,
747
976
  onChunk,
977
+ // Строка о неуверенности — диагностика: только verbose-режим.
978
+ verbose: resolveVerbosity(this.deps.config.ui) === "verbose",
979
+ // Заземление заявлений на диск (отсутствующие/непарсящиеся файлы,
980
+ // поданные как созданные/валидные). Резолвится здесь — у гейта нет fs.
981
+ artifacts: resolveArtifactStates(this.deps.baseDir, textContent),
748
982
  });
749
983
  if (verdict.action === "block") {
750
984
  return {
@@ -786,7 +1020,7 @@ export class Agent {
786
1020
  if (!textContent?.trim()) {
787
1021
  if (state.emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
788
1022
  state.emptyResponseRetries++;
789
- logger.warn(`Empty response on iteration ${state.iteration} (retry ${state.emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
1023
+ logger.diagnostic("warn", `Empty response on iteration ${state.iteration} (retry ${state.emptyResponseRetries}/${MAX_EMPTY_RESPONSE_RETRIES})`);
790
1024
  contextManager.addMessage({
791
1025
  role: "user",
792
1026
  content: `<system-summary>Your previous response was empty. Answer the user's task now with a final text response or call a tool. Do not reply with reasoning only.</system-summary>`,
@@ -794,7 +1028,22 @@ export class Agent {
794
1028
  continue;
795
1029
  }
796
1030
  state.emptyResponseExhausted = true;
797
- logger.warn(`Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
1031
+ logger.diagnostic("warn", `Empty response retries exhausted after ${MAX_EMPTY_RESPONSE_RETRIES} attempts`);
1032
+ }
1033
+ const criticAction = await criticGate.evaluate({
1034
+ state,
1035
+ iteration: state.iteration,
1036
+ logger,
1037
+ contextManager,
1038
+ slog,
1039
+ task: input,
1040
+ planSummary: this.deps.planSummary?.() ?? undefined,
1041
+ toolEvents: gatherTurnToolEvents(contextManager),
1042
+ beforeFinish: true,
1043
+ });
1044
+ if (criticAction === "retry") {
1045
+ state.suppressRepetitionRetry = true;
1046
+ continue;
798
1047
  }
799
1048
  const auditVerdict = await auditGate.evaluate({
800
1049
  state,
@@ -812,8 +1061,6 @@ export class Agent {
812
1061
  // auditFailed уже выставлен вентилем.
813
1062
  break;
814
1063
  }
815
- // reject → continue: аудит уже добавил контекст и взвёл suppressRepetitionRetry.
816
- continue;
817
1064
  }
818
1065
  const tokensUsed = contextManager.getEstimatedTokens();
819
1066
  const budget = contextManager.getBudget();
@@ -838,13 +1085,15 @@ export class Agent {
838
1085
  };
839
1086
  }
840
1087
  return {
841
- success: state.emptyResponseExhausted || state.auditFailed ? false : true,
1088
+ success: !(state.emptyResponseExhausted || state.auditFailed || state.retryLoopStopped),
842
1089
  text: state.lastText,
843
1090
  error: state.emptyResponseExhausted
844
1091
  ? t("error.empty_response")
845
1092
  : state.auditFailed
846
1093
  ? t("error.audit_failed", { summary: state.lastAuditSummary })
847
- : undefined,
1094
+ : state.retryLoopStopped
1095
+ ? t("error.retry_loop_stopped", { count: state.consecutiveFailedBatches })
1096
+ : undefined,
848
1097
  iterationCount: state.iteration,
849
1098
  contextUsed: tokensUsed,
850
1099
  contextLimit: budget.history,
@@ -883,16 +1132,54 @@ export class Agent {
883
1132
  source: state.cacheSource,
884
1133
  };
885
1134
  }
1135
+ /** Сбрасывает контекст (следующий прогон пересоберёт системный промпт). */
886
1136
  clearContext() {
887
1137
  this.deps.contextManager.clear();
888
1138
  this.systemPromptAdded = false;
889
1139
  }
1140
+ /**
1141
+ * Применяет новый конфиг живьём: provider-first проба контекста, пересборка
1142
+ * провайдера, ресайз бюджетов, обновление стоимости.
1143
+ * @param config новый конфиг приложения.
1144
+ */
890
1145
  async reconfigure(config) {
891
1146
  const { TokenCounter } = await import("../llm/token-counter");
892
- const { provider: newProvider } = buildActiveProvider(config, this.deps.logger, {
1147
+ // Provider-first контекст: при смене модели/бэкенда спрашиваем новый
1148
+ // провайдер о реально отдаваемом окне ДО пересборки бюджетов вокруг
1149
+ // (возможно протухшего) настроенного значения. Ручные правки
1150
+ // `contextWindow` без смены идентичности уважаются как есть.
1151
+ // Fail-open: ошибки пробы хранят настроенное значение.
1152
+ try {
1153
+ const nextId = probeIdentityOf(config);
1154
+ const prevId = this.lastProbeIdentity;
1155
+ const identityChanged = prevId.model !== nextId.model ||
1156
+ prevId.baseUrl !== nextId.baseUrl ||
1157
+ prevId.active !== nextId.active;
1158
+ if (identityChanged) {
1159
+ const nextEndpoint = activeProviderEndpoint(config.provider);
1160
+ const { probeModel, probeProviderContext } = await import("../llm/model-loader");
1161
+ const lmProbe = await probeModel(nextEndpoint.baseUrl, config.model);
1162
+ const detected = (lmProbe ? { model: lmProbe.model, actual: lmProbe.contextLength } : null) ??
1163
+ (await probeProviderContext(nextEndpoint.baseUrl, config.model, {
1164
+ apiKey: nextEndpoint.apiKey,
1165
+ providerType: nextEndpoint.type,
1166
+ }));
1167
+ if (detected && detected.actual !== config.contextWindow) {
1168
+ this.deps.logger?.info(`Context probe: auto-set contextWindow from provider — model "${detected.model}" serves ${detected.actual} tokens (configured was ${config.contextWindow}).`);
1169
+ config.contextWindow = detected.actual;
1170
+ }
1171
+ this.lastProbeIdentity = nextId;
1172
+ }
1173
+ }
1174
+ catch {
1175
+ // Проба — best-effort, переключение модели/провайдера не блокируем никогда.
1176
+ }
1177
+ const built = buildActiveProvider(config, this.deps.logger, {
893
1178
  getSessionId: () => this.deps.sessionManager?.getActiveMeta()?.id,
894
1179
  });
1180
+ const newProvider = built.provider;
895
1181
  this.deps.llmProvider = newProvider;
1182
+ this.deps.getProviderName = () => built.manager.activeName;
896
1183
  this.deps.toolExecutor.updateProvider(newProvider);
897
1184
  const newTokenCounter = new TokenCounter(config.model);
898
1185
  this.deps.contextManager.resize(config.contextWindow, config.contextBudget, newTokenCounter);
@@ -900,7 +1187,25 @@ export class Agent {
900
1187
  if (this.deps.configRef)
901
1188
  this.deps.configRef.current = config;
902
1189
  this.costTracker.setModel(config.model);
1190
+ this.applyReasoningStrategy(config.provider);
1191
+ }
1192
+ /**
1193
+ * Пересчитывает стратегию reasoning из АКТИВНОЙ записи провайдера. Без этого
1194
+ * `/provider use` и смена модели оставляли старую стратегию (prompt-tag против
1195
+ * openai-effort) на весь сеанс. При "none" гасит probe-флаг, чтобы auto-режим
1196
+ * не полагался на устаревшую пробу.
1197
+ * @param provider конфиг провайдера (entries/active).
1198
+ */
1199
+ applyReasoningStrategy(provider) {
1200
+ const strategy = reasoningStrategyForProvider(provider);
1201
+ this.deps.reasoningStrategy = strategy;
1202
+ if (strategy === "none")
1203
+ this.probePassed = false;
903
1204
  }
1205
+ /**
1206
+ * Переключает активного провайдера (и модель опционально).
1207
+ * @param name имя записи провайдера; @param model новая модель.
1208
+ */
904
1209
  setProvider(name, model) {
905
1210
  const { manager, provider } = buildActiveProvider(this.deps.config, this.deps.logger, {
906
1211
  getSessionId: () => this.deps.sessionManager?.getActiveMeta()?.id,
@@ -908,9 +1213,15 @@ export class Agent {
908
1213
  manager.switch(name, model);
909
1214
  const providerCfg = manager.toConfig();
910
1215
  this.deps.config.provider = providerCfg;
1216
+ this.applyReasoningStrategy(providerCfg);
911
1217
  this.deps.llmProvider = provider;
1218
+ this.deps.getProviderName = () => manager.activeName;
912
1219
  this.deps.toolExecutor.updateProvider(provider);
913
1220
  }
1221
+ /**
1222
+ * Список записей провайдеров с флагом активности.
1223
+ * @returns записи с label и active.
1224
+ */
914
1225
  listProviders() {
915
1226
  const manager = new ProviderManager(this.deps.config.provider, {
916
1227
  contextWindow: this.deps.config.contextWindow,
@@ -924,7 +1235,7 @@ export class Agent {
924
1235
  active: e.label === activeName || e.type === activeName,
925
1236
  }));
926
1237
  }
927
- /** Build (or get cached) the provider for a named entry — health checks, routing. */
1238
+ /** Строит (или берёт кэшированный) провайдер именованной записи — health-чеки, роутинг. */
928
1239
  getProviderFor(name) {
929
1240
  try {
930
1241
  const manager = new ProviderManager(this.deps.config.provider, {
@@ -938,10 +1249,15 @@ export class Agent {
938
1249
  manager.switch(name);
939
1250
  return manager.active;
940
1251
  }
941
- catch {
1252
+ catch (err) {
1253
+ this.deps.logger.warn(t("error.provider_build", { name, error: errMsg(err) }));
942
1254
  return undefined;
943
1255
  }
944
1256
  }
1257
+ /**
1258
+ * Заменяет контекст историей (resume): системный промпт + сообщения без дубля system.
1259
+ * @param messages сообщения для восстановления.
1260
+ */
945
1261
  setContext(messages) {
946
1262
  const { contextManager } = this.deps;
947
1263
  contextManager.clear();
@@ -958,6 +1274,11 @@ export class Agent {
958
1274
  });
959
1275
  }
960
1276
  }
1277
+ /**
1278
+ * Останавливает прогон: abort, kill фоновых процессов, onSessionEnd.
1279
+ * Лог сессии НЕ закрываем — сессия ещё активна (закрытие только на
1280
+ * реальном конце: exit, delete).
1281
+ */
961
1282
  shutdown() {
962
1283
  this.shutdownRequested = true;
963
1284
  this.abortController?.abort();
@@ -967,8 +1288,8 @@ export class Agent {
967
1288
  if (killed > 0) {
968
1289
  logger.info(t("env.killed_processes", { count: String(killed) }));
969
1290
  }
970
- // Note: do NOT close session log here — the session is still active.
971
- // Session log is closed only when the session actually ends (exit, session delete).
1291
+ // Сессионный лог тут НЕ закрываем — сессия ещё активна.
1292
+ // Закрытие только на реальном конце (exit, удаление сессии).
972
1293
  pluginManager.runOnSessionEnd({
973
1294
  logger,
974
1295
  sessionManager: sessionManager?.getActiveMeta(),