@itphx/synth-core 0.2.0 → 0.3.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 (510) hide show
  1. package/definitions/protected-paths.json +36 -0
  2. package/definitions/shell-policy.json +36 -0
  3. package/dist/definitions/protected-paths.json +36 -0
  4. package/dist/definitions/shell-policy.json +36 -0
  5. package/dist/engine/agents/definitions/admin.json +3 -1
  6. package/dist/engine/agents/definitions/analyst.json +3 -1
  7. package/dist/engine/agents/definitions/assistant.json +2 -2
  8. package/dist/engine/agents/definitions/auditor.json +4 -1
  9. package/dist/engine/agents/definitions/developer.json +4 -1
  10. package/dist/engine/agents/definitions/devops.json +4 -1
  11. package/dist/engine/agents/definitions/explore.json +3 -3
  12. package/dist/engine/agents/definitions/general.json +2 -2
  13. package/dist/engine/agents/definitions/pm.json +5 -1
  14. package/dist/engine/agents/definitions/qa.json +4 -1
  15. package/dist/engine/agents/definitions/title.json +2 -2
  16. package/dist/engine/agents/index.d.ts +4 -0
  17. package/dist/engine/agents/index.d.ts.map +1 -1
  18. package/dist/engine/agents/index.js +9 -3
  19. package/dist/engine/agents/index.js.map +1 -1
  20. package/dist/engine/agents/mode.d.ts +5 -3
  21. package/dist/engine/agents/mode.d.ts.map +1 -1
  22. package/dist/engine/agents/mode.js +6 -6
  23. package/dist/engine/agents/mode.js.map +1 -1
  24. package/dist/engine/agents/mode.test.js +9 -3
  25. package/dist/engine/agents/mode.test.js.map +1 -1
  26. package/dist/engine/agents/normalize.d.ts +9 -0
  27. package/dist/engine/agents/normalize.d.ts.map +1 -0
  28. package/dist/engine/agents/normalize.js +22 -0
  29. package/dist/engine/agents/normalize.js.map +1 -0
  30. package/dist/engine/agents/normalize.test.d.ts +2 -0
  31. package/dist/engine/agents/normalize.test.d.ts.map +1 -0
  32. package/dist/engine/agents/normalize.test.js +37 -0
  33. package/dist/engine/agents/normalize.test.js.map +1 -0
  34. package/dist/engine/agents/registry.d.ts +2 -1
  35. package/dist/engine/agents/registry.d.ts.map +1 -1
  36. package/dist/engine/agents/registry.js +7 -6
  37. package/dist/engine/agents/registry.js.map +1 -1
  38. package/dist/engine/agents/registry.test.js +24 -0
  39. package/dist/engine/agents/registry.test.js.map +1 -1
  40. package/dist/engine/context/config.d.ts +43 -6
  41. package/dist/engine/context/config.d.ts.map +1 -1
  42. package/dist/engine/context/config.js +123 -26
  43. package/dist/engine/context/config.js.map +1 -1
  44. package/dist/engine/context/config.security.test.d.ts +2 -0
  45. package/dist/engine/context/config.security.test.d.ts.map +1 -0
  46. package/dist/engine/context/config.security.test.js +82 -0
  47. package/dist/engine/context/config.security.test.js.map +1 -0
  48. package/dist/engine/context/config.test.js +199 -10
  49. package/dist/engine/context/config.test.js.map +1 -1
  50. package/dist/engine/context/optimizer.d.ts +20 -1
  51. package/dist/engine/context/optimizer.d.ts.map +1 -1
  52. package/dist/engine/context/optimizer.js +63 -29
  53. package/dist/engine/context/optimizer.js.map +1 -1
  54. package/dist/engine/context/optimizer.test.js +156 -0
  55. package/dist/engine/context/optimizer.test.js.map +1 -1
  56. package/dist/engine/gateway/gateway-provider.d.ts +4 -0
  57. package/dist/engine/gateway/gateway-provider.d.ts.map +1 -1
  58. package/dist/engine/gateway/gateway-provider.js +7 -2
  59. package/dist/engine/gateway/gateway-provider.js.map +1 -1
  60. package/dist/engine/gateway/gateway-provider.test.js +3 -3
  61. package/dist/engine/gateway/gateway-provider.test.js.map +1 -1
  62. package/dist/engine/gateway/input-guard.d.ts.map +1 -1
  63. package/dist/engine/gateway/input-guard.js +2 -1
  64. package/dist/engine/gateway/input-guard.js.map +1 -1
  65. package/dist/engine/gateway/secret-detector.d.ts.map +1 -1
  66. package/dist/engine/gateway/secret-detector.js +2 -3
  67. package/dist/engine/gateway/secret-detector.js.map +1 -1
  68. package/dist/engine/gateway/secret-detector.test.js +4 -4
  69. package/dist/engine/gateway/secret-detector.test.js.map +1 -1
  70. package/dist/engine/llm-error.d.ts +23 -0
  71. package/dist/engine/llm-error.d.ts.map +1 -0
  72. package/dist/engine/llm-error.js +86 -0
  73. package/dist/engine/llm-error.js.map +1 -0
  74. package/dist/engine/llm-error.test.d.ts +2 -0
  75. package/dist/engine/llm-error.test.d.ts.map +1 -0
  76. package/dist/engine/llm-error.test.js +101 -0
  77. package/dist/engine/llm-error.test.js.map +1 -0
  78. package/dist/engine/orchestrator-deny-abort.test.d.ts +2 -0
  79. package/dist/engine/orchestrator-deny-abort.test.d.ts.map +1 -0
  80. package/dist/engine/orchestrator-deny-abort.test.js +414 -0
  81. package/dist/engine/orchestrator-deny-abort.test.js.map +1 -0
  82. package/dist/engine/orchestrator-error.test.d.ts +2 -0
  83. package/dist/engine/orchestrator-error.test.d.ts.map +1 -0
  84. package/dist/engine/orchestrator-error.test.js +136 -0
  85. package/dist/engine/orchestrator-error.test.js.map +1 -0
  86. package/dist/engine/orchestrator.d.ts +123 -10
  87. package/dist/engine/orchestrator.d.ts.map +1 -1
  88. package/dist/engine/orchestrator.js +791 -181
  89. package/dist/engine/orchestrator.js.map +1 -1
  90. package/dist/engine/orchestrator.test.js +723 -0
  91. package/dist/engine/orchestrator.test.js.map +1 -1
  92. package/dist/engine/permission/approval.d.ts.map +1 -1
  93. package/dist/engine/permission/approval.js +29 -10
  94. package/dist/engine/permission/approval.js.map +1 -1
  95. package/dist/engine/permission/approval.test.js +57 -0
  96. package/dist/engine/permission/approval.test.js.map +1 -1
  97. package/dist/engine/permission/evaluator.d.ts +13 -0
  98. package/dist/engine/permission/evaluator.d.ts.map +1 -1
  99. package/dist/engine/permission/evaluator.js +121 -22
  100. package/dist/engine/permission/evaluator.js.map +1 -1
  101. package/dist/engine/permission/evaluator.test.js +188 -4
  102. package/dist/engine/permission/evaluator.test.js.map +1 -1
  103. package/dist/engine/permission/groups.d.ts.map +1 -1
  104. package/dist/engine/permission/groups.js +19 -1
  105. package/dist/engine/permission/groups.js.map +1 -1
  106. package/dist/engine/permission/integration.test.js +165 -4
  107. package/dist/engine/permission/integration.test.js.map +1 -1
  108. package/dist/engine/plan.integration.test.js +142 -33
  109. package/dist/engine/plan.integration.test.js.map +1 -1
  110. package/dist/engine/router/micro/tasks/classify-intent.d.ts.map +1 -1
  111. package/dist/engine/router/micro/tasks/classify-intent.js +24 -18
  112. package/dist/engine/router/micro/tasks/classify-intent.js.map +1 -1
  113. package/dist/engine/router/micro/tasks/classify-intent.test.js +4 -4
  114. package/dist/engine/router/micro/tasks/classify-intent.test.js.map +1 -1
  115. package/dist/engine/router/micro/tasks/enum-decision.d.ts.map +1 -1
  116. package/dist/engine/router/micro/tasks/enum-decision.js +14 -8
  117. package/dist/engine/router/micro/tasks/enum-decision.js.map +1 -1
  118. package/dist/engine/router/micro/tasks/enum-decision.test.js +2 -2
  119. package/dist/engine/router/micro/tasks/enum-decision.test.js.map +1 -1
  120. package/dist/engine/router/micro/tasks/small-talk.d.ts.map +1 -1
  121. package/dist/engine/router/micro/tasks/small-talk.js +20 -17
  122. package/dist/engine/router/micro/tasks/small-talk.js.map +1 -1
  123. package/dist/engine/router/micro/types.d.ts +2 -0
  124. package/dist/engine/router/micro/types.d.ts.map +1 -1
  125. package/dist/engine/router/provider-pool.d.ts +2 -2
  126. package/dist/engine/router/provider-pool.d.ts.map +1 -1
  127. package/dist/engine/router/provider-pool.js +5 -3
  128. package/dist/engine/router/provider-pool.js.map +1 -1
  129. package/dist/engine/router/quality-checker.d.ts.map +1 -1
  130. package/dist/engine/router/quality-checker.js +15 -14
  131. package/dist/engine/router/quality-checker.js.map +1 -1
  132. package/dist/engine/router/router.d.ts +2 -2
  133. package/dist/engine/router/router.d.ts.map +1 -1
  134. package/dist/engine/router/router.js +8 -5
  135. package/dist/engine/router/router.js.map +1 -1
  136. package/dist/engine/router/utils.d.ts +2 -2
  137. package/dist/engine/router/utils.d.ts.map +1 -1
  138. package/dist/engine/router/utils.js +2 -1
  139. package/dist/engine/router/utils.js.map +1 -1
  140. package/dist/engine/security/index.d.ts +6 -0
  141. package/dist/engine/security/index.d.ts.map +1 -1
  142. package/dist/engine/security/index.js +3 -0
  143. package/dist/engine/security/index.js.map +1 -1
  144. package/dist/engine/security/integration.test.js +11 -11
  145. package/dist/engine/security/integration.test.js.map +1 -1
  146. package/dist/engine/security/prompt-injection.d.ts +1 -1
  147. package/dist/engine/security/prompt-injection.d.ts.map +1 -1
  148. package/dist/engine/security/prompt-injection.js +2 -1
  149. package/dist/engine/security/prompt-injection.js.map +1 -1
  150. package/dist/engine/security/prompt-injection.test.js +1 -1
  151. package/dist/engine/security/prompt-injection.test.js.map +1 -1
  152. package/dist/engine/security/protected-paths-store.d.ts +56 -0
  153. package/dist/engine/security/protected-paths-store.d.ts.map +1 -0
  154. package/dist/engine/security/protected-paths-store.js +239 -0
  155. package/dist/engine/security/protected-paths-store.js.map +1 -0
  156. package/dist/engine/security/protected-paths.d.ts +54 -0
  157. package/dist/engine/security/protected-paths.d.ts.map +1 -0
  158. package/dist/engine/security/protected-paths.js +193 -0
  159. package/dist/engine/security/protected-paths.js.map +1 -0
  160. package/dist/engine/security/protected-paths.test.d.ts +2 -0
  161. package/dist/engine/security/protected-paths.test.d.ts.map +1 -0
  162. package/dist/engine/security/protected-paths.test.js +166 -0
  163. package/dist/engine/security/protected-paths.test.js.map +1 -0
  164. package/dist/engine/security/shell-guard.d.ts +117 -0
  165. package/dist/engine/security/shell-guard.d.ts.map +1 -0
  166. package/dist/engine/security/shell-guard.js +578 -0
  167. package/dist/engine/security/shell-guard.js.map +1 -0
  168. package/dist/engine/security/shell-guard.test.d.ts +2 -0
  169. package/dist/engine/security/shell-guard.test.d.ts.map +1 -0
  170. package/dist/engine/security/shell-guard.test.js +378 -0
  171. package/dist/engine/security/shell-guard.test.js.map +1 -0
  172. package/dist/engine/system-messages.d.ts +237 -0
  173. package/dist/engine/system-messages.d.ts.map +1 -0
  174. package/dist/engine/system-messages.js +276 -0
  175. package/dist/engine/system-messages.js.map +1 -0
  176. package/dist/engine/system-messages.test.d.ts +2 -0
  177. package/dist/engine/system-messages.test.d.ts.map +1 -0
  178. package/dist/engine/system-messages.test.js +81 -0
  179. package/dist/engine/system-messages.test.js.map +1 -0
  180. package/dist/engine/tools/abort.d.ts +21 -0
  181. package/dist/engine/tools/abort.d.ts.map +1 -0
  182. package/dist/engine/tools/abort.js +44 -0
  183. package/dist/engine/tools/abort.js.map +1 -0
  184. package/dist/engine/tools/classify-conversation.d.ts.map +1 -1
  185. package/dist/engine/tools/classify-conversation.js +7 -2
  186. package/dist/engine/tools/classify-conversation.js.map +1 -1
  187. package/dist/engine/tools/fs-read.d.ts.map +1 -1
  188. package/dist/engine/tools/fs-read.js +17 -6
  189. package/dist/engine/tools/fs-read.js.map +1 -1
  190. package/dist/engine/tools/fs-read.test.js +43 -0
  191. package/dist/engine/tools/fs-read.test.js.map +1 -1
  192. package/dist/engine/tools/fs-write.d.ts.map +1 -1
  193. package/dist/engine/tools/fs-write.js +47 -17
  194. package/dist/engine/tools/fs-write.js.map +1 -1
  195. package/dist/engine/tools/git.d.ts +2 -1
  196. package/dist/engine/tools/git.d.ts.map +1 -1
  197. package/dist/engine/tools/git.js +85 -17
  198. package/dist/engine/tools/git.js.map +1 -1
  199. package/dist/engine/tools/git.test.js +39 -0
  200. package/dist/engine/tools/git.test.js.map +1 -1
  201. package/dist/engine/tools/index.d.ts.map +1 -1
  202. package/dist/engine/tools/index.js +16 -2
  203. package/dist/engine/tools/index.js.map +1 -1
  204. package/dist/engine/tools/json-schema.js +38 -0
  205. package/dist/engine/tools/json-schema.js.map +1 -1
  206. package/dist/engine/tools/json-schema.test.d.ts +2 -0
  207. package/dist/engine/tools/json-schema.test.d.ts.map +1 -0
  208. package/dist/engine/tools/json-schema.test.js +40 -0
  209. package/dist/engine/tools/json-schema.test.js.map +1 -0
  210. package/dist/engine/tools/path-utils.d.ts.map +1 -1
  211. package/dist/engine/tools/path-utils.js +24 -0
  212. package/dist/engine/tools/path-utils.js.map +1 -1
  213. package/dist/engine/tools/path-utils.test.js +15 -0
  214. package/dist/engine/tools/path-utils.test.js.map +1 -1
  215. package/dist/engine/tools/plan.d.ts +11 -3
  216. package/dist/engine/tools/plan.d.ts.map +1 -1
  217. package/dist/engine/tools/plan.js +326 -95
  218. package/dist/engine/tools/plan.js.map +1 -1
  219. package/dist/engine/tools/plan.test.js +161 -47
  220. package/dist/engine/tools/plan.test.js.map +1 -1
  221. package/dist/engine/tools/project-context.d.ts.map +1 -1
  222. package/dist/engine/tools/project-context.js +54 -42
  223. package/dist/engine/tools/project-context.js.map +1 -1
  224. package/dist/engine/tools/project-context.test.js +37 -22
  225. package/dist/engine/tools/project-context.test.js.map +1 -1
  226. package/dist/engine/tools/project-manage.d.ts +24 -0
  227. package/dist/engine/tools/project-manage.d.ts.map +1 -0
  228. package/dist/engine/tools/project-manage.js +311 -0
  229. package/dist/engine/tools/project-manage.js.map +1 -0
  230. package/dist/engine/tools/project-manage.test.d.ts +2 -0
  231. package/dist/engine/tools/project-manage.test.d.ts.map +1 -0
  232. package/dist/engine/tools/project-manage.test.js +196 -0
  233. package/dist/engine/tools/project-manage.test.js.map +1 -0
  234. package/dist/engine/tools/read-reference.d.ts.map +1 -1
  235. package/dist/engine/tools/read-reference.js +10 -5
  236. package/dist/engine/tools/read-reference.js.map +1 -1
  237. package/dist/engine/tools/read-reference.test.js +5 -2
  238. package/dist/engine/tools/read-reference.test.js.map +1 -1
  239. package/dist/engine/tools/search.d.ts.map +1 -1
  240. package/dist/engine/tools/search.js +16 -6
  241. package/dist/engine/tools/search.js.map +1 -1
  242. package/dist/engine/tools/shell.d.ts.map +1 -1
  243. package/dist/engine/tools/shell.js +17 -4
  244. package/dist/engine/tools/shell.js.map +1 -1
  245. package/dist/engine/tools/shell.test.js +63 -1
  246. package/dist/engine/tools/shell.test.js.map +1 -1
  247. package/dist/engine/tools/task-resume.d.ts +93 -0
  248. package/dist/engine/tools/task-resume.d.ts.map +1 -0
  249. package/dist/engine/tools/task-resume.js +269 -0
  250. package/dist/engine/tools/task-resume.js.map +1 -0
  251. package/dist/engine/tools/task-resume.test.d.ts +2 -0
  252. package/dist/engine/tools/task-resume.test.d.ts.map +1 -0
  253. package/dist/engine/tools/task-resume.test.js +423 -0
  254. package/dist/engine/tools/task-resume.test.js.map +1 -0
  255. package/dist/engine/tools/task.d.ts +81 -2
  256. package/dist/engine/tools/task.d.ts.map +1 -1
  257. package/dist/engine/tools/task.js +898 -471
  258. package/dist/engine/tools/task.js.map +1 -1
  259. package/dist/engine/tools/task.test.js +371 -3
  260. package/dist/engine/tools/task.test.js.map +1 -1
  261. package/dist/engine/tools/todo.d.ts +27 -1
  262. package/dist/engine/tools/todo.d.ts.map +1 -1
  263. package/dist/engine/tools/todo.js +320 -14
  264. package/dist/engine/tools/todo.js.map +1 -1
  265. package/dist/engine/tools/todo.test.js +150 -48
  266. package/dist/engine/tools/todo.test.js.map +1 -1
  267. package/dist/engine/tools/web.d.ts.map +1 -1
  268. package/dist/engine/tools/web.js +65 -18
  269. package/dist/engine/tools/web.js.map +1 -1
  270. package/dist/engine/tools/web.test.js +94 -2
  271. package/dist/engine/tools/web.test.js.map +1 -1
  272. package/dist/index.d.ts +8 -2
  273. package/dist/index.d.ts.map +1 -1
  274. package/dist/index.js +5 -2
  275. package/dist/index.js.map +1 -1
  276. package/dist/logger.d.ts.map +1 -1
  277. package/dist/logger.js +0 -1
  278. package/dist/logger.js.map +1 -1
  279. package/dist/main.js +3 -2
  280. package/dist/main.js.map +1 -1
  281. package/dist/prompts/roles/ADMIN.en.md +12 -11
  282. package/dist/prompts/roles/ADMIN.ru.md +12 -11
  283. package/dist/prompts/roles/ANALYST.en.md +12 -11
  284. package/dist/prompts/roles/ANALYST.ru.md +12 -11
  285. package/dist/prompts/roles/ASSISTANT.en.md +12 -11
  286. package/dist/prompts/roles/ASSISTANT.ru.md +12 -11
  287. package/dist/prompts/roles/AUDITOR.en.md +12 -11
  288. package/dist/prompts/roles/AUDITOR.ru.md +12 -11
  289. package/dist/prompts/roles/DEVELOPER.en.md +12 -11
  290. package/dist/prompts/roles/DEVELOPER.ru.md +12 -11
  291. package/dist/prompts/roles/DEVOPS.en.md +12 -11
  292. package/dist/prompts/roles/DEVOPS.ru.md +12 -11
  293. package/dist/prompts/roles/PM.en.md +12 -11
  294. package/dist/prompts/roles/PM.ru.md +12 -11
  295. package/dist/prompts/roles/QA.en.md +12 -11
  296. package/dist/prompts/roles/QA.ru.md +12 -11
  297. package/dist/prompts/roles/TITLE.en.md +30 -0
  298. package/dist/prompts/roles/TITLE.ru.md +3 -11
  299. package/dist/prompts/security/ask-protected-path.en.md +1 -0
  300. package/dist/prompts/security/ask-protected-path.ru.md +1 -0
  301. package/dist/prompts/security/deny-command.en.md +1 -0
  302. package/dist/prompts/security/deny-command.ru.md +1 -0
  303. package/dist/prompts/security/deny-protected-dir.en.md +1 -0
  304. package/dist/prompts/security/deny-protected-dir.ru.md +1 -0
  305. package/dist/prompts/security/deny-protected-path.en.md +1 -0
  306. package/dist/prompts/security/deny-protected-path.ru.md +1 -0
  307. package/dist/prompts/subagents/explore.en.md +23 -0
  308. package/dist/prompts/subagents/general.en.md +19 -0
  309. package/dist/prompts/system/MODE_ACT.en.md +20 -0
  310. package/{prompts/modes/act.md → dist/prompts/system/MODE_ACT.ru.md} +2 -0
  311. package/dist/prompts/system/MODE_PLAN.en.md +22 -0
  312. package/dist/prompts/{modes/plan.md → system/MODE_PLAN.ru.md} +4 -3
  313. package/dist/prompts/system/MODE_YOLO.en.md +22 -0
  314. package/{prompts/modes/yolo.md → dist/prompts/system/MODE_YOLO.ru.md} +2 -0
  315. package/dist/prompts/system/PLAN_GUIDE.en.md +36 -0
  316. package/dist/prompts/system/PLAN_GUIDE.ru.md +33 -0
  317. package/dist/prompts/system/PLAN_MODE_AUTO.en.md +11 -0
  318. package/dist/prompts/system/PLAN_MODE_AUTO.ru.md +12 -0
  319. package/dist/prompts/system/PLAN_MODE_STRICT_APPROVED.en.md +8 -0
  320. package/dist/prompts/system/PLAN_MODE_STRICT_APPROVED.ru.md +8 -0
  321. package/dist/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.en.md +7 -0
  322. package/dist/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.ru.md +7 -0
  323. package/dist/providers/anthropic.d.ts.map +1 -1
  324. package/dist/providers/anthropic.js +28 -2
  325. package/dist/providers/anthropic.js.map +1 -1
  326. package/dist/providers/base.d.ts +20 -1
  327. package/dist/providers/base.d.ts.map +1 -1
  328. package/dist/providers/base.js +17 -0
  329. package/dist/providers/base.js.map +1 -1
  330. package/dist/providers/exceptions.d.ts.map +1 -1
  331. package/dist/providers/exceptions.js +3 -2
  332. package/dist/providers/exceptions.js.map +1 -1
  333. package/dist/providers/ollama.d.ts +23 -0
  334. package/dist/providers/ollama.d.ts.map +1 -1
  335. package/dist/providers/ollama.js +128 -12
  336. package/dist/providers/ollama.js.map +1 -1
  337. package/dist/providers/ollama.test.d.ts +2 -0
  338. package/dist/providers/ollama.test.d.ts.map +1 -0
  339. package/dist/providers/ollama.test.js +129 -0
  340. package/dist/providers/ollama.test.js.map +1 -0
  341. package/dist/providers/openai.d.ts.map +1 -1
  342. package/dist/providers/openai.js +82 -9
  343. package/dist/providers/openai.js.map +1 -1
  344. package/dist/providers/openai.test.d.ts +2 -0
  345. package/dist/providers/openai.test.d.ts.map +1 -0
  346. package/dist/providers/openai.test.js +84 -0
  347. package/dist/providers/openai.test.js.map +1 -0
  348. package/dist/providers/stream-error.test.d.ts +2 -0
  349. package/dist/providers/stream-error.test.d.ts.map +1 -0
  350. package/dist/providers/stream-error.test.js +63 -0
  351. package/dist/providers/stream-error.test.js.map +1 -0
  352. package/dist/providers/timeout.test.d.ts +2 -0
  353. package/dist/providers/timeout.test.d.ts.map +1 -0
  354. package/dist/providers/timeout.test.js +129 -0
  355. package/dist/providers/timeout.test.js.map +1 -0
  356. package/dist/queue/processor.d.ts +22 -0
  357. package/dist/queue/processor.d.ts.map +1 -1
  358. package/dist/queue/processor.js +150 -25
  359. package/dist/queue/processor.js.map +1 -1
  360. package/dist/queue/processor.test.d.ts +2 -0
  361. package/dist/queue/processor.test.d.ts.map +1 -0
  362. package/dist/queue/processor.test.js +211 -0
  363. package/dist/queue/processor.test.js.map +1 -0
  364. package/dist/server/app.d.ts.map +1 -1
  365. package/dist/server/app.js +30 -8
  366. package/dist/server/app.js.map +1 -1
  367. package/dist/server/chat.d.ts.map +1 -1
  368. package/dist/server/chat.js +18 -2
  369. package/dist/server/chat.js.map +1 -1
  370. package/dist/server/routes/agents.d.ts.map +1 -1
  371. package/dist/server/routes/agents.js +39 -23
  372. package/dist/server/routes/agents.js.map +1 -1
  373. package/dist/server/routes/fs.d.ts.map +1 -1
  374. package/dist/server/routes/fs.js +185 -6
  375. package/dist/server/routes/fs.js.map +1 -1
  376. package/dist/server/routes/models.d.ts.map +1 -1
  377. package/dist/server/routes/models.js +3 -1
  378. package/dist/server/routes/models.js.map +1 -1
  379. package/dist/server/routes/queue.d.ts.map +1 -1
  380. package/dist/server/routes/queue.js +91 -17
  381. package/dist/server/routes/queue.js.map +1 -1
  382. package/dist/server/routes/sessions.d.ts +2 -1
  383. package/dist/server/routes/sessions.d.ts.map +1 -1
  384. package/dist/server/routes/sessions.js +68 -10
  385. package/dist/server/routes/sessions.js.map +1 -1
  386. package/dist/server/routes/sessions.test.js +102 -2
  387. package/dist/server/routes/sessions.test.js.map +1 -1
  388. package/dist/server/routes/settings.d.ts +6 -1
  389. package/dist/server/routes/settings.d.ts.map +1 -1
  390. package/dist/server/routes/settings.js +138 -0
  391. package/dist/server/routes/settings.js.map +1 -1
  392. package/dist/server/routes/settings.test.js +65 -1
  393. package/dist/server/routes/settings.test.js.map +1 -1
  394. package/dist/server/serialize.d.ts +9 -3
  395. package/dist/server/serialize.d.ts.map +1 -1
  396. package/dist/server/serialize.js +9 -3
  397. package/dist/server/serialize.js.map +1 -1
  398. package/dist/sessions/auto-title.d.ts +1 -1
  399. package/dist/sessions/auto-title.d.ts.map +1 -1
  400. package/dist/sessions/auto-title.js +24 -13
  401. package/dist/sessions/auto-title.js.map +1 -1
  402. package/dist/sessions/manager.d.ts +1 -2
  403. package/dist/sessions/manager.d.ts.map +1 -1
  404. package/dist/sessions/manager.js +11 -36
  405. package/dist/sessions/manager.js.map +1 -1
  406. package/dist/sessions/manager.test.js +2 -10
  407. package/dist/sessions/manager.test.js.map +1 -1
  408. package/dist/storage/file-store.d.ts.map +1 -1
  409. package/dist/storage/file-store.js +2 -6
  410. package/dist/storage/file-store.js.map +1 -1
  411. package/dist/storage/file-store.test.js +1 -6
  412. package/dist/storage/file-store.test.js.map +1 -1
  413. package/dist/storage/project-store.d.ts.map +1 -1
  414. package/dist/storage/project-store.js +12 -0
  415. package/dist/storage/project-store.js.map +1 -1
  416. package/dist/storage/runtime-settings.d.ts +25 -1
  417. package/dist/storage/runtime-settings.d.ts.map +1 -1
  418. package/dist/storage/runtime-settings.js +31 -0
  419. package/dist/storage/runtime-settings.js.map +1 -1
  420. package/package.json +6 -5
  421. package/prompts/roles/TITLE.en.md +30 -0
  422. package/prompts/security/ask-protected-path.en.md +1 -0
  423. package/prompts/security/ask-protected-path.ru.md +1 -0
  424. package/prompts/security/deny-command.en.md +1 -0
  425. package/prompts/security/deny-command.ru.md +1 -0
  426. package/prompts/security/deny-protected-dir.en.md +1 -0
  427. package/prompts/security/deny-protected-dir.ru.md +1 -0
  428. package/prompts/security/deny-protected-path.en.md +1 -0
  429. package/prompts/security/deny-protected-path.ru.md +1 -0
  430. package/prompts/subagents/explore.en.md +23 -0
  431. package/prompts/subagents/general.en.md +19 -0
  432. package/prompts/system/MODE_ACT.en.md +20 -0
  433. package/{dist/prompts/modes/act.md → prompts/system/MODE_ACT.ru.md} +2 -0
  434. package/prompts/system/MODE_PLAN.en.md +22 -0
  435. package/{dist/prompts/prompts/modes/plan.md → prompts/system/MODE_PLAN.ru.md} +4 -3
  436. package/prompts/system/MODE_YOLO.en.md +22 -0
  437. package/{dist/prompts/prompts/modes/yolo.md → prompts/system/MODE_YOLO.ru.md} +2 -0
  438. package/prompts/system/PLAN_GUIDE.en.md +36 -0
  439. package/prompts/system/PLAN_GUIDE.ru.md +33 -0
  440. package/prompts/system/PLAN_MODE_AUTO.en.md +11 -0
  441. package/prompts/system/PLAN_MODE_AUTO.ru.md +12 -0
  442. package/prompts/system/PLAN_MODE_STRICT_APPROVED.en.md +8 -0
  443. package/prompts/system/PLAN_MODE_STRICT_APPROVED.ru.md +8 -0
  444. package/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.en.md +7 -0
  445. package/prompts/system/PLAN_MODE_STRICT_NOT_APPROVED.ru.md +7 -0
  446. package/dist/dictionaries/dictionaries/coding.en.yaml +0 -20
  447. package/dist/dictionaries/dictionaries/coding.ru.yaml +0 -22
  448. package/dist/dictionaries/dictionaries/coding.tools.yaml +0 -12
  449. package/dist/dictionaries/dictionaries/enum.en.yaml +0 -54
  450. package/dist/dictionaries/dictionaries/enum.ru.yaml +0 -67
  451. package/dist/dictionaries/dictionaries/injection.en.yaml +0 -51
  452. package/dist/dictionaries/dictionaries/injection.ru.yaml +0 -51
  453. package/dist/dictionaries/dictionaries/intents.en.yaml +0 -68
  454. package/dist/dictionaries/dictionaries/intents.ru.yaml +0 -78
  455. package/dist/dictionaries/dictionaries/research.en.yaml +0 -17
  456. package/dist/dictionaries/dictionaries/research.ru.yaml +0 -17
  457. package/dist/dictionaries/dictionaries/small-talk.en.yaml +0 -67
  458. package/dist/dictionaries/dictionaries/small-talk.ru.yaml +0 -81
  459. package/dist/prompts/modes/yolo.md +0 -20
  460. package/dist/prompts/prompts/modes/act.md +0 -18
  461. package/dist/prompts/prompts/roles/ADMIN.en.md +0 -33
  462. package/dist/prompts/prompts/roles/ADMIN.ru.md +0 -33
  463. package/dist/prompts/prompts/roles/ANALYST.en.md +0 -33
  464. package/dist/prompts/prompts/roles/ANALYST.ru.md +0 -33
  465. package/dist/prompts/prompts/roles/ASSISTANT.en.md +0 -38
  466. package/dist/prompts/prompts/roles/ASSISTANT.ru.md +0 -38
  467. package/dist/prompts/prompts/roles/AUDITOR.en.md +0 -33
  468. package/dist/prompts/prompts/roles/AUDITOR.ru.md +0 -33
  469. package/dist/prompts/prompts/roles/DEVELOPER.en.md +0 -42
  470. package/dist/prompts/prompts/roles/DEVELOPER.ru.md +0 -42
  471. package/dist/prompts/prompts/roles/DEVOPS.en.md +0 -35
  472. package/dist/prompts/prompts/roles/DEVOPS.ru.md +0 -35
  473. package/dist/prompts/prompts/roles/PM.en.md +0 -33
  474. package/dist/prompts/prompts/roles/PM.ru.md +0 -33
  475. package/dist/prompts/prompts/roles/QA.en.md +0 -33
  476. package/dist/prompts/prompts/roles/QA.ru.md +0 -33
  477. package/dist/prompts/prompts/roles/TITLE.ru.md +0 -30
  478. package/dist/prompts/prompts/subagents/explore.ru.md +0 -23
  479. package/dist/prompts/prompts/subagents/general.ru.md +0 -19
  480. package/dist/prompts/prompts/system/COMPRESSED_DIALOG.en.md +0 -5
  481. package/dist/prompts/prompts/system/COMPRESSED_DIALOG.ru.md +0 -5
  482. package/dist/prompts/prompts/system/INTERVIEW.en.md +0 -18
  483. package/dist/prompts/prompts/system/INTERVIEW.ru.md +0 -18
  484. package/dist/prompts/prompts/system/PARTICIPANTS_NOTICE.en.md +0 -7
  485. package/dist/prompts/prompts/system/PARTICIPANTS_NOTICE.ru.md +0 -7
  486. package/dist/prompts/prompts/system/ROLES_DESCRIPTION.en.md +0 -16
  487. package/dist/prompts/prompts/system/ROLES_DESCRIPTION.ru.md +0 -16
  488. package/dist/prompts/prompts/system/ROUTING.en.md +0 -13
  489. package/dist/prompts/prompts/system/ROUTING.md +0 -6
  490. package/dist/prompts/prompts/system/ROUTING.ru.md +0 -13
  491. package/dist/prompts/prompts/system/RULES.en.md +0 -10
  492. package/dist/prompts/prompts/system/RULES.md +0 -10
  493. package/dist/prompts/prompts/system/RULES.ru.md +0 -10
  494. package/dist/prompts/prompts/system/STICKY_NOTES.en.md +0 -6
  495. package/dist/prompts/prompts/system/STICKY_NOTES.ru.md +0 -6
  496. package/dist/prompts/prompts/system/SUMMARIZER.en.md +0 -54
  497. package/dist/prompts/prompts/system/SUMMARIZER.ru.md +0 -54
  498. package/dist/prompts/prompts/system/USER_CONTEXT.en.md +0 -5
  499. package/dist/prompts/prompts/system/USER_CONTEXT.ru.md +0 -5
  500. package/dist/prompts/system/ROUTING.md +0 -6
  501. package/dist/prompts/system/RULES.md +0 -10
  502. package/dist/server/routes/i18n.d.ts +0 -3
  503. package/dist/server/routes/i18n.d.ts.map +0 -1
  504. package/dist/server/routes/i18n.js +0 -83
  505. package/dist/server/routes/i18n.js.map +0 -1
  506. package/prompts/modes/plan.md +0 -20
  507. package/prompts/system/ROUTING.md +0 -6
  508. package/prompts/system/RULES.md +0 -10
  509. /package/dist/prompts/{prompts/system → system}/GATEWAY.en.md +0 -0
  510. /package/dist/prompts/{prompts/system → system}/GATEWAY.ru.md +0 -0
@@ -1,8 +1,11 @@
1
- import { MessageRole, nowISO, ToolCapability, generateId, makeTextPart } from '@itphx/synth-shared';
1
+ import { MessageRole, nowISO, ToolCapability, generateId, makeTextPart, lookupContextWindow, generateStreamId } from '@itphx/synth-shared';
2
2
  import { join, isAbsolute, normalize, resolve } from 'node:path';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { DebugCollector } from '../debug.js';
5
5
  import { ProviderFactory } from '../providers/base.js';
6
+ import { ContextLengthExceededError } from '../providers/exceptions.js';
7
+ import { renderPlanMarkdown } from './tools/plan.js';
8
+ import { renderTodosMarkdown } from './tools/todo.js';
6
9
  import { info, warning } from '../logger.js';
7
10
  import { autoTitleSession } from '../sessions/auto-title.js';
8
11
  import { ModelRouter } from './router/router.js';
@@ -13,15 +16,21 @@ import { DictionaryStore } from './router/dictionary-store.js';
13
16
  import { MicroPipeline } from './router/micro/index.js';
14
17
  import { TaskClassifier } from './router/micro/task-classifier.js';
15
18
  import { RulesModel, EmbeddingModel, MicroLlmModel } from './router/micro/models/index.js';
16
- import { PermissionEvaluator } from './permission/evaluator.js';
19
+ import { PermissionEvaluator, isPlanRestricted, isPlanBlockedTool, getMcpToolAvailability, extractPaths } from './permission/evaluator.js';
17
20
  import { PermissionApprovalManager } from './permission/approval.js';
18
21
  import { emptyPermissionConfig } from './permission/derive.js';
22
+ import { classifyLlmError, isLlmErrorRecoverable } from './llm-error.js';
23
+ import { runSubagentResume, validateSubagentResume, SubagentResumeError } from './tools/task-resume.js';
19
24
  import { getToolTrust } from './security/trust.js';
20
25
  import { detectPromptInjection, applyPolicy, wrapContent, buildWarnLine } from './security/prompt-injection.js';
26
+ import { loadProtectedPaths } from './security/protected-paths.js';
21
27
  import { GatewayProvider, AuditLogger, RateLimiter, ObfuscationMapStore, obfuscateSecrets, maskSecret, PLACEHOLDER_PATTERN } from './gateway/index.js';
28
+ import { t as sm, i18n as i18nMeta } from './system-messages.js';
22
29
  const MAX_DOOM_LOOP_COUNT = 3;
23
30
  const DEFAULT_MAX_ITERATIONS = Number.POSITIVE_INFINITY;
24
- function toCoreProviderConfig(shared, model) {
31
+ /** Порог заполненности контекстного окна (%), после которого публикуем context.limit_warning. */
32
+ const CONTEXT_LIMIT_WARNING_PERCENT = 90;
33
+ function toCoreProviderConfig(shared, model, reasoningLevel) {
25
34
  return {
26
35
  name: shared.name,
27
36
  type: shared.type,
@@ -29,6 +38,7 @@ function toCoreProviderConfig(shared, model) {
29
38
  apiKey: shared.apiKey || '',
30
39
  model,
31
40
  timeout: shared.timeout,
41
+ reasoningLevel,
32
42
  };
33
43
  }
34
44
  /**
@@ -70,10 +80,44 @@ export class ChatOrchestrator {
70
80
  /** Внешний audit-logger (backend/runner) — переиспользуется при live-включении gateway. */
71
81
  _gatewayAuditExternal;
72
82
  _providerPool;
83
+ /** Политика защищённых путей (СКВОЗНАЯ проверка файловых тулов + shell) — ленивая загрузка с кэшем. */
84
+ _protectedPaths = null;
85
+ _protectedPathsLoaded = false;
73
86
  /** Публичный доступ к реестру агентов (для сериализации modeApplicable). */
74
87
  get agentRegistry() {
75
88
  return this._agentRegistry;
76
89
  }
90
+ /**
91
+ * Политика защищённых путей: системный дефолт definitions/protected-paths.json
92
+ * + override {dataDir}/security/protected-paths.json (каскад add/remove).
93
+ * Ленивая загрузка с кэшем; при недоступности dataDir/файлов — системный дефолт.
94
+ */
95
+ _getProtectedPaths() {
96
+ if (this._protectedPathsLoaded)
97
+ return this._protectedPaths;
98
+ this._protectedPathsLoaded = true;
99
+ try {
100
+ this._protectedPaths = loadProtectedPaths({ overridePath: join(this._dataDir, 'security', 'protected-paths.json') });
101
+ }
102
+ catch {
103
+ this._protectedPaths = null;
104
+ }
105
+ return this._protectedPaths;
106
+ }
107
+ /** Включена ли защита защищённых путей (тумблер полного отключения). */
108
+ _getProtectedPathsEnabled() {
109
+ return this._securityConfig.protectedPaths?.enabled ?? true;
110
+ }
111
+ /**
112
+ * Локализованный текст сообщения безопасности для model/UI. Для решений с
113
+ * messageId (защищённые пути, запрещённые команды) берётся из промпт-файла;
114
+ * иначе — обычный reason. Текст включает префикс "Permission denied/required".
115
+ */
116
+ _securityMessage(decision, lang) {
117
+ if (!decision.messageId)
118
+ return undefined;
119
+ return this._contextConfig?.getSecurityMessage(decision.messageId, lang, decision.params) ?? undefined;
120
+ }
77
121
  constructor(options) {
78
122
  this._sessionManager = options.sessionManager;
79
123
  this._eventBus = options.eventBus;
@@ -93,7 +137,7 @@ export class ChatOrchestrator {
93
137
  this._approvalManager = new PermissionApprovalManager(this._eventBus);
94
138
  this._defaultPlanMode = options.defaultPlanMode ?? 'auto';
95
139
  this._securityConfig = options.securityConfig
96
- ?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' } };
140
+ ?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' }, protectedPaths: { enabled: true } };
97
141
  this._dataDir = options.dataDir || '/var/synth/data';
98
142
  this._gatewayPayloadLogPath = options.gatewayPayloadLogPath
99
143
  ?? join(options.dataDir || '/var/synth/data', 'audit', 'llm-payloads.jsonl');
@@ -210,6 +254,22 @@ export class ChatOrchestrator {
210
254
  isGatewayEnabled() {
211
255
  return !!this._gatewayConfig;
212
256
  }
257
+ /**
258
+ * Live-переключение секции «Безопасность» (runtime-настройка из UI).
259
+ * Обновляет тумблер защищённых путей без рестарта сервера.
260
+ */
261
+ setSecurityConfig(config) {
262
+ this._securityConfig = config
263
+ ?? { promptInjection: { enabled: true, wrapContent: true, mode: 'flag' }, protectedPaths: { enabled: true } };
264
+ }
265
+ /**
266
+ * AD-029: live-применение глобального таймаута подтверждения (runtime-настройка
267
+ * из UI, раздел «Безопасность»). Обновляет дефолтный permission-конфиг, который
268
+ * наследуют сессии без собственного пер-сессионного permission-конфига.
269
+ */
270
+ setDefaultPermissionTimeout(askTimeoutMs) {
271
+ this._permissionConfig = { ...this._permissionConfig, askTimeoutMs };
272
+ }
213
273
  /**
214
274
  * AD-036: обфусцирует user-сообщение при сохранении в историю (до отправки в LLM).
215
275
  * Карта обфускации сессии обновляется и сохраняется; в message.metadata.secrets
@@ -278,49 +338,159 @@ export class ChatOrchestrator {
278
338
  return dir;
279
339
  }
280
340
  /**
281
- * AD-025/AD-027: добавляет в system prompt блоки режима Plan/Act и
282
- * утверждённого плана. Вызывается при старте обработки и пересобирается
283
- * после plan_enter/plan_exit в текущей итерации.
341
+ * AD-025/AD-039: добавляет в system prompt блоки режима Plan/Act и
342
+ * утверждённого плана проекта. Вызывается при старте обработки и
343
+ * пересобирается после plan_enter/plan_exit в текущей итерации.
284
344
  */
285
- _buildPlanSystemPrompt(base, session, planMode, sessionMode, planApproved, planContent) {
345
+ async _buildPlanSystemPrompt(base, session, planMode, sessionMode, planApproved) {
286
346
  const parts = [];
287
- // AD-025: промпт текущего режима загружается из контекстных файлов
288
- // (prompts/modes/{mode}.md, переопределяемые через dataDir/context/modes).
289
- // Если файл не найден fallback на встроенный текст ниже.
347
+ // AD-025: промпт текущего режима загружается из contextConfig.getModePrompt
348
+ // (prompts/system/MODE_{PLAN,ACT,YOLO}.{lang}.md, переопределяемые через dataDir/context/user/system).
349
+ // Если getModePrompt вернул null (нет contextConfig или файла) блок режима
350
+ // не добавляется ни для одного режима.
290
351
  const language = session.settings.language || 'ru';
291
352
  const modePrompt = this._contextConfig?.getModePrompt(sessionMode, language) ?? null;
292
353
  if (modePrompt) {
293
354
  parts.push(modePrompt);
294
355
  }
295
- else if (sessionMode === 'plan') {
296
- parts.push('[MODE: PLAN]\n'
297
- + 'Ты в режиме планирования: чтение и исследование разрешены, но запись/изменение файлов '
298
- + 'заблокированы (единственное исключение — .synth/plans/*.md). Изучи задачу, составь план '
299
- + утверди его через plan_exit(plan="..."), чтобы перейти в режим act.');
356
+ // AD-028/AD-039: промпт строгости цикла Plan→Build загружается из contextConfig
357
+ // (prompts/system/PLAN_MODE_{AUTO,STRICT_APPROVED,STRICT_NOT_APPROVED}.{lang}.md,
358
+ // переопределяемые через dataDir/context/user/system). planMode 'off' null
359
+ // блок не добавляется.
360
+ const planModePrompt = this._contextConfig?.getPlanModePrompt(planMode, planApproved, language) ?? null;
361
+ if (planModePrompt) {
362
+ parts.push(planModePrompt);
363
+ }
364
+ // AD-039: блок [APPROVED PLAN] собирается из плана проекта (project.plan).
365
+ const approvedPlan = await this._renderProjectPlan(session);
366
+ if (approvedPlan) {
367
+ parts.push(`[APPROVED PLAN]\n${approvedPlan}`);
368
+ }
369
+ // AD-039: блок [TODO] собирается из session.todos.
370
+ const todosMarkdown = renderTodosMarkdown(session.todos ?? []);
371
+ if (todosMarkdown) {
372
+ parts.push(`[TODO]\n${todosMarkdown}`);
373
+ }
374
+ // AD-039: инструкция по ведению плана и todo (PLAN_GUIDE) встраивается
375
+ // всегда, когда доступен контекст (contextConfig), независимо от наличия
376
+ // плана или todo — модель должна знать разницу между ними с первого запроса.
377
+ const guide = this._contextConfig?.getPlanGuide(language) ?? null;
378
+ if (guide) {
379
+ parts.push(guide);
380
+ }
381
+ if (parts.length === 0)
382
+ return base;
383
+ return `${base}\n\n${parts.join('\n\n')}`;
384
+ }
385
+ /**
386
+ * AD-025: фильтрует список тулов, передаваемых LLM, по текущему режиму сессии.
387
+ * В plan-режиме (или strict без утверждённого плана) из списка убираются
388
+ * write/edit-тулы и git-запись (группы edit/git_write) — модель не видит
389
+ * инструментов, которые всё равно будут отклонены PermissionEvaluator'ом.
390
+ * MCP-инструменты фильтруются по настройкам доступности (plan/act).
391
+ * Режимы act/yolo — полный список без изменений.
392
+ */
393
+ _filterToolsByMode(tools, session, sessionMode, planMode, planApproved) {
394
+ if (!tools || tools.length === 0)
395
+ return tools;
396
+ // Рестрикция не активна (act/yolo/off) — полный список тулов.
397
+ if (!isPlanRestricted(sessionMode, planMode, planApproved))
398
+ return tools;
399
+ const mcpToolSettings = session.settings.mcpToolSettings;
400
+ const result = [];
401
+ for (const tool of tools) {
402
+ const toolName = tool.function.name;
403
+ const toolDef = this._toolRegistry?.get(toolName);
404
+ const capabilities = toolDef?.capabilities;
405
+ // Edit-группа (write/edit/delete) и git-запись блокируются в plan-режиме —
406
+ // скрываем их от модели, чтобы она не пыталась их вызывать.
407
+ if (isPlanBlockedTool(toolName, capabilities))
408
+ continue;
409
+ // MCP-инструменты: в plan-режиме недоступный по настройкам тул убираем из
410
+ // списка (консистентно с PermissionEvaluator: modeKey = 'plan').
411
+ if (capabilities?.includes(ToolCapability.MCP) && mcpToolSettings) {
412
+ const availability = getMcpToolAvailability(mcpToolSettings, toolName);
413
+ if (availability && !availability.plan)
414
+ continue;
415
+ }
416
+ result.push(tool);
417
+ }
418
+ return result;
419
+ }
420
+ /**
421
+ * AD-039: markdown-рендер плана проекта. Null — если проекта нет, нет plan
422
+ * или план пуст (нет пунктов).
423
+ */
424
+ async _renderProjectPlan(session) {
425
+ if (!session.projectId || !this._projectStore)
426
+ return null;
427
+ try {
428
+ const project = await this._projectStore.load(session.projectId);
429
+ const plan = project?.plan;
430
+ if (!plan || plan.items.length === 0)
431
+ return null;
432
+ return renderPlanMarkdown(plan);
300
433
  }
301
- if (planMode === 'auto') {
302
- parts.push('[PLAN MODE: AUTO]\n'
303
- + 'Если задача сложная, затрагивает несколько файлов или требует архитектурных решений — '
304
- + 'вызови plan_enter перед изменениями. Для простых фиксов — делай сразу.');
434
+ catch {
435
+ return null;
305
436
  }
306
- else if (planMode === 'strict') {
307
- if (planApproved) {
308
- parts.push('[PLAN MODE: STRICT]\n'
309
- + 'План утверждён ты можешь вносить изменения в соответствии с ним.');
437
+ }
438
+ /**
439
+ * AD-040: добавляет в system prompt контекстный блок сессии — наличие/отсутствие
440
+ * проекта, рабочий каталог и ограничения. Вызывается после резолва workingDir.
441
+ */
442
+ async _buildSessionContextPrompt(base, session, workingDir) {
443
+ const lang = session.settings.language || 'ru';
444
+ const lines = [];
445
+ if (session.projectId) {
446
+ let project = null;
447
+ if (this._projectStore) {
448
+ try {
449
+ project = await this._projectStore.load(session.projectId);
450
+ }
451
+ catch {
452
+ project = null;
453
+ }
454
+ }
455
+ if (project) {
456
+ lines.push('[КОНТЕКСТ СЕССИИ]');
457
+ lines.push(`Проект: ${project.name}`);
458
+ lines.push(`ID проекта: ${session.projectId}`);
459
+ lines.push(`Рабочий каталог: ${project.workspace || workingDir}`);
460
+ const invariants = project.invariants?.trim();
461
+ if (invariants) {
462
+ lines.push('');
463
+ lines.push(lang === 'en' ? '[PROJECT INVARIANTS]' : '[ИНВАРИАНТЫ ПРОЕКТА]');
464
+ lines.push(project.invariants);
465
+ }
310
466
  }
311
467
  else {
312
- parts.push('[PLAN MODE: STRICT]\n'
313
- + 'Перед любым изменением файлов план обязателен: вызови plan_enter, составь план и утверди '
314
- + 'его через plan_exit. До утверждения плана запись файлов заблокирована (чтение разрешено).');
468
+ lines.push('[КОНТЕКСТ СЕССИИ]');
469
+ lines.push(`Проект: не найден (id: ${session.projectId})`);
470
+ lines.push(`Рабочий каталог: ${workingDir}`);
315
471
  }
316
472
  }
317
- const approvedPlan = planContent ?? session.planContent;
318
- if (approvedPlan) {
319
- parts.push(`[APPROVED PLAN]\n${approvedPlan}`);
473
+ else if (lang === 'en') {
474
+ lines.push('[SESSION CONTEXT]');
475
+ lines.push('Project: none');
476
+ lines.push(`Working directory: ${workingDir}`);
477
+ lines.push('Limitations: a plan is unavailable (the plan is bound to a project). '
478
+ + 'File and git tools operate within the working directory. Todo is available.');
479
+ lines.push('If the task requires long-term work with files, git, or a plan — suggest that the user '
480
+ + 'create a project and move the session into it.');
320
481
  }
321
- if (parts.length === 0)
482
+ else {
483
+ lines.push('[КОНТЕКСТ СЕССИИ]');
484
+ lines.push('Проект: отсутствует');
485
+ lines.push(`Рабочий каталог: ${workingDir}`);
486
+ lines.push('Ограничения: план недоступен (план привязан к проекту). '
487
+ + 'Файловые и git-инструменты работают в рамках рабочего каталога. Todo доступен.');
488
+ lines.push('Если задача требует длительной работы с файлами, git или планом — предложи пользователю '
489
+ + 'создать проект и перенести сессию в него.');
490
+ }
491
+ if (lines.length === 0)
322
492
  return base;
323
- return `${base}\n\n${parts.join('\n\n')}`;
493
+ return `${base}\n\n${lines.join('\n')}`;
324
494
  }
325
495
  /**
326
496
  * processMessage callback для QueueProcessor.
@@ -332,13 +502,13 @@ export class ChatOrchestrator {
332
502
  }
333
503
  async _processMessage(session, message, signal, publishChunk) {
334
504
  const streamId = message.metadata?.stream_id || 'unknown';
335
- const effectiveAgentRole = message.agentRole || session.settings.agentRole || 'assistant';
505
+ const effectiveAgentRole = message.agentRole || session.settings.agentRole;
336
506
  const author = message.author || 'unknown';
337
507
  const workingDir = await this._resolveWorkingDir(session);
338
508
  // AD-025: режим Plan/Act — runtime-состояние сессии + строгость из настроек
339
509
  const planMode = session.settings.planMode ?? this._defaultPlanMode ?? 'auto';
340
510
  let sessionMode = session.mode ?? 'act';
341
- let planApproved = Boolean(session.planContent);
511
+ let planApproved = Boolean(session.planApproved);
342
512
  // AD-029: permissions — сессионный override приоритетнее глобального конфига.
343
513
  // Конфиг может быть расширен «разрешить в сессии» (once:false) в процессе
344
514
  // обработки, поэтому ссылка и эвалуатор пересоздаются.
@@ -349,13 +519,15 @@ export class ChatOrchestrator {
349
519
  const maxIterations = (agentMaxSteps !== undefined && agentMaxSteps > 0)
350
520
  ? agentMaxSteps
351
521
  : DEFAULT_MAX_ITERATIONS;
522
+ // Уровень размышлений агента: если агент не задал — берём настройку сессии
523
+ const effectiveReasoningLevel = agentDef?.reasoningLevel ?? session.settings.reasoningLevel;
352
524
  const debugCollector = new DebugCollector(session.settings.debugEnabled);
353
525
  // --- Обработка системных сообщений (суммаризация, шедулер, etc.) ---
354
526
  if (message.role === MessageRole.SYSTEM && message.metadata?.type === 'summarization_request') {
355
527
  return this._handleSummarization(session, message, signal, publishChunk, debugCollector, streamId);
356
528
  }
357
529
  if (message.role === MessageRole.SYSTEM && message.metadata?.type === 'debug_system_message') {
358
- return this._handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId);
530
+ return this._handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId, effectiveReasoningLevel);
359
531
  }
360
532
  // --- AD-035: Micro-first — пробуем ответить без LLM ---
361
533
  if (this._microPipeline && session.settings.microFirst) {
@@ -380,6 +552,9 @@ export class ChatOrchestrator {
380
552
  // Сохраняем сообщение ассистента
381
553
  const microMetadata = {
382
554
  stream_id: streamId,
555
+ ...(microResult.microResult.i18nKey
556
+ ? { i18n: i18nMeta(microResult.microResult.i18nKey) }
557
+ : {}),
383
558
  micro_first: {
384
559
  label: microResult.microResult.label,
385
560
  confidence: microResult.microResult.confidence,
@@ -413,7 +588,7 @@ export class ChatOrchestrator {
413
588
  // Авто-тайтл если это первое сообщение
414
589
  if (session.messages.filter((m) => m.role === MessageRole.USER).length <= 1) {
415
590
  try {
416
- session.title = `Диалог ${session.id.slice(0, 8)}`;
591
+ session.title = sm('session.dialog_title', { id: session.id.slice(0, 8) });
417
592
  await this._sessionManager.saveSession(session);
418
593
  }
419
594
  catch {
@@ -434,11 +609,11 @@ export class ChatOrchestrator {
434
609
  }
435
610
  try {
436
611
  // 1. Get provider (через роутер если доступен, иначе напрямую)
437
- const routeResult = this._routeProvider(session, message);
612
+ const routeResult = this._routeProvider(session, message, effectiveReasoningLevel);
438
613
  if (!routeResult) {
439
614
  const providerName = message.provider || session.settings.provider || '';
440
615
  return {
441
- error: `Провайдер ${providerName || 'не указан'} не найден или не настроен`,
616
+ error: sm('router.provider_not_found', { provider: providerName || sm('router.provider_unspecified') }),
442
617
  errorType: 'provider_not_found',
443
618
  canRetry: false,
444
619
  };
@@ -487,8 +662,10 @@ export class ChatOrchestrator {
487
662
  finalSystemPrompt = `${finalSystemPrompt}\n\n${gatewayInstructions}`;
488
663
  }
489
664
  }
490
- // AD-025/AD-027: блоки режима Plan/Act + утверждённый план
491
- finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
665
+ // AD-025/AD-039: блоки режима Plan/Act + утверждённый план проекта
666
+ finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
667
+ // AD-040: контекстный блок сессии (проект/рабочий каталог/ограничения)
668
+ finalSystemPrompt = await this._buildSessionContextPrompt(finalSystemPrompt, session, workingDir);
492
669
  // 3. Convert messages for LLM (без summary — они уже в system prompt)
493
670
  const messagesForLlm = optimizedMessages.filter((m) => m.role !== MessageRole.SUMMARY);
494
671
  const baseLlmMessages = ChatOrchestrator._sessionMessagesToLlm(messagesForLlm, effectiveAgentRole, author);
@@ -518,32 +695,45 @@ export class ChatOrchestrator {
518
695
  }
519
696
  }
520
697
  }
521
- // 4. Get tools from registry, filtered by agent capabilities
522
- let tools;
698
+ // 4. Get tools from registry, filtered by agent capabilities.
699
+ // baseTools — полный набор по агентам; tools — с учётом режима сессии
700
+ // (AD-025): пересобирается из baseTools после plan_enter/plan_exit.
701
+ let baseTools;
523
702
  if (this._toolRegistry) {
524
- if (this._agentRegistry && effectiveAgentRole) {
525
- tools = this._agentRegistry.getToolsForAgent(effectiveAgentRole);
703
+ // agentRole всегда определён при нормальной работе (пользователь, @-mention,
704
+ // сабагент). Если агент НЕ найден в реестре или role undefined — модель получает
705
+ // НИКАКИХ инструментов. Никакого молчаливого fallback на «все non-MCP», иначе
706
+ // агент без shell видит execute_shell и «гадает по именам» вместо честного ответа.
707
+ const agentDef = effectiveAgentRole ? this._agentRegistry?.get(effectiveAgentRole) : undefined;
708
+ if (agentDef && effectiveAgentRole) {
709
+ baseTools = this._agentRegistry.getToolsForAgent(effectiveAgentRole);
526
710
  }
527
- if (tools === undefined) {
528
- // Агент не найден в реестре — отдаём только встроенные инструменты (без MCP)
529
- const allNonMcp = Object.values(ToolCapability).filter((c) => c !== ToolCapability.MCP);
530
- tools = this._toolRegistry.filterByCapabilities(allNonMcp);
531
- }
532
- else if (tools.length === 0) {
533
- tools = [];
711
+ else {
712
+ baseTools = [];
534
713
  }
535
714
  }
715
+ let tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
536
716
  // 5. Create cancellation token
717
+ // Внутренний AbortController: абортится и по cancelToken.cancel()
718
+ // (POST /sessions/:id/abort → orchestrator.cancel), и по внешнему signal
719
+ // (cancelRetry в QueueProcessor). Его signal передаётся в тулы
720
+ // (ctx.abortSignal) — долгие тулы (например task/сабагент) подписываются
721
+ // на него и прерываются сразу, не дожидаясь завершения.
722
+ const toolAbortController = new AbortController();
537
723
  const cancelToken = {
538
724
  cancelled: false,
539
725
  cancel: () => {
540
726
  cancelToken.cancelled = true;
727
+ toolAbortController.abort();
541
728
  },
542
729
  };
543
730
  this._cancellationTokens.set(streamId, cancelToken);
544
731
  signal.addEventListener('abort', () => {
545
732
  cancelToken.cancel();
546
733
  }, { once: true });
734
+ // signal мог быть уже абортирован до входа (гонка с отменой из очереди)
735
+ if (signal.aborted)
736
+ cancelToken.cancel();
547
737
  // 6. Tool loop — stream LLM, execute tools, continue until no tool calls
548
738
  let toolIteration = 0;
549
739
  const currentMessages = [...baseLlmMessages];
@@ -554,8 +744,17 @@ export class ChatOrchestrator {
554
744
  let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
555
745
  let finalModel;
556
746
  const allToolCalls = [];
747
+ const allRawToolCalls = [];
557
748
  let stopAfterDeny = false;
558
749
  let denyStopReason;
750
+ /** metadata.i18n подставленного финального фоллбека (лимит итераций / deny / пустой ответ). */
751
+ let fallbackI18n;
752
+ /** Были ли deny-отказы за цикл (для осмысленного fallback пустого ответа). */
753
+ let hadDeny = false;
754
+ /** Отмена пришла во время исполнения тула — завершать обработку как отмену. */
755
+ let cancelledDuringTool = false;
756
+ // Предупреждение о заполненности контекстного окна (fire-and-forget, не блокирует вызов)
757
+ this._publishContextLimitWarning(session.id, routeResult.modelName, messagesForLlm);
559
758
  while (toolIteration < maxIterations) {
560
759
  if (cancelToken.cancelled)
561
760
  break;
@@ -611,23 +810,28 @@ export class ChatOrchestrator {
611
810
  catch (e) {
612
811
  const errMsg = String(e);
613
812
  warning('ORCHESTRATOR', `LLM stream error: ${errMsg}`);
614
- if (accumulatedContent || accumulatedReasoning) {
615
- await this._saveAssistantMessage(session.id, accumulatedContent, streamId, effectiveAgentRole, finalUsage, chunkModel, accumulatedReasoning, debugCollector, true, providerName);
616
- }
617
813
  this._cancellationTokens.delete(streamId);
618
- return {
619
- error: errMsg,
620
- errorType: this._classifyError(errMsg),
621
- canRetry: this._isRecoverable(errMsg),
622
- };
814
+ return this._handleLlmFailure(session.id, streamId, e, publishChunk);
623
815
  }
624
816
  if (cancelToken.cancelled) {
625
- if (accumulatedContent || accumulatedReasoning) {
626
- await this._saveAssistantMessage(session.id, accumulatedContent, streamId, effectiveAgentRole, finalUsage, chunkModel, accumulatedReasoning, debugCollector, true, providerName);
817
+ // Отмена: сохраняем уже выполненные tool-вызовы (персистентность
818
+ // tool-chain) иначе timeline вызовов пропадает из истории.
819
+ await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, chunkModel ?? finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
820
+ // Баг: накопленный текст ВСЕХ итераций (завершённые + текущая
821
+ // незавершённая), а не только последней. Иначе при стопе в истории
822
+ // остаётся обрывок на случайном месте, хотя пользователь видел весь
823
+ // текст в UI до стопа (тексты завершённых итераций лежат в
824
+ // allIntermediateTexts и раньше не попадали в content).
825
+ const partialContent = this._joinAccumulatedTexts(allIntermediateTexts, accumulatedContent);
826
+ // При отмене НЕ кладём allIntermediateTexts в metadata
827
+ // (intermediate_text_blocks): они уже вошли в partialContent, иначе
828
+ // UI (режим 'bottom') склеит их с content повторно — дубль.
829
+ if (partialContent || accumulatedReasoning || allToolCalls.length > 0) {
830
+ await this._saveAssistantMessage(session.id, partialContent, streamId, effectiveAgentRole, finalUsage, chunkModel, accumulatedReasoning, debugCollector, true, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, undefined);
627
831
  }
628
832
  publishChunk('stream.cancelled', {
629
833
  stream_id: streamId,
630
- content: accumulatedContent,
834
+ content: partialContent,
631
835
  });
632
836
  this._cancellationTokens.delete(streamId);
633
837
  return {};
@@ -644,6 +848,7 @@ export class ChatOrchestrator {
644
848
  if (accumulatedContent) {
645
849
  allIntermediateTexts.push({ text: accumulatedContent, iteration: toolIteration });
646
850
  }
851
+ allRawToolCalls.push({ iteration: toolIteration, calls: toolCalls });
647
852
  // Tool calls detected — execute them
648
853
  debugCollector.captureRawModelResponse(`[tool_calls iteration ${toolIteration}]: ${JSON.stringify(toolCalls)}`);
649
854
  // Add assistant message with tool calls to currentMessages
@@ -677,36 +882,11 @@ export class ChatOrchestrator {
677
882
  else {
678
883
  toolArgs = func.arguments || {};
679
884
  }
680
- // Doom loop detection
681
- const doomKey = `${toolName}:${JSON.stringify(toolArgs)}`;
682
- const doomCount = (doomLoopTracker.get(doomKey) || 0) + 1;
683
- doomLoopTracker.set(doomKey, doomCount);
684
- if (doomCount >= MAX_DOOM_LOOP_COUNT) {
685
- warning('ORCHESTRATOR', `Doom loop detected: ${toolName} called ${doomCount} times with same args`);
686
- currentMessages.push({
687
- id: '',
688
- role: 'tool',
689
- content: `Ошибка: инструмент "${toolName}" вызван ${doomCount} раз(а) с одинаковыми аргументами. Цикл прерван.`,
690
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
691
- disabled: false,
692
- pinned: false,
693
- branchId: '',
694
- toolCallId: tcId,
695
- });
696
- continue;
697
- }
698
- const tcStartedAt = new Date().toISOString();
699
- publishChunk('tool_call.started', {
700
- stream_id: streamId,
701
- session_id: session.id,
702
- call_id: tcId,
703
- name: toolName,
704
- arguments: toolArgs,
705
- iteration: toolIteration,
706
- });
707
885
  const toolDef = this._toolRegistry?.get(toolName);
708
886
  let toolResult;
709
- // AD-029: гранулярные permissions — проверяем перед выполнением тула.
887
+ // AD-029: гранулярные permissions — проверяем ДО doom loop, чтобы
888
+ // deny-повторы не блокировались безликим «Цикл прерван», а получали
889
+ // полноценный feedback (tool_call.result) с объяснением политики.
710
890
  // AD-025: учитываем текущий режим Plan/Act и строгость плана.
711
891
  const permissionDecision = permissionEvaluator.check({
712
892
  toolName,
@@ -717,12 +897,48 @@ export class ChatOrchestrator {
717
897
  planMode,
718
898
  planApproved,
719
899
  mcpToolSettings: session.settings.mcpToolSettings,
900
+ shellPolicy: this._getProtectedPaths() ?? undefined,
901
+ protectedPaths: this._getProtectedPaths() ?? undefined,
902
+ protectedPathsEnabled: this._getProtectedPathsEnabled(),
903
+ });
904
+ // Doom loop detection — только для вызовов, НЕ заблокированных
905
+ // политикой. Deny-вызовы исключены: каждая попытка должна получать
906
+ // понятный feedback с объяснением причины запрета, а не «Цикл
907
+ // прерван». Механика для повторяющихся легальных вызовов сохраняется.
908
+ if (permissionDecision.action !== 'deny') {
909
+ const doomKey = `${toolName}:${JSON.stringify(toolArgs)}`;
910
+ const doomCount = (doomLoopTracker.get(doomKey) || 0) + 1;
911
+ doomLoopTracker.set(doomKey, doomCount);
912
+ if (doomCount >= MAX_DOOM_LOOP_COUNT) {
913
+ warning('ORCHESTRATOR', `Doom loop detected: ${toolName} called ${doomCount} times with same args`);
914
+ currentMessages.push({
915
+ id: '',
916
+ role: 'tool',
917
+ content: sm('tool.doom_loop', { tool: toolName, count: String(doomCount) }),
918
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
919
+ disabled: false,
920
+ pinned: false,
921
+ branchId: '',
922
+ toolCallId: tcId,
923
+ });
924
+ continue;
925
+ }
926
+ }
927
+ const tcStartedAt = new Date().toISOString();
928
+ publishChunk('tool_call.started', {
929
+ stream_id: streamId,
930
+ session_id: session.id,
931
+ call_id: tcId,
932
+ name: toolName,
933
+ arguments: toolArgs,
934
+ iteration: toolIteration,
720
935
  });
721
936
  let executeTool = permissionDecision.action === 'allow';
722
937
  let deniedReason;
723
938
  let userDenied = false;
724
939
  if (permissionDecision.action === 'deny') {
725
- deniedReason = permissionDecision.reason;
940
+ deniedReason = this._securityMessage(permissionDecision, 'en')
941
+ ?? `Permission denied: ${permissionDecision.reason}`;
726
942
  }
727
943
  else if (permissionDecision.action === 'ask') {
728
944
  const approval = await this._approvalManager.waitForDecision(session.id, {
@@ -730,24 +946,27 @@ export class ChatOrchestrator {
730
946
  callId: tcId,
731
947
  toolName,
732
948
  arguments: toolArgs,
733
- reason: permissionDecision.reason,
949
+ reason: this._securityMessage(permissionDecision, 'en')
950
+ ?? permissionDecision.reason,
734
951
  }, effectivePermissionConfig.askTimeoutMs);
735
952
  if (approval.approved) {
736
953
  executeTool = true;
737
954
  // «Разрешить в сессии» (once:false) — добавляем allow-правило для
738
955
  // тула в permission-конфиг сессии, чтобы не спрашивать повторно.
739
956
  if (approval.once === false) {
740
- effectivePermissionConfig = await this._grantSessionToolAllow(session, toolName);
957
+ effectivePermissionConfig = await this._grantSessionToolAllow(session, toolName, toolArgs, workingDir);
741
958
  permissionEvaluator = new PermissionEvaluator(effectivePermissionConfig);
742
959
  }
743
960
  }
744
961
  else {
745
962
  const why = approval.timeout
746
- ? 'истекло время ожидания подтверждения'
963
+ ? sm('permission.ask_timeout')
747
964
  : approval.noSubscribers
748
- ? 'нет подключённого пользователя для подтверждения'
749
- : 'отклонено пользователем';
750
- deniedReason = `${permissionDecision.reason} (${why})`;
965
+ ? sm('permission.ask_no_subscribers')
966
+ : sm('permission.ask_denied');
967
+ const baseDeny = this._securityMessage(permissionDecision, 'en')
968
+ ?? `Permission denied: ${permissionDecision.reason}`;
969
+ deniedReason = `${baseDeny} (${why})`;
751
970
  userDenied = !approval.timeout && !approval.noSubscribers;
752
971
  }
753
972
  }
@@ -761,24 +980,29 @@ export class ChatOrchestrator {
761
980
  matched_rule: permissionDecision.matchedRule,
762
981
  });
763
982
  toolResult = {
764
- content: `Permission denied: ${deniedReason}`,
983
+ content: deniedReason ?? sm('tool.permission_denied_default'),
765
984
  isError: true,
766
985
  metadata: {
986
+ ...(permissionDecision.messageId
987
+ ? { i18n: i18nMeta(`security.${permissionDecision.messageId.replaceAll('-', '_')}`, permissionDecision.params) }
988
+ : { i18n: i18nMeta('tool.permission_denied_default') }),
767
989
  permission: {
768
990
  action: 'deny',
769
- reason: deniedReason,
991
+ reason: deniedReason ?? '',
770
992
  matched_rule: permissionDecision.matchedRule,
771
993
  },
772
994
  },
773
995
  };
996
+ hadDeny = true;
774
997
  }
775
998
  else {
776
999
  if (toolDef) {
777
1000
  const parsed = toolDef.parameters.safeParse(toolArgs);
778
1001
  if (!parsed.success) {
779
1002
  toolResult = {
780
- content: `Ошибка аргументов инструмента "${toolName}": ${parsed.error.issues.map(i => `${i.path.join('.') || 'root'}: ${i.message}`).join('; ')}`,
1003
+ content: sm('tool.args_error', { tool: toolName, issues: parsed.error.issues.map(i => `${i.path.join('.') || 'root'}: ${i.message}`).join('; ') }),
781
1004
  isError: true,
1005
+ metadata: { i18n: i18nMeta('tool.args_error', { tool: toolName }) },
782
1006
  };
783
1007
  }
784
1008
  else {
@@ -786,7 +1010,10 @@ export class ChatOrchestrator {
786
1010
  toolResult = await toolDef.execute(parsed.data, {
787
1011
  sessionId: session.id,
788
1012
  messageId: message.metadata?.stream_id || streamId,
789
- abortSignal: signal,
1013
+ // Живой сигнал отмены: абортится как по /abort (cancel()),
1014
+ // так и по cancelRetry (внешний signal). Без этого тулы
1015
+ // (task/сабагент) не узнают о «Стоп» до своего завершения.
1016
+ abortSignal: toolAbortController.signal,
790
1017
  agentRole: effectiveAgentRole,
791
1018
  workingDir,
792
1019
  dataDir: this._modelPriceLoader ? undefined : undefined,
@@ -808,37 +1035,64 @@ export class ChatOrchestrator {
808
1035
  routerConfig: this._routerConfig ?? undefined,
809
1036
  contextConfig: this._contextConfig ?? undefined,
810
1037
  securityConfig: this._securityConfig,
1038
+ language: session.settings.language || 'ru',
811
1039
  publish: (type, data) => publishChunk(type, data),
812
1040
  streamId,
813
1041
  sessionId: session.id,
1042
+ // ID собственного tool-вызова — для вложенности вызовов
1043
+ // саб-агента под блоком `task` в родительском стриме.
1044
+ callId: tcId,
814
1045
  toolRegistry: this._toolRegistry,
815
1046
  permissionConfig: effectivePermissionConfig,
1047
+ shellPolicy: this._getProtectedPaths() ?? undefined,
1048
+ protectedPaths: this._getProtectedPaths() ?? undefined,
1049
+ protectedPathsEnabled: this._getProtectedPathsEnabled(),
816
1050
  },
817
1051
  });
818
1052
  }
819
1053
  catch (execError) {
820
1054
  toolResult = {
821
- content: `Ошибка исполнения инструмента "${toolName}": ${String(execError)}`,
1055
+ content: sm('tool.exec_error', { tool: toolName, error: String(execError) }),
822
1056
  isError: true,
1057
+ metadata: { i18n: i18nMeta('tool.exec_error', { tool: toolName }) },
823
1058
  };
824
1059
  }
825
1060
  }
826
1061
  }
827
1062
  else {
828
1063
  toolResult = {
829
- content: `Инструмент "${toolName}" не найден`,
1064
+ content: sm('tool.not_found', { tool: toolName }),
830
1065
  isError: true,
1066
+ metadata: { i18n: i18nMeta('tool.not_found', { tool: toolName }) },
831
1067
  };
832
1068
  }
833
1069
  }
834
- publishChunk('tool_call.result', {
835
- stream_id: streamId,
836
- session_id: session.id,
837
- call_id: tcId,
838
- name: toolName,
839
- result: toolResult,
840
- iteration: toolIteration,
841
- });
1070
+ // Отмена пришла ВО ВРЕМЯ исполнения тула (например task/сабагент).
1071
+ // Фиксируем результат (цепочка тулов сохранится в истории), публикуем
1072
+ // tool_call.result (UI видит, чем тул завершился) и прекращаем
1073
+ // дальнейшие тулы/итерации — обработка завершится как отмена (ниже),
1074
+ // а не как обычное завершение с «ответ модели пуст».
1075
+ if (cancelToken.cancelled || toolAbortController.signal.aborted) {
1076
+ allToolCalls.push({
1077
+ name: toolName,
1078
+ args: toolArgs,
1079
+ id: tcId,
1080
+ result: toolResult,
1081
+ started_at: tcStartedAt,
1082
+ completed_at: new Date().toISOString(),
1083
+ iteration: toolIteration,
1084
+ });
1085
+ publishChunk('tool_call.result', {
1086
+ stream_id: streamId,
1087
+ session_id: session.id,
1088
+ call_id: tcId,
1089
+ name: toolName,
1090
+ result: toolResult,
1091
+ iteration: toolIteration,
1092
+ });
1093
+ cancelledDuringTool = true;
1094
+ break;
1095
+ }
842
1096
  // IPI-защита: оборачиваем tool-контент по матрице доверия + детектируем
843
1097
  // подозрительные паттерны (флаг/sanitize/block по конфигу).
844
1098
  if (this._securityConfig.promptInjection.enabled && !toolResult.isError) {
@@ -858,10 +1112,11 @@ export class ChatOrchestrator {
858
1112
  const applied = applyPolicy(toolResult.content, piConfig.mode, detection);
859
1113
  if (applied.blocked) {
860
1114
  toolResult = {
861
- content: `[BLOCKED] Результат инструмента "${toolName}" заблокирован: обнаружены признаки prompt injection (${detection.matches.slice(0, 5).join(', ')})`,
1115
+ content: sm('security.injection_blocked', { tool: toolName, matches: detection.matches.slice(0, 5).join(', ') }),
862
1116
  isError: true,
863
1117
  metadata: {
864
1118
  ...toolResult.metadata,
1119
+ i18n: i18nMeta('security.injection_blocked', { tool: toolName }),
865
1120
  injection_suspicious: true,
866
1121
  injection_matches: detection.matches,
867
1122
  },
@@ -888,6 +1143,18 @@ export class ChatOrchestrator {
888
1143
  }
889
1144
  }
890
1145
  }
1146
+ // Публикуем результат ПОСЛЕ IPI-обёртки: живой UI (tool_call.result)
1147
+ // должен получать тот же контент, что сохраняется в историю
1148
+ // (metadata.tool_results и tool-chain tool-сообщения), иначе при
1149
+ // перезагрузке вид результата меняется.
1150
+ publishChunk('tool_call.result', {
1151
+ stream_id: streamId,
1152
+ session_id: session.id,
1153
+ call_id: tcId,
1154
+ name: toolName,
1155
+ result: toolResult,
1156
+ iteration: toolIteration,
1157
+ });
891
1158
  // classify_conversation: сохраняем категорию в настройках сессии (AD-034)
892
1159
  if (toolName === 'classify_conversation' && !toolResult.isError) {
893
1160
  const cat = toolResult.metadata?.routingCategory || undefined;
@@ -902,20 +1169,19 @@ export class ChatOrchestrator {
902
1169
  });
903
1170
  }
904
1171
  }
905
- // AD-025/AD-027: обновляем локальный режим после plan_enter/plan_exit,
906
- // чтобы ограничения и system prompt применились к следующим тулам/итерациям.
1172
+ // AD-025/AD-039: обновляем локальный режим после plan_enter/plan_exit,
1173
+ // чтобы ограничения, system prompt и список тулов применились к
1174
+ // следующим тулам/итерациям.
907
1175
  if (!toolResult.isError && toolName === 'plan_enter') {
908
1176
  sessionMode = 'plan';
909
- finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
1177
+ finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
1178
+ tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
910
1179
  }
911
1180
  else if (!toolResult.isError && toolName === 'plan_exit') {
912
- const meta = (toolResult.metadata ?? {});
913
- const planContent = typeof meta.plan_content === 'string' ? meta.plan_content : undefined;
914
- if (planContent) {
915
- planApproved = true;
916
- sessionMode = 'act';
917
- }
918
- finalSystemPrompt = this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved, planContent);
1181
+ planApproved = true;
1182
+ sessionMode = 'act';
1183
+ finalSystemPrompt = await this._buildPlanSystemPrompt(finalSystemPrompt, session, planMode, sessionMode, planApproved);
1184
+ tools = this._filterToolsByMode(baseTools, session, sessionMode, planMode, planApproved);
919
1185
  }
920
1186
  allToolCalls.push({
921
1187
  name: toolName,
@@ -946,6 +1212,12 @@ export class ChatOrchestrator {
946
1212
  break;
947
1213
  }
948
1214
  }
1215
+ // Отмена между тулами итерации (например во время IPI-обработки) —
1216
+ // тоже завершаем как отмену, а не идём на следующую LLM-итерацию.
1217
+ if (!stopAfterDeny && (cancelToken.cancelled || toolAbortController.signal.aborted)) {
1218
+ cancelledDuringTool = true;
1219
+ break;
1220
+ }
949
1221
  publishChunk('tool_call.loop', {
950
1222
  stream_id: streamId,
951
1223
  session_id: session.id,
@@ -956,14 +1228,45 @@ export class ChatOrchestrator {
956
1228
  if (stopAfterDeny)
957
1229
  break;
958
1230
  }
1231
+ // Отмена во время исполнения тула: завершаем ровно как ветку отмены
1232
+ // (persist tool-chain + stream.cancelled), а не как обычное завершение
1233
+ // с подставленным «Не удалось выполнить запрос: ответ модели пуст».
1234
+ if (cancelledDuringTool) {
1235
+ await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
1236
+ const partialContent = this._joinAccumulatedTexts(allIntermediateTexts, '');
1237
+ // Промежуточные тексты уже вошли в partialContent — как в ветке отмены
1238
+ // выше, НЕ дублируем их в metadata.intermediate_text_blocks.
1239
+ if (partialContent || allToolCalls.length > 0) {
1240
+ await this._saveAssistantMessage(session.id, partialContent, streamId, effectiveAgentRole, finalUsage, finalModel, allReasoning.map((r) => r.text).join('\n\n---\n\n'), debugCollector, true, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, undefined);
1241
+ }
1242
+ publishChunk('stream.cancelled', {
1243
+ stream_id: streamId,
1244
+ content: partialContent,
1245
+ });
1246
+ this._cancellationTokens.delete(streamId);
1247
+ return {};
1248
+ }
959
1249
  // 7. Handle max iterations exceeded
960
1250
  if (toolIteration >= maxIterations && !finalContent) {
961
- finalContent = '(достигнут лимит итераций инструментов)';
1251
+ finalContent = sm('session.max_iterations');
1252
+ fallbackI18n = i18nMeta('session.max_iterations');
962
1253
  }
963
1254
  // AD-029: пользователь отклонил ask — обработка завершена. Фиксируем
964
1255
  // причину в финальном сообщении (история tool_result уже собрана).
965
1256
  if (stopAfterDeny && !finalContent) {
966
- finalContent = `Обработка остановлена: ${denyStopReason ?? 'пользователь отклонил запрос'}`;
1257
+ const reason = denyStopReason ?? sm('session.stopped_reason_denied');
1258
+ finalContent = sm('session.stopped', { reason });
1259
+ fallbackI18n = i18nMeta('session.stopped', { reason });
1260
+ }
1261
+ // Защита от пустого финального ответа: если цикл завершился (в т.ч.
1262
+ // через doom loop / лимит итераций / модель вернула пустой контент)
1263
+ // с пустым finalContent — подставляем осмысленное сообщение, чтобы
1264
+ // в истории не оставалось пустого assistant-сообщения.
1265
+ if (!finalContent) {
1266
+ finalContent = hadDeny
1267
+ ? sm('session.denied_blocked')
1268
+ : sm('session.empty_response');
1269
+ fallbackI18n = i18nMeta(hadDeny ? 'session.denied_blocked' : 'session.empty_response');
967
1270
  }
968
1271
  // 7.5. Quality check + escalation (AD-034)
969
1272
  let qaPass;
@@ -985,7 +1288,7 @@ export class ChatOrchestrator {
985
1288
  reason: 'escalation_fallback',
986
1289
  strategy: 'default',
987
1290
  };
988
- const fbProvider = this._router.getProvider(fbDecision);
1291
+ const fbProvider = this._router.getProvider(fbDecision, undefined, effectiveReasoningLevel);
989
1292
  if (fbProvider) {
990
1293
  // Перевызываем LLM с fallback-моделью (без стриминга в UI)
991
1294
  try {
@@ -996,7 +1299,7 @@ export class ChatOrchestrator {
996
1299
  if (fbResponse.content) {
997
1300
  finalContent = fbResponse.content;
998
1301
  qaPass = true;
999
- qaReason = `Эскалация на ${fallbackProvider}/${fallbackModel}: ${qaResult.reason}`;
1302
+ qaReason = sm('router.escalation', { provider: fallbackProvider, model: fallbackModel, reason: qaResult.reason });
1000
1303
  if (fbResponse.usage)
1001
1304
  finalUsage = fbResponse.usage;
1002
1305
  if (fbResponse.model)
@@ -1029,9 +1332,15 @@ export class ChatOrchestrator {
1029
1332
  });
1030
1333
  }
1031
1334
  }
1335
+ // 7.8. Персистентность tool-chain (Вариант B): настоящие пары
1336
+ // «assistant с toolCalls + tool-результаты» в session.messages, чтобы
1337
+ // _sessionMessagesToLlm на следующем запросе выдавал OpenAI-совместимый
1338
+ // диалог. Промежуточные сообщения помечаются metadata.tool_chain=true —
1339
+ // UI их скрывает (визуализация уже есть в финальном сообщении).
1340
+ await this._persistToolChain(session.id, streamId, providerName, effectiveAgentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning);
1032
1341
  // 8. Save final assistant message
1033
1342
  const reasoningJoined = allReasoning.map((r) => r.text).join('\n\n---\n\n');
1034
- const savedContent = await this._saveAssistantMessage(session.id, finalContent, streamId, effectiveAgentRole, finalUsage, finalModel, reasoningJoined, debugCollector, false, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, allIntermediateTexts, qaPass, qaReason);
1343
+ const savedContent = await this._saveAssistantMessage(session.id, finalContent, streamId, effectiveAgentRole, finalUsage, finalModel, reasoningJoined, debugCollector, false, providerName, MessageRole.ASSISTANT, undefined, false, allToolCalls, allIntermediateTexts, qaPass, qaReason, fallbackI18n ? { i18n: fallbackI18n } : undefined);
1035
1344
  // Получаем актуальную сессию после сохранения
1036
1345
  const updatedSession = await this._sessionManager.getSession(session.id);
1037
1346
  const doneData = {
@@ -1066,7 +1375,23 @@ export class ChatOrchestrator {
1066
1375
  if (updatedSession) {
1067
1376
  const userMsgCount = updatedSession.messages.filter((m) => m.role === MessageRole.USER && !m.metadata?.synthetic).length;
1068
1377
  if (userMsgCount === 1) {
1069
- autoTitleSession(updatedSession, this._sessionManager, provider).catch(() => { });
1378
+ const titleAgentDef = this._agentRegistry?.get('title');
1379
+ let titleProvider = provider;
1380
+ if (titleAgentDef?.reasoningLevel && providerName) {
1381
+ const tp = this._getProvider(providerName, updatedSession, titleAgentDef.reasoningLevel);
1382
+ if (tp)
1383
+ titleProvider = tp;
1384
+ }
1385
+ autoTitleSession(updatedSession, this._sessionManager, titleProvider)
1386
+ .then((title) => {
1387
+ if (title) {
1388
+ this._eventBus.publish(updatedSession.id, 'session.updated', {
1389
+ session_id: updatedSession.id,
1390
+ name: title,
1391
+ });
1392
+ }
1393
+ })
1394
+ .catch(() => { });
1070
1395
  }
1071
1396
  }
1072
1397
  // 9. Check context optimization triggers
@@ -1135,11 +1460,8 @@ export class ChatOrchestrator {
1135
1460
  catch (e) {
1136
1461
  const errMsg = String(e);
1137
1462
  warning('ORCHESTRATOR', `Unexpected error: ${errMsg}`);
1138
- return {
1139
- error: errMsg,
1140
- errorType: 'internal',
1141
- canRetry: false,
1142
- };
1463
+ this._cancellationTokens.delete(streamId);
1464
+ return this._handleLlmFailure(session.id, streamId, e, publishChunk);
1143
1465
  }
1144
1466
  }
1145
1467
  cancel(streamId) {
@@ -1147,12 +1469,118 @@ export class ChatOrchestrator {
1147
1469
  if (token)
1148
1470
  token.cancel();
1149
1471
  }
1150
- async _handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId) {
1472
+ /**
1473
+ * Регистрация внешнего cancel-токена (resume саб-агента): позволяет отменить
1474
+ * фоновый прогон обычным путём — orchestrator.cancel(streamId) из
1475
+ * POST /sessions/:id/abort (токен ищется в общей карте по streamId).
1476
+ */
1477
+ registerCancelToken(streamId, token) {
1478
+ this._cancellationTokens.set(streamId, token);
1479
+ }
1480
+ unregisterCancelToken(streamId) {
1481
+ this._cancellationTokens.delete(streamId);
1482
+ }
1483
+ /**
1484
+ * B2: возобновление прерванной/ошибочной дочерней сессии саб-агента.
1485
+ * Валидация и захват блокировки — синхронно (ошибки маппятся route-ом в
1486
+ * 400/404/409), выполнение — фоновое (mimo очереди: у дочерней сессии нет
1487
+ * своего QueueProcessor). Защита от двойного resume — acquireLock +
1488
+ * currentStream (последний также делает сессию abort-совместимой).
1489
+ */
1490
+ async startSubagentResume(childSessionId) {
1491
+ const deps = this._subagentResumeDeps();
1492
+ const { child, parent } = await validateSubagentResume(deps, childSessionId);
1493
+ const streamId = generateStreamId();
1494
+ const processId = `resume:${streamId}`;
1495
+ // Атомарный захват — защита от гонки параллельных resume-запросов
1496
+ const acquired = await this._sessionManager.acquireLock(childSessionId, processId);
1497
+ if (!acquired) {
1498
+ throw new SubagentResumeError('LOCKED', sm('task.resume_locked', { id: childSessionId }));
1499
+ }
1500
+ // currentStream — маркер активности для UI и abort-механики
1501
+ await this._sessionManager.withLock(childSessionId, async (s) => {
1502
+ s.currentStream = {
1503
+ streamId,
1504
+ message: sm('task.resume_marker'),
1505
+ agentRole: s.settings.agentRole,
1506
+ startedAt: nowISO(),
1507
+ cancelled: false,
1508
+ };
1509
+ await this._sessionManager.saveSession(s);
1510
+ });
1511
+ // Отмена: orchestrator.cancel(streamId) → resumeAbort.abort() →
1512
+ // runSubagent видит отмену (стрим + backoff-пауза) → finishAborted.
1513
+ const resumeAbort = new AbortController();
1514
+ const cancelToken = {
1515
+ cancelled: false,
1516
+ cancel: () => {
1517
+ cancelToken.cancelled = true;
1518
+ resumeAbort.abort();
1519
+ },
1520
+ };
1521
+ this.registerCancelToken(streamId, cancelToken);
1522
+ const freshChild = (await this._sessionManager.getSession(childSessionId)) ?? child;
1523
+ void runSubagentResume(deps, {
1524
+ child: freshChild,
1525
+ parent,
1526
+ streamId,
1527
+ abortSignal: resumeAbort.signal,
1528
+ })
1529
+ .catch((err) => {
1530
+ warning('ORCHESTRATOR', `subagent resume failed: ${err instanceof Error ? err.message : String(err)}`);
1531
+ this._eventBus.publish(childSessionId, 'stream.error', {
1532
+ stream_id: streamId,
1533
+ error: String(err),
1534
+ error_type: 'resume_failed',
1535
+ can_retry: false,
1536
+ });
1537
+ })
1538
+ .finally(async () => {
1539
+ this.unregisterCancelToken(streamId);
1540
+ try {
1541
+ await this._sessionManager.withLock(childSessionId, async (s) => {
1542
+ s.currentStream = undefined;
1543
+ await this._sessionManager.saveSession(s);
1544
+ });
1545
+ }
1546
+ catch { /* сессия могла быть удалена */ }
1547
+ try {
1548
+ await this._sessionManager.releaseLock(childSessionId, processId);
1549
+ }
1550
+ catch { /* ignore */ }
1551
+ });
1552
+ return {
1553
+ status: 'started',
1554
+ child_session_id: childSessionId,
1555
+ parent_session_id: parent.id,
1556
+ stream_id: streamId,
1557
+ };
1558
+ }
1559
+ /** Зависимости resume из приватных резолверов оркестратора. */
1560
+ _subagentResumeDeps() {
1561
+ return {
1562
+ sessionManager: this._sessionManager,
1563
+ eventBus: this._eventBus,
1564
+ agentRegistry: this._agentRegistry ?? { get: () => undefined },
1565
+ toolRegistry: this._toolRegistry ?? undefined,
1566
+ contextConfig: this._contextConfig ?? undefined,
1567
+ contextOptimizer: this._optimizer ?? undefined,
1568
+ routerConfig: this._routerConfig ?? undefined,
1569
+ securityConfig: this._securityConfig,
1570
+ permissionConfig: this._permissionConfig,
1571
+ shellPolicy: this._getProtectedPaths() ?? undefined,
1572
+ protectedPaths: this._getProtectedPaths() ?? undefined,
1573
+ protectedPathsEnabled: this._getProtectedPathsEnabled(),
1574
+ getProvider: (child) => this._getProvider(child.settings.provider || '', child, child.settings.reasoningLevel),
1575
+ getWorkingDir: (child) => this._resolveWorkingDir(child),
1576
+ };
1577
+ }
1578
+ async _handleDebugSystemMessage(session, message, signal, publishChunk, debugCollector, streamId, reasoningLevel) {
1151
1579
  const providerName = message.provider || session.settings.provider || '';
1152
- const provider = this._getProvider(providerName, session);
1580
+ const provider = this._getProvider(providerName, session, reasoningLevel);
1153
1581
  if (!provider) {
1154
1582
  return {
1155
- error: `Провайдер ${providerName || 'не указан'} не найден или не настроен`,
1583
+ error: sm('router.provider_not_found', { provider: providerName || sm('router.provider_unspecified') }),
1156
1584
  errorType: 'provider_not_found',
1157
1585
  canRetry: false,
1158
1586
  };
@@ -1207,7 +1635,8 @@ export class ChatOrchestrator {
1207
1635
  }
1208
1636
  catch (e) {
1209
1637
  warning('ORCHESTRATOR', `Debug system message stream error: ${e}`);
1210
- return { error: String(e), errorType: this._classifyError(String(e)), canRetry: this._isRecoverable(String(e)) };
1638
+ this._cancellationTokens.delete(streamId);
1639
+ return this._handleLlmFailure(session.id, streamId, e, publishChunk);
1211
1640
  }
1212
1641
  if (cancelToken.cancelled) {
1213
1642
  publishChunk('stream.cancelled', { stream_id: streamId, content: accumulatedContent });
@@ -1287,7 +1716,8 @@ export class ChatOrchestrator {
1287
1716
  }
1288
1717
  catch (e) {
1289
1718
  warning('ORCHESTRATOR', `Summarization stream error: ${e}`);
1290
- return { error: String(e), errorType: this._classifyError(String(e)), canRetry: this._isRecoverable(String(e)) };
1719
+ this._cancellationTokens.delete(streamId);
1720
+ return this._handleLlmFailure(session.id, streamId, e, publishChunk);
1291
1721
  }
1292
1722
  if (cancelToken.cancelled) {
1293
1723
  publishChunk('stream.cancelled', { stream_id: streamId, content: accumulatedContent });
@@ -1343,13 +1773,19 @@ export class ChatOrchestrator {
1343
1773
  this._cancellationTokens.delete(streamId);
1344
1774
  return {};
1345
1775
  }
1346
- _getProvider(providerName, session) {
1776
+ _getProvider(providerName, session, reasoningLevel) {
1347
1777
  const modelName = session.settings.model || '';
1348
1778
  const sharedConfig = this._providerLoader(providerName);
1349
1779
  if (!sharedConfig)
1350
1780
  return null;
1351
1781
  try {
1352
- const provider = ProviderFactory.create(providerName, toCoreProviderConfig(sharedConfig, modelName || sharedConfig.defaultModel || ''), session.settings.temperature);
1782
+ const effectiveModel = modelName || sharedConfig.defaultModel || '';
1783
+ const contextWindow = this._resolveModelContextWindow(effectiveModel);
1784
+ const provider = ProviderFactory.create(providerName, {
1785
+ ...toCoreProviderConfig(sharedConfig, effectiveModel, reasoningLevel ?? session.settings.reasoningLevel),
1786
+ maxTokens: session.settings.maxTokens,
1787
+ numCtx: contextWindow > 0 ? contextWindow : undefined,
1788
+ }, session.settings.temperature);
1353
1789
  return this._wrapWithGateway(provider, session);
1354
1790
  }
1355
1791
  catch (e) {
@@ -1357,6 +1793,39 @@ export class ChatOrchestrator {
1357
1793
  return null;
1358
1794
  }
1359
1795
  }
1796
+ /**
1797
+ * Размер контекстного окна модели: приоритет у сохранённого значения
1798
+ * (ModelConfig.contextWindow через оптимизатор), иначе справочник.
1799
+ */
1800
+ _resolveModelContextWindow(modelName) {
1801
+ const window = this._optimizer?.getContextWindow(modelName);
1802
+ if (window && window > 0)
1803
+ return window;
1804
+ return lookupContextWindow(modelName).window;
1805
+ }
1806
+ /**
1807
+ * Публикует context.limit_warning, когда оценка токенов контекста достигла
1808
+ * порога (CONTEXT_LIMIT_WARNING_PERCENT) от окна модели. Уведомление не
1809
+ * блокирует вызов LLM — это fire-and-forget сигнал для UI.
1810
+ */
1811
+ _publishContextLimitWarning(sessionId, modelName, messages) {
1812
+ if (!this._optimizer)
1813
+ return;
1814
+ const contextWindow = this._resolveModelContextWindow(modelName);
1815
+ if (!(contextWindow > 0))
1816
+ return;
1817
+ const estimatedTokens = this._optimizer.estimateTokens(messages);
1818
+ if (estimatedTokens <= 0)
1819
+ return;
1820
+ const percent = Math.round((estimatedTokens / contextWindow) * 100);
1821
+ if (percent >= CONTEXT_LIMIT_WARNING_PERCENT) {
1822
+ this._eventBus.publish(sessionId, 'context.limit_warning', {
1823
+ percent,
1824
+ context_window: contextWindow,
1825
+ estimated_tokens: estimatedTokens,
1826
+ });
1827
+ }
1828
+ }
1360
1829
  /**
1361
1830
  * AD-036: оборачивает провайдер в GatewayProvider с привязкой к сессии.
1362
1831
  * Если gateway выключен — возвращает провайдер как есть (обратная совместимость).
@@ -1383,7 +1852,7 @@ export class ChatOrchestrator {
1383
1852
  * Выбор провайдера и модели через роутер (AD-034).
1384
1853
  * Если роутер недоступен — fallback на _getProvider().
1385
1854
  */
1386
- _routeProvider(session, message) {
1855
+ _routeProvider(session, message, reasoningLevel) {
1387
1856
  if (this._router && session.settings.routingEnabled !== false) {
1388
1857
  const toolCallNames = message.toolCalls?.map((tc) => {
1389
1858
  const func = tc.function;
@@ -1394,7 +1863,8 @@ export class ChatOrchestrator {
1394
1863
  userMessage: message.content,
1395
1864
  toolCalls: toolCallNames,
1396
1865
  });
1397
- let provider = this._router.getProvider(decision);
1866
+ const routedWindow = this._resolveModelContextWindow(decision.modelName);
1867
+ let provider = this._router.getProvider(decision, undefined, reasoningLevel ?? session.settings.reasoningLevel, session.settings.maxTokens, routedWindow > 0 ? routedWindow : undefined);
1398
1868
  if (provider) {
1399
1869
  provider = this._wrapWithGateway(provider, session);
1400
1870
  return {
@@ -1408,7 +1878,7 @@ export class ChatOrchestrator {
1408
1878
  // (например, категория не имеет маппинга в routingContent),
1409
1879
  // используем стандартный _getProvider() для провайдера сессии
1410
1880
  const fallbackName = session.settings.provider || '';
1411
- provider = this._getProvider(fallbackName, session);
1881
+ provider = this._getProvider(fallbackName, session, reasoningLevel);
1412
1882
  if (provider) {
1413
1883
  return {
1414
1884
  provider,
@@ -1420,7 +1890,7 @@ export class ChatOrchestrator {
1420
1890
  }
1421
1891
  // Fallback: старый метод (если роутер недоступен или все пути выше не сработали)
1422
1892
  const providerName = message.provider || session.settings.provider || '';
1423
- const provider = this._getProvider(providerName, session);
1893
+ const provider = this._getProvider(providerName, session, reasoningLevel);
1424
1894
  if (!provider)
1425
1895
  return null;
1426
1896
  return {
@@ -1468,6 +1938,7 @@ export class ChatOrchestrator {
1468
1938
  branchId: '',
1469
1939
  toolUse: isOwnMessage ? msg.toolCalls : undefined,
1470
1940
  toolCallId: isOwnMessage ? msg.toolCallId : undefined,
1941
+ reasoning: isOwnMessage && llmRole === MessageRole.ASSISTANT ? msg.reasoning : undefined,
1471
1942
  agentRole: msg.agentRole,
1472
1943
  });
1473
1944
  }
@@ -1503,7 +1974,86 @@ export class ChatOrchestrator {
1503
1974
  return {};
1504
1975
  }
1505
1976
  }
1506
- async _saveAssistantMessage(sessionId, content, streamId, agentRole, usage, model, reasoning, debugCollector, isCancelled, provider, role = MessageRole.ASSISTANT, summaryOf, skipEvent = false, toolResults, intermediateTextBlocks, qaPass, qaReason) {
1977
+ /**
1978
+ * Персистентность tool-chain (Вариант B): настоящие пары
1979
+ * «assistant с toolCalls + tool-результаты» в session.messages, чтобы
1980
+ * _sessionMessagesToLlm на следующем запросе выдавал OpenAI-совместимый
1981
+ * диалог. Промежуточные сообщения помечаются metadata.tool_chain=true —
1982
+ * UI их скрывает (визуализация уже есть в финальном сообщении).
1983
+ * Вызывается и в успешном пути, и при отмене (уже выполненные итерации).
1984
+ */
1985
+ async _persistToolChain(sessionId, streamId, providerName, agentRole, finalModel, allRawToolCalls, allToolCalls, allIntermediateTexts, allReasoning) {
1986
+ for (const raw of allRawToolCalls) {
1987
+ const iteration = raw.iteration;
1988
+ const iterText = allIntermediateTexts.find((t) => t.iteration === iteration)?.text ?? '';
1989
+ const iterReasoning = allReasoning.find((r) => r.iteration === iteration)?.text;
1990
+ const assistantMsg = {
1991
+ id: generateId(),
1992
+ role: MessageRole.ASSISTANT,
1993
+ seq: 0,
1994
+ origin: 'local',
1995
+ parts: [makeTextPart(iterText)],
1996
+ content: iterText,
1997
+ toolCalls: raw.calls,
1998
+ createdAt: nowISO(),
1999
+ disabled: false,
2000
+ pinned: false,
2001
+ hidden: false,
2002
+ model: finalModel,
2003
+ provider: providerName,
2004
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
2005
+ agentRole,
2006
+ metadata: { stream_id: streamId, tool_chain: true },
2007
+ ...(iterReasoning ? { reasoning: iterReasoning } : {}),
2008
+ };
2009
+ await this._sessionManager.addMessage(sessionId, assistantMsg);
2010
+ for (const tc of allToolCalls.filter((t) => t.iteration === iteration)) {
2011
+ const toolContent = String(tc.result?.content ?? '');
2012
+ // metadata ToolResult (в т.ч. i18n) пробрасывается в metadata сообщения —
2013
+ // иначе локализационные ключи не доходят до истории и UI.
2014
+ const toolResultMeta = tc.result?.metadata;
2015
+ const toolMsg = {
2016
+ id: generateId(),
2017
+ role: MessageRole.TOOL,
2018
+ seq: 0,
2019
+ origin: 'local',
2020
+ parts: [makeTextPart(toolContent)],
2021
+ content: toolContent,
2022
+ toolCallId: tc.id,
2023
+ createdAt: nowISO(),
2024
+ disabled: false,
2025
+ pinned: false,
2026
+ hidden: false,
2027
+ model: finalModel,
2028
+ provider: providerName,
2029
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
2030
+ agentRole,
2031
+ metadata: {
2032
+ ...(toolResultMeta ?? {}),
2033
+ stream_id: streamId,
2034
+ tool_chain: true,
2035
+ tool_name: tc.name,
2036
+ args: tc.args,
2037
+ },
2038
+ };
2039
+ await this._sessionManager.addMessage(sessionId, toolMsg);
2040
+ }
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Склейка накопленного текста: все завершённые итерации (allIntermediateTexts)
2045
+ * + текст текущей (незавершённой) итерации. Пустые фрагменты пропускаются.
2046
+ * Используется при отмене (abort), чтобы в истории сохранялся полный
2047
+ * частичный ответ, а не только обрывок последней итерации.
2048
+ */
2049
+ _joinAccumulatedTexts(intermediateTexts, current) {
2050
+ const parts = [...intermediateTexts.map((t) => t.text), current]
2051
+ .filter((t) => t && t.trim().length > 0);
2052
+ return parts.join('\n\n');
2053
+ }
2054
+ async _saveAssistantMessage(sessionId, content, streamId, agentRole, usage, model, reasoning, debugCollector, isCancelled, provider, role = MessageRole.ASSISTANT, summaryOf, skipEvent = false, toolResults, intermediateTextBlocks, qaPass, qaReason,
2055
+ /** Дополнительные поля metadata (например metadata.i18n фоллбек-статусов). */
2056
+ metadataExtra) {
1507
2057
  const finalContent = content;
1508
2058
  if (!isCancelled && content) {
1509
2059
  debugCollector.captureRawModelResponse(content);
@@ -1512,7 +2062,7 @@ export class ChatOrchestrator {
1512
2062
  debugCollector.captureReasoning(reasoning);
1513
2063
  }
1514
2064
  const debugInfo = debugCollector.getDebugInfo();
1515
- const metadata = { stream_id: streamId };
2065
+ const metadata = { stream_id: streamId, ...metadataExtra };
1516
2066
  if (toolResults && toolResults.length > 0) {
1517
2067
  metadata.tool_results = toolResults;
1518
2068
  }
@@ -1600,51 +2150,111 @@ export class ChatOrchestrator {
1600
2150
  return finalContent;
1601
2151
  }
1602
2152
  _classifyError(errMsg) {
1603
- const lower = errMsg.toLowerCase();
1604
- if (lower.includes('rate') || lower.includes('limit') || lower.includes('429'))
1605
- return 'rate_limit';
1606
- if (lower.includes('timeout') || lower.includes('timed out'))
1607
- return 'timeout';
1608
- if (lower.includes('502') || lower.includes('503') || lower.includes('bad gateway'))
1609
- return 'server_error';
1610
- if (lower.includes('context length') || lower.includes('too long'))
1611
- return 'context_length';
1612
- if (lower.includes('401') || lower.includes('403') || lower.includes('auth'))
1613
- return 'auth_error';
1614
- if (lower.includes('model') && (lower.includes('not found') || lower.includes('invalid')))
1615
- return 'model_not_found';
1616
- return 'unknown';
2153
+ // Общий хелпер (используется также retry-циклом саб-агента) — таксономия единая
2154
+ return classifyLlmError(errMsg);
1617
2155
  }
1618
- _isRecoverable(errMsg) {
1619
- const lower = errMsg.toLowerCase();
1620
- const unrecoverable = [
1621
- 'model not found', 'model_not_found', 'does not exist',
1622
- 'invalid model', 'unknown model', 'invalid api key',
1623
- 'incorrect api key', '401', 'authentication', 'unauthorized',
1624
- ];
1625
- return !unrecoverable.some((p) => lower.includes(p));
2156
+ /**
2157
+ * Человекочитаемое описание ошибки LLM (таймаут/обрыв соединения, сетевая
2158
+ * ошибка). Для остальных ошибок — исходный текст.
2159
+ */
2160
+ _formatLlmError(error) {
2161
+ const raw = error instanceof Error ? error.message : String(error);
2162
+ const lower = raw.toLowerCase();
2163
+ if (lower.includes('abort') || lower.includes('operation was aborted') || lower.includes('timed out') || lower.includes('timeout')) {
2164
+ return sm('router.timeout');
2165
+ }
2166
+ if (lower.includes('fetch failed') || lower.includes('network') || lower.includes('econnrefused') || lower.includes('enotfound') || lower.includes('eai_again')) {
2167
+ return sm('router.network_error');
2168
+ }
2169
+ return raw;
1626
2170
  }
1627
2171
  /**
1628
- * AD-029: «Разрешить в сессии» (once:false). Добавляет allow-правило для тула
1629
- * в permission-конфиг сессии и сохраняет его. Если у сессии не было своего
1630
- * конфига наследует глобальные правила, чтобы не потерять их при оверрайде.
2172
+ * Единая обработка ошибки LLM-стрима: пишет last_error в user-сообщение
2173
+ * сессии (по stream_id) и возвращает ProcessMessageResult. Публикация
2174
+ * финального исхода (stream.retry для recoverable / stream.error для
2175
+ * non-recoverable) — зона ответственности QueueProcessor, чтобы не было
2176
+ * двойной публикации stream.error. Если ошибка recoverable — session
2177
+ * разблокируется на уровне QueueProcessor после исчерпания попыток; сессия
2178
+ * не остаётся заблокированной навсегда.
1631
2179
  */
1632
- async _grantSessionToolAllow(session, toolName) {
1633
- let cfg = session.settings.permission;
1634
- if (!cfg) {
1635
- cfg = {
1636
- rules: { ...this._permissionConfig.rules },
1637
- askTimeoutMs: this._permissionConfig.askTimeoutMs,
1638
- };
2180
+ async _handleLlmFailure(sessionId, streamId, error, _publishChunk) {
2181
+ const rawError = error instanceof Error ? error.message : String(error);
2182
+ const humanError = this._formatLlmError(error);
2183
+ // ContextLengthExceededError несёт русское сообщение («Контекстное окно
2184
+ // превышено») текстовый классификатор его не распознаёт, поэтому тип
2185
+ // определяем по классу ошибки, чтобы UI показал дружелюбное сообщение.
2186
+ const errorType = error instanceof ContextLengthExceededError
2187
+ ? 'context_length'
2188
+ : this._classifyError(rawError);
2189
+ const canRetry = isLlmErrorRecoverable(error, rawError, errorType);
2190
+ try {
2191
+ await this._sessionManager.withLock(sessionId, async (s) => {
2192
+ const userMsg = s.messages.find((m) => m.role === MessageRole.USER && m.metadata?.stream_id === streamId);
2193
+ if (userMsg) {
2194
+ userMsg.metadata = { ...userMsg.metadata, last_error: humanError };
2195
+ await this._sessionManager.saveSession(s);
2196
+ }
2197
+ });
2198
+ }
2199
+ catch (err) {
2200
+ warning('ORCHESTRATOR', `Не удалось записать last_error: ${err instanceof Error ? err.message : String(err)}`);
1639
2201
  }
1640
- const next = {
1641
- ...cfg,
1642
- rules: { ...cfg.rules, [toolName]: 'allow' },
2202
+ return { error: humanError, errorType, canRetry };
2203
+ }
2204
+ /** Строит permission-конфиг с добавленным грантом для заданной базы правил. */
2205
+ _applyToolAllow(cfg, toolName, paths) {
2206
+ const base = cfg ?? {
2207
+ rules: { ...this._permissionConfig.rules },
2208
+ askTimeoutMs: this._permissionConfig.askTimeoutMs,
1643
2209
  };
2210
+ const rules = { ...base.rules };
2211
+ if (paths.length > 0) {
2212
+ if (rules[toolName] !== 'allow') {
2213
+ const existing = (typeof rules[toolName] === 'object' && rules[toolName])
2214
+ ? { ...rules[toolName] }
2215
+ : {};
2216
+ for (const p of paths)
2217
+ existing[p] = 'allow';
2218
+ rules[toolName] = existing;
2219
+ }
2220
+ }
2221
+ else {
2222
+ rules[toolName] = 'allow';
2223
+ }
2224
+ return { ...base, rules };
2225
+ }
2226
+ /**
2227
+ * AD-029: «Разрешить в сессии» (once:false). Если у вызова есть файловые
2228
+ * параметры — одобряет КОНКРЕТНЫЕ абсолютные пути (правило-объект
2229
+ * `rules[tool] = { path: 'allow' }`), иначе (shell/task/mcp) — весь тул.
2230
+ * Если тул уже разрешён полностью ('allow') — не понижает до пути.
2231
+ */
2232
+ async _grantSessionToolAllow(session, toolName, toolArgs, workingDir) {
2233
+ const paths = extractPaths(toolName, toolArgs, workingDir);
2234
+ const next = this._applyToolAllow(session.settings.permission, toolName, paths);
1644
2235
  await this._sessionManager.withLock(session.id, async (s) => {
1645
2236
  s.settings.permission = next;
1646
2237
  await this._sessionManager.saveSession(s);
2238
+ this._eventBus.publish(session.id, 'session.updated', {
2239
+ session_id: session.id,
2240
+ settings: s.settings,
2241
+ });
1647
2242
  });
2243
+ // AD-029: «Разрешить в сессии» для сабагентской (дочерней) сессии
2244
+ // распространяется и на родителя (parentId): путь в правиле — абсолютный
2245
+ // канонический, поэтому грант совпадёт при чтении того же файла и родителем,
2246
+ // и ребёнком. Родительский грант обновляет и effectivePermissionConfig, и
2247
+ // PermissionEvaluator при следующей обработке (сессия сохраняется в store).
2248
+ if (session.parentId) {
2249
+ await this._sessionManager.withLock(session.parentId, async (parent) => {
2250
+ parent.settings.permission = this._applyToolAllow(parent.settings.permission, toolName, paths);
2251
+ await this._sessionManager.saveSession(parent);
2252
+ this._eventBus.publish(parent.id, 'session.updated', {
2253
+ session_id: parent.id,
2254
+ settings: parent.settings,
2255
+ });
2256
+ });
2257
+ }
1648
2258
  return next;
1649
2259
  }
1650
2260
  }