@cjhyy/code-shell-core 0.5.0-rc.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 (484) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +162 -0
  3. package/dist/agent/coordinator.d.ts +49 -0
  4. package/dist/agent/coordinator.js +77 -0
  5. package/dist/arena/arena.d.ts +43 -0
  6. package/dist/arena/arena.js +334 -0
  7. package/dist/arena/context/context-tools.d.ts +16 -0
  8. package/dist/arena/context/context-tools.js +231 -0
  9. package/dist/arena/detect-mode.d.ts +20 -0
  10. package/dist/arena/detect-mode.js +78 -0
  11. package/dist/arena/digest-builder.d.ts +25 -0
  12. package/dist/arena/digest-builder.js +120 -0
  13. package/dist/arena/index.d.ts +29 -0
  14. package/dist/arena/index.js +29 -0
  15. package/dist/arena/iterate/convergence.d.ts +25 -0
  16. package/dist/arena/iterate/convergence.js +103 -0
  17. package/dist/arena/iterate/formats/index.d.ts +22 -0
  18. package/dist/arena/iterate/formats/index.js +283 -0
  19. package/dist/arena/iterate/index.d.ts +11 -0
  20. package/dist/arena/iterate/index.js +9 -0
  21. package/dist/arena/iterate/iterative-arena.d.ts +23 -0
  22. package/dist/arena/iterate/iterative-arena.js +237 -0
  23. package/dist/arena/iterate/parse.d.ts +42 -0
  24. package/dist/arena/iterate/parse.js +123 -0
  25. package/dist/arena/iterate/phases/argue.d.ts +22 -0
  26. package/dist/arena/iterate/phases/argue.js +159 -0
  27. package/dist/arena/iterate/phases/revise.d.ts +16 -0
  28. package/dist/arena/iterate/phases/revise.js +62 -0
  29. package/dist/arena/iterate/phases/tournament.d.ts +34 -0
  30. package/dist/arena/iterate/phases/tournament.js +113 -0
  31. package/dist/arena/iterate/tools/web-tools.d.ts +13 -0
  32. package/dist/arena/iterate/tools/web-tools.js +54 -0
  33. package/dist/arena/iterate/types.d.ts +152 -0
  34. package/dist/arena/iterate/types.js +8 -0
  35. package/dist/arena/ledger.d.ts +47 -0
  36. package/dist/arena/ledger.js +151 -0
  37. package/dist/arena/lenses/architecture.d.ts +5 -0
  38. package/dist/arena/lenses/architecture.js +22 -0
  39. package/dist/arena/lenses/engineering.d.ts +5 -0
  40. package/dist/arena/lenses/engineering.js +22 -0
  41. package/dist/arena/lenses/general.d.ts +5 -0
  42. package/dist/arena/lenses/general.js +20 -0
  43. package/dist/arena/lenses/index.d.ts +16 -0
  44. package/dist/arena/lenses/index.js +47 -0
  45. package/dist/arena/lenses/product.d.ts +5 -0
  46. package/dist/arena/lenses/product.js +22 -0
  47. package/dist/arena/model-presets.d.ts +23 -0
  48. package/dist/arena/model-presets.js +44 -0
  49. package/dist/arena/phases/adjudication.d.ts +24 -0
  50. package/dist/arena/phases/adjudication.js +144 -0
  51. package/dist/arena/phases/build-consensus.d.ts +29 -0
  52. package/dist/arena/phases/build-consensus.js +86 -0
  53. package/dist/arena/phases/claim-registry.d.ts +26 -0
  54. package/dist/arena/phases/claim-registry.js +60 -0
  55. package/dist/arena/phases/cross-review.d.ts +45 -0
  56. package/dist/arena/phases/cross-review.js +224 -0
  57. package/dist/arena/phases/debate-rounds.d.ts +27 -0
  58. package/dist/arena/phases/debate-rounds.js +162 -0
  59. package/dist/arena/phases/participant-research.d.ts +38 -0
  60. package/dist/arena/phases/participant-research.js +317 -0
  61. package/dist/arena/phases/planning-detail-expansion.d.ts +38 -0
  62. package/dist/arena/phases/planning-detail-expansion.js +121 -0
  63. package/dist/arena/planner.d.ts +27 -0
  64. package/dist/arena/planner.js +312 -0
  65. package/dist/arena/providers/docs.d.ts +6 -0
  66. package/dist/arena/providers/docs.js +108 -0
  67. package/dist/arena/providers/git.d.ts +8 -0
  68. package/dist/arena/providers/git.js +174 -0
  69. package/dist/arena/providers/index.d.ts +32 -0
  70. package/dist/arena/providers/index.js +132 -0
  71. package/dist/arena/providers/none.d.ts +7 -0
  72. package/dist/arena/providers/none.js +11 -0
  73. package/dist/arena/providers/repo.d.ts +6 -0
  74. package/dist/arena/providers/repo.js +255 -0
  75. package/dist/arena/render/session.d.ts +17 -0
  76. package/dist/arena/render/session.js +190 -0
  77. package/dist/arena/render/terminal.d.ts +34 -0
  78. package/dist/arena/render/terminal.js +286 -0
  79. package/dist/arena/strategies/discussion.d.ts +25 -0
  80. package/dist/arena/strategies/discussion.js +143 -0
  81. package/dist/arena/strategies/index.d.ts +15 -0
  82. package/dist/arena/strategies/index.js +28 -0
  83. package/dist/arena/strategies/language-wrapper.d.ts +17 -0
  84. package/dist/arena/strategies/language-wrapper.js +102 -0
  85. package/dist/arena/strategies/lens-wrapper.d.ts +16 -0
  86. package/dist/arena/strategies/lens-wrapper.js +236 -0
  87. package/dist/arena/strategies/planning.d.ts +30 -0
  88. package/dist/arena/strategies/planning.js +225 -0
  89. package/dist/arena/strategies/review.d.ts +26 -0
  90. package/dist/arena/strategies/review.js +168 -0
  91. package/dist/arena/strategies/utils.d.ts +39 -0
  92. package/dist/arena/strategies/utils.js +647 -0
  93. package/dist/arena/tools/selector.d.ts +17 -0
  94. package/dist/arena/tools/selector.js +61 -0
  95. package/dist/arena/transitions.d.ts +48 -0
  96. package/dist/arena/transitions.js +92 -0
  97. package/dist/arena/types.d.ts +508 -0
  98. package/dist/arena/types.js +27 -0
  99. package/dist/cli/agent-server-stdio.d.ts +32 -0
  100. package/dist/cli/agent-server-stdio.js +116 -0
  101. package/dist/colorizer.d.ts +23 -0
  102. package/dist/colorizer.js +12 -0
  103. package/dist/context/compaction.d.ts +116 -0
  104. package/dist/context/compaction.js +534 -0
  105. package/dist/context/manager.d.ts +145 -0
  106. package/dist/context/manager.js +423 -0
  107. package/dist/context/token-counter.d.ts +27 -0
  108. package/dist/context/token-counter.js +91 -0
  109. package/dist/context/tool-result-storage.d.ts +96 -0
  110. package/dist/context/tool-result-storage.js +296 -0
  111. package/dist/cost-tracker.d.ts +64 -0
  112. package/dist/cost-tracker.js +314 -0
  113. package/dist/cron/scheduler.d.ts +34 -0
  114. package/dist/cron/scheduler.js +115 -0
  115. package/dist/data/deepseek-models.json +46 -0
  116. package/dist/data/gemini-models.json +67 -0
  117. package/dist/data/openai-models.json +175 -0
  118. package/dist/data/openrouter-models.d.ts +35 -0
  119. package/dist/data/openrouter-models.js +36 -0
  120. package/dist/data/openrouter-models.json +4626 -0
  121. package/dist/data/openrouter-sync.d.ts +16 -0
  122. package/dist/data/openrouter-sync.js +53 -0
  123. package/dist/data/static-catalogs.d.ts +25 -0
  124. package/dist/data/static-catalogs.js +26 -0
  125. package/dist/data/zai-models.json +56 -0
  126. package/dist/engine/cost-store.d.ts +22 -0
  127. package/dist/engine/cost-store.js +15 -0
  128. package/dist/engine/engine.d.ts +295 -0
  129. package/dist/engine/engine.js +1467 -0
  130. package/dist/engine/model-facade.d.ts +24 -0
  131. package/dist/engine/model-facade.js +194 -0
  132. package/dist/engine/parse-task.d.ts +59 -0
  133. package/dist/engine/parse-task.js +135 -0
  134. package/dist/engine/patch-orphaned-tools.d.ts +52 -0
  135. package/dist/engine/patch-orphaned-tools.js +95 -0
  136. package/dist/engine/query.d.ts +50 -0
  137. package/dist/engine/query.js +107 -0
  138. package/dist/engine/runtime.d.ts +50 -0
  139. package/dist/engine/runtime.js +59 -0
  140. package/dist/engine/streaming-tool-queue.d.ts +34 -0
  141. package/dist/engine/streaming-tool-queue.js +61 -0
  142. package/dist/engine/token-budget.d.ts +22 -0
  143. package/dist/engine/token-budget.js +40 -0
  144. package/dist/engine/tool-summary.d.ts +13 -0
  145. package/dist/engine/tool-summary.js +35 -0
  146. package/dist/engine/turn-loop.d.ts +122 -0
  147. package/dist/engine/turn-loop.js +602 -0
  148. package/dist/engine/turn-state.d.ts +20 -0
  149. package/dist/engine/turn-state.js +17 -0
  150. package/dist/exceptions.d.ts +61 -0
  151. package/dist/exceptions.js +116 -0
  152. package/dist/git/utils.d.ts +35 -0
  153. package/dist/git/utils.js +98 -0
  154. package/dist/git/worktree.d.ts +36 -0
  155. package/dist/git/worktree.js +139 -0
  156. package/dist/hooks/events.d.ts +114 -0
  157. package/dist/hooks/events.js +4 -0
  158. package/dist/hooks/inject.d.ts +19 -0
  159. package/dist/hooks/inject.js +29 -0
  160. package/dist/hooks/registry.d.ts +15 -0
  161. package/dist/hooks/registry.js +84 -0
  162. package/dist/hooks/shell-runner.d.ts +45 -0
  163. package/dist/hooks/shell-runner.js +173 -0
  164. package/dist/index.d.ts +117 -0
  165. package/dist/index.js +145 -0
  166. package/dist/llm/api-key-sanitize.d.ts +22 -0
  167. package/dist/llm/api-key-sanitize.js +81 -0
  168. package/dist/llm/capabilities/index.d.ts +20 -0
  169. package/dist/llm/capabilities/index.js +39 -0
  170. package/dist/llm/capabilities/rules.d.ts +26 -0
  171. package/dist/llm/capabilities/rules.js +215 -0
  172. package/dist/llm/capabilities/types.d.ts +114 -0
  173. package/dist/llm/capabilities/types.js +24 -0
  174. package/dist/llm/client-base.d.ts +29 -0
  175. package/dist/llm/client-base.js +137 -0
  176. package/dist/llm/client-factory.d.ts +10 -0
  177. package/dist/llm/client-factory.js +29 -0
  178. package/dist/llm/model-cache.d.ts +21 -0
  179. package/dist/llm/model-cache.js +46 -0
  180. package/dist/llm/model-fetcher.d.ts +29 -0
  181. package/dist/llm/model-fetcher.js +282 -0
  182. package/dist/llm/model-pool.d.ts +95 -0
  183. package/dist/llm/model-pool.js +246 -0
  184. package/dist/llm/provider-catalog.d.ts +32 -0
  185. package/dist/llm/provider-catalog.js +50 -0
  186. package/dist/llm/provider-kinds.d.ts +21 -0
  187. package/dist/llm/provider-kinds.js +132 -0
  188. package/dist/llm/providers/anthropic.d.ts +19 -0
  189. package/dist/llm/providers/anthropic.js +267 -0
  190. package/dist/llm/providers/openai.d.ts +64 -0
  191. package/dist/llm/providers/openai.js +606 -0
  192. package/dist/llm/retry.d.ts +12 -0
  193. package/dist/llm/retry.js +17 -0
  194. package/dist/llm/stream-watchdog.d.ts +42 -0
  195. package/dist/llm/stream-watchdog.js +70 -0
  196. package/dist/llm/token-counter.d.ts +17 -0
  197. package/dist/llm/token-counter.js +36 -0
  198. package/dist/llm/types.d.ts +37 -0
  199. package/dist/llm/types.js +4 -0
  200. package/dist/logging/logger.d.ts +109 -0
  201. package/dist/logging/logger.js +390 -0
  202. package/dist/logging/sanitize-messages.d.ts +42 -0
  203. package/dist/logging/sanitize-messages.js +150 -0
  204. package/dist/logging/session-recorder.d.ts +90 -0
  205. package/dist/logging/session-recorder.js +304 -0
  206. package/dist/lsp/client.d.ts +41 -0
  207. package/dist/lsp/client.js +166 -0
  208. package/dist/lsp/manager.d.ts +40 -0
  209. package/dist/lsp/manager.js +124 -0
  210. package/dist/lsp/servers.d.ts +16 -0
  211. package/dist/lsp/servers.js +60 -0
  212. package/dist/migrate-models.d.ts +45 -0
  213. package/dist/migrate-models.js +179 -0
  214. package/dist/onboarding.d.ts +189 -0
  215. package/dist/onboarding.js +625 -0
  216. package/dist/plugins/gitOps.d.ts +32 -0
  217. package/dist/plugins/gitOps.js +62 -0
  218. package/dist/plugins/installedPlugins.d.ts +15 -0
  219. package/dist/plugins/installedPlugins.js +56 -0
  220. package/dist/plugins/knownMarketplaces.d.ts +10 -0
  221. package/dist/plugins/knownMarketplaces.js +46 -0
  222. package/dist/plugins/loadPluginHooks.d.ts +52 -0
  223. package/dist/plugins/loadPluginHooks.js +162 -0
  224. package/dist/plugins/marketplaceManager.d.ts +49 -0
  225. package/dist/plugins/marketplaceManager.js +142 -0
  226. package/dist/plugins/parseMarketplaceInput.d.ts +16 -0
  227. package/dist/plugins/parseMarketplaceInput.js +73 -0
  228. package/dist/plugins/pluginCommandHook.d.ts +54 -0
  229. package/dist/plugins/pluginCommandHook.js +180 -0
  230. package/dist/plugins/pluginCommandsLoader.d.ts +25 -0
  231. package/dist/plugins/pluginCommandsLoader.js +114 -0
  232. package/dist/plugins/pluginInstaller.d.ts +36 -0
  233. package/dist/plugins/pluginInstaller.js +228 -0
  234. package/dist/plugins/schemas.d.ts +9 -0
  235. package/dist/plugins/schemas.js +136 -0
  236. package/dist/plugins/types.d.ts +75 -0
  237. package/dist/plugins/types.js +7 -0
  238. package/dist/plugins/varRewrite.d.ts +42 -0
  239. package/dist/plugins/varRewrite.js +129 -0
  240. package/dist/preset/index.d.ts +60 -0
  241. package/dist/preset/index.js +159 -0
  242. package/dist/product/define.d.ts +60 -0
  243. package/dist/product/define.js +123 -0
  244. package/dist/product/index.d.ts +5 -0
  245. package/dist/product/index.js +4 -0
  246. package/dist/product/types.d.ts +111 -0
  247. package/dist/product/types.js +21 -0
  248. package/dist/prompt/composer.d.ts +49 -0
  249. package/dist/prompt/composer.js +168 -0
  250. package/dist/prompt/instruction-scanner.d.ts +54 -0
  251. package/dist/prompt/instruction-scanner.js +187 -0
  252. package/dist/prompt/section-cache.d.ts +15 -0
  253. package/dist/prompt/section-cache.js +31 -0
  254. package/dist/prompt/section-loader.d.ts +26 -0
  255. package/dist/prompt/section-loader.js +53 -0
  256. package/dist/prompt/sections/base.md +35 -0
  257. package/dist/prompt/sections/coding.md +31 -0
  258. package/dist/prompt/sections/orchestration.md +16 -0
  259. package/dist/prompt/sections/tone.md +9 -0
  260. package/dist/protocol/chat-session-manager.d.ts +33 -0
  261. package/dist/protocol/chat-session-manager.js +65 -0
  262. package/dist/protocol/chat-session.d.ts +49 -0
  263. package/dist/protocol/chat-session.js +86 -0
  264. package/dist/protocol/client.d.ts +104 -0
  265. package/dist/protocol/client.js +260 -0
  266. package/dist/protocol/factories.d.ts +93 -0
  267. package/dist/protocol/factories.js +64 -0
  268. package/dist/protocol/helpers.d.ts +42 -0
  269. package/dist/protocol/helpers.js +55 -0
  270. package/dist/protocol/index.d.ts +4 -0
  271. package/dist/protocol/index.js +4 -0
  272. package/dist/protocol/server.d.ts +80 -0
  273. package/dist/protocol/server.js +855 -0
  274. package/dist/protocol/transport.d.ts +37 -0
  275. package/dist/protocol/transport.js +85 -0
  276. package/dist/protocol/types.d.ts +221 -0
  277. package/dist/protocol/types.js +61 -0
  278. package/dist/remote/bridge.d.ts +37 -0
  279. package/dist/remote/bridge.js +103 -0
  280. package/dist/run/ArtifactTracker.d.ts +38 -0
  281. package/dist/run/ArtifactTracker.js +132 -0
  282. package/dist/run/CheckpointWriter.d.ts +53 -0
  283. package/dist/run/CheckpointWriter.js +135 -0
  284. package/dist/run/EngineRunner.d.ts +72 -0
  285. package/dist/run/EngineRunner.js +119 -0
  286. package/dist/run/Evaluator.d.ts +56 -0
  287. package/dist/run/Evaluator.js +62 -0
  288. package/dist/run/FileRunStore.d.ts +39 -0
  289. package/dist/run/FileRunStore.js +176 -0
  290. package/dist/run/Heartbeat.d.ts +54 -0
  291. package/dist/run/Heartbeat.js +123 -0
  292. package/dist/run/RunApprovalBackend.d.ts +53 -0
  293. package/dist/run/RunApprovalBackend.js +85 -0
  294. package/dist/run/RunLock.d.ts +40 -0
  295. package/dist/run/RunLock.js +98 -0
  296. package/dist/run/RunManager.d.ts +82 -0
  297. package/dist/run/RunManager.js +587 -0
  298. package/dist/run/RunQueue.d.ts +27 -0
  299. package/dist/run/RunQueue.js +73 -0
  300. package/dist/run/RunStore.d.ts +25 -0
  301. package/dist/run/RunStore.js +9 -0
  302. package/dist/run/factory.d.ts +63 -0
  303. package/dist/run/factory.js +51 -0
  304. package/dist/run/index.d.ts +17 -0
  305. package/dist/run/index.js +23 -0
  306. package/dist/run/types.d.ts +136 -0
  307. package/dist/run/types.js +28 -0
  308. package/dist/runtime/safe-spawn.d.ts +118 -0
  309. package/dist/runtime/safe-spawn.js +268 -0
  310. package/dist/services/analytics.d.ts +29 -0
  311. package/dist/services/analytics.js +84 -0
  312. package/dist/services/auto-dream.d.ts +49 -0
  313. package/dist/services/auto-dream.js +124 -0
  314. package/dist/services/diagnostics.d.ts +41 -0
  315. package/dist/services/diagnostics.js +99 -0
  316. package/dist/services/extract-memories.d.ts +31 -0
  317. package/dist/services/extract-memories.js +71 -0
  318. package/dist/services/index.d.ts +11 -0
  319. package/dist/services/index.js +17 -0
  320. package/dist/services/memory-orchestrator.d.ts +60 -0
  321. package/dist/services/memory-orchestrator.js +133 -0
  322. package/dist/services/notifier.d.ts +25 -0
  323. package/dist/services/notifier.js +69 -0
  324. package/dist/services/oauth.d.ts +27 -0
  325. package/dist/services/oauth.js +153 -0
  326. package/dist/services/session-memory.d.ts +37 -0
  327. package/dist/services/session-memory.js +81 -0
  328. package/dist/session/file-history.d.ts +41 -0
  329. package/dist/session/file-history.js +115 -0
  330. package/dist/session/memory.d.ts +104 -0
  331. package/dist/session/memory.js +310 -0
  332. package/dist/session/session-manager.d.ts +41 -0
  333. package/dist/session/session-manager.js +261 -0
  334. package/dist/session/transcript.d.ts +39 -0
  335. package/dist/session/transcript.js +186 -0
  336. package/dist/settings/manager.d.ts +36 -0
  337. package/dist/settings/manager.js +187 -0
  338. package/dist/settings/schema.d.ts +1219 -0
  339. package/dist/settings/schema.js +246 -0
  340. package/dist/skills/frontmatter.d.ts +13 -0
  341. package/dist/skills/frontmatter.js +70 -0
  342. package/dist/skills/index.d.ts +7 -0
  343. package/dist/skills/index.js +6 -0
  344. package/dist/skills/scanner.d.ts +31 -0
  345. package/dist/skills/scanner.js +171 -0
  346. package/dist/state.d.ts +160 -0
  347. package/dist/state.js +267 -0
  348. package/dist/tool-system/builtin/agent-notifications.d.ts +84 -0
  349. package/dist/tool-system/builtin/agent-notifications.js +197 -0
  350. package/dist/tool-system/builtin/agent-registry.d.ts +67 -0
  351. package/dist/tool-system/builtin/agent-registry.js +126 -0
  352. package/dist/tool-system/builtin/agent-transcript-translator.d.ts +22 -0
  353. package/dist/tool-system/builtin/agent-transcript-translator.js +160 -0
  354. package/dist/tool-system/builtin/agent.d.ts +16 -0
  355. package/dist/tool-system/builtin/agent.js +364 -0
  356. package/dist/tool-system/builtin/apply-patch/applier.d.ts +26 -0
  357. package/dist/tool-system/builtin/apply-patch/applier.js +250 -0
  358. package/dist/tool-system/builtin/apply-patch/index.d.ts +20 -0
  359. package/dist/tool-system/builtin/apply-patch/index.js +101 -0
  360. package/dist/tool-system/builtin/apply-patch/parser.d.ts +17 -0
  361. package/dist/tool-system/builtin/apply-patch/parser.js +212 -0
  362. package/dist/tool-system/builtin/apply-patch/seek-sequence.d.ts +18 -0
  363. package/dist/tool-system/builtin/apply-patch/seek-sequence.js +123 -0
  364. package/dist/tool-system/builtin/apply-patch/types.d.ts +49 -0
  365. package/dist/tool-system/builtin/apply-patch/types.js +13 -0
  366. package/dist/tool-system/builtin/arena.d.ts +31 -0
  367. package/dist/tool-system/builtin/arena.js +416 -0
  368. package/dist/tool-system/builtin/ask-user.d.ts +20 -0
  369. package/dist/tool-system/builtin/ask-user.js +99 -0
  370. package/dist/tool-system/builtin/bash.d.ts +21 -0
  371. package/dist/tool-system/builtin/bash.js +149 -0
  372. package/dist/tool-system/builtin/brief.d.ts +6 -0
  373. package/dist/tool-system/builtin/brief.js +41 -0
  374. package/dist/tool-system/builtin/config.d.ts +6 -0
  375. package/dist/tool-system/builtin/config.js +67 -0
  376. package/dist/tool-system/builtin/cron.d.ts +10 -0
  377. package/dist/tool-system/builtin/cron.js +61 -0
  378. package/dist/tool-system/builtin/edit.d.ts +6 -0
  379. package/dist/tool-system/builtin/edit.js +100 -0
  380. package/dist/tool-system/builtin/file-cache.d.ts +11 -0
  381. package/dist/tool-system/builtin/file-cache.js +39 -0
  382. package/dist/tool-system/builtin/glob.d.ts +7 -0
  383. package/dist/tool-system/builtin/glob.js +76 -0
  384. package/dist/tool-system/builtin/grep.d.ts +7 -0
  385. package/dist/tool-system/builtin/grep.js +150 -0
  386. package/dist/tool-system/builtin/index.d.ts +20 -0
  387. package/dist/tool-system/builtin/index.js +449 -0
  388. package/dist/tool-system/builtin/lsp.d.ts +6 -0
  389. package/dist/tool-system/builtin/lsp.js +141 -0
  390. package/dist/tool-system/builtin/mcp-tools.d.ts +10 -0
  391. package/dist/tool-system/builtin/mcp-tools.js +102 -0
  392. package/dist/tool-system/builtin/memory.d.ts +26 -0
  393. package/dist/tool-system/builtin/memory.js +226 -0
  394. package/dist/tool-system/builtin/notebook-edit.d.ts +6 -0
  395. package/dist/tool-system/builtin/notebook-edit.js +124 -0
  396. package/dist/tool-system/builtin/plan.d.ts +15 -0
  397. package/dist/tool-system/builtin/plan.js +55 -0
  398. package/dist/tool-system/builtin/powershell.d.ts +11 -0
  399. package/dist/tool-system/builtin/powershell.js +64 -0
  400. package/dist/tool-system/builtin/read.d.ts +6 -0
  401. package/dist/tool-system/builtin/read.js +70 -0
  402. package/dist/tool-system/builtin/remote-trigger.d.ts +6 -0
  403. package/dist/tool-system/builtin/remote-trigger.js +54 -0
  404. package/dist/tool-system/builtin/repl.d.ts +11 -0
  405. package/dist/tool-system/builtin/repl.js +82 -0
  406. package/dist/tool-system/builtin/send-message.d.ts +6 -0
  407. package/dist/tool-system/builtin/send-message.js +47 -0
  408. package/dist/tool-system/builtin/skill-prompt.d.ts +20 -0
  409. package/dist/tool-system/builtin/skill-prompt.js +52 -0
  410. package/dist/tool-system/builtin/skill.d.ts +9 -0
  411. package/dist/tool-system/builtin/skill.js +55 -0
  412. package/dist/tool-system/builtin/sleep.d.ts +6 -0
  413. package/dist/tool-system/builtin/sleep.js +33 -0
  414. package/dist/tool-system/builtin/task.d.ts +48 -0
  415. package/dist/tool-system/builtin/task.js +139 -0
  416. package/dist/tool-system/builtin/tool-search.d.ts +11 -0
  417. package/dist/tool-system/builtin/tool-search.js +90 -0
  418. package/dist/tool-system/builtin/web-fetch.d.ts +10 -0
  419. package/dist/tool-system/builtin/web-fetch.js +328 -0
  420. package/dist/tool-system/builtin/web-search.d.ts +25 -0
  421. package/dist/tool-system/builtin/web-search.js +174 -0
  422. package/dist/tool-system/builtin/worktree.d.ts +10 -0
  423. package/dist/tool-system/builtin/worktree.js +95 -0
  424. package/dist/tool-system/builtin/write.d.ts +6 -0
  425. package/dist/tool-system/builtin/write.js +36 -0
  426. package/dist/tool-system/context.d.ts +165 -0
  427. package/dist/tool-system/context.js +18 -0
  428. package/dist/tool-system/executor.d.ts +49 -0
  429. package/dist/tool-system/executor.js +495 -0
  430. package/dist/tool-system/investigation-guard.d.ts +46 -0
  431. package/dist/tool-system/investigation-guard.js +155 -0
  432. package/dist/tool-system/mcp-manager.d.ts +67 -0
  433. package/dist/tool-system/mcp-manager.js +249 -0
  434. package/dist/tool-system/permission.d.ts +111 -0
  435. package/dist/tool-system/permission.js +774 -0
  436. package/dist/tool-system/registry.d.ts +32 -0
  437. package/dist/tool-system/registry.js +131 -0
  438. package/dist/tool-system/sandbox/bwrap.d.ts +13 -0
  439. package/dist/tool-system/sandbox/bwrap.js +63 -0
  440. package/dist/tool-system/sandbox/index.d.ts +78 -0
  441. package/dist/tool-system/sandbox/index.js +222 -0
  442. package/dist/tool-system/sandbox/off.d.ts +2 -0
  443. package/dist/tool-system/sandbox/off.js +8 -0
  444. package/dist/tool-system/sandbox/seatbelt.d.ts +18 -0
  445. package/dist/tool-system/sandbox/seatbelt.js +109 -0
  446. package/dist/tool-system/task-guard.d.ts +29 -0
  447. package/dist/tool-system/task-guard.js +70 -0
  448. package/dist/tool-system/validation.d.ts +5 -0
  449. package/dist/tool-system/validation.js +39 -0
  450. package/dist/types.d.ts +376 -0
  451. package/dist/types.js +4 -0
  452. package/dist/updater.d.ts +65 -0
  453. package/dist/updater.js +405 -0
  454. package/dist/utils/debug.d.ts +41 -0
  455. package/dist/utils/debug.js +110 -0
  456. package/dist/utils/earlyInput.d.ts +43 -0
  457. package/dist/utils/earlyInput.js +166 -0
  458. package/dist/utils/env.d.ts +24 -0
  459. package/dist/utils/env.js +104 -0
  460. package/dist/utils/envUtils.d.ts +51 -0
  461. package/dist/utils/envUtils.js +135 -0
  462. package/dist/utils/execFileNoThrow.d.ts +39 -0
  463. package/dist/utils/execFileNoThrow.js +74 -0
  464. package/dist/utils/format.d.ts +44 -0
  465. package/dist/utils/format.js +236 -0
  466. package/dist/utils/intl.d.ts +22 -0
  467. package/dist/utils/intl.js +83 -0
  468. package/dist/utils/lockfile.d.ts +15 -0
  469. package/dist/utils/lockfile.js +30 -0
  470. package/dist/utils/memoize.d.ts +19 -0
  471. package/dist/utils/memoize.js +14 -0
  472. package/dist/utils/semver.d.ts +6 -0
  473. package/dist/utils/semver.js +13 -0
  474. package/dist/utils/sliceAnsi.d.ts +12 -0
  475. package/dist/utils/sliceAnsi.js +62 -0
  476. package/dist/utils/systemTheme.d.ts +40 -0
  477. package/dist/utils/systemTheme.js +108 -0
  478. package/dist/utils/task-sanitizer.d.ts +6 -0
  479. package/dist/utils/task-sanitizer.js +24 -0
  480. package/dist/utils/theme.d.ts +93 -0
  481. package/dist/utils/theme.js +544 -0
  482. package/dist/utils/toolDisplay.d.ts +31 -0
  483. package/dist/utils/toolDisplay.js +188 -0
  484. package/package.json +72 -0
@@ -0,0 +1,1467 @@
1
+ /**
2
+ * Engine — the main facade that wires all components together.
3
+ */
4
+ import { createLLMClient } from "../llm/client-factory.js";
5
+ import { ToolRegistry } from "../tool-system/registry.js";
6
+ import { ToolExecutor } from "../tool-system/executor.js";
7
+ import { InvestigationGuard } from "../tool-system/investigation-guard.js";
8
+ import { TaskGuard } from "../tool-system/task-guard.js";
9
+ import { readLastTodoSnapshot } from "../tool-system/builtin/task.js";
10
+ import { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, InteractiveApprovalBackend, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
11
+ import { HookRegistry } from "../hooks/registry.js";
12
+ import { wrapHookMessages } from "../hooks/inject.js";
13
+ import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
14
+ import { patchOrphanedToolUses } from "./patch-orphaned-tools.js";
15
+ import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
16
+ import { ContextManager } from "../context/manager.js";
17
+ import { PromptComposer } from "../prompt/composer.js";
18
+ import { SessionManager } from "../session/session-manager.js";
19
+ import { ModelFacade } from "./model-facade.js";
20
+ import { logger, setCurrentSid, runWithSid } from "../logging/logger.js";
21
+ import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
22
+ import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
23
+ import { TurnLoop } from "./turn-loop.js";
24
+ import { MCPManager } from "../tool-system/mcp-manager.js";
25
+ import { SettingsManager } from "../settings/manager.js";
26
+ import { FileHistory } from "../session/file-history.js";
27
+ import { defaultSandboxConfig, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
28
+ import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
29
+ import { ModelPool } from "../llm/model-pool.js";
30
+ import { ProviderCatalog } from "../llm/provider-catalog.js";
31
+ import { defaultCacheDir } from "../llm/model-cache.js";
32
+ import { detectProviderFromApiKey, buildModelPool, } from "../onboarding.js";
33
+ import { detectPastedNoise } from "../utils/task-sanitizer.js";
34
+ import { parseTaskWithImages, } from "./parse-task.js";
35
+ import { capabilitiesFor } from "../llm/capabilities/index.js";
36
+ import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
37
+ import { join } from "node:path";
38
+ import { homedir } from "node:os";
39
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
40
+ export class Engine {
41
+ config;
42
+ preset;
43
+ toolRegistry;
44
+ hooks;
45
+ sessionManager;
46
+ mcpManager;
47
+ modelPool;
48
+ /** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
49
+ runtime;
50
+ /** Active permission mode for this Engine instance. */
51
+ permissionMode;
52
+ /** True when permissionMode === "plan". */
53
+ planMode;
54
+ // Lazy SettingsManager — reused across updateConfig/readSetting so we
55
+ // don't re-read 6+ JSON files on every /model, /login, etc. The manager
56
+ // handles its own cache invalidation in saveUserSetting().
57
+ settingsManager;
58
+ // Live state from the current/most-recent run, retained for /compact and
59
+ // for live-mutating PermissionClassifier on permission-mode switch.
60
+ lastContextManager;
61
+ lastMessages;
62
+ lastSessionId;
63
+ compactedMessagesBySession = new Map();
64
+ /**
65
+ * SIDs whose ctx-bar seed we've already emitted in this process. The seed
66
+ * is a rough char/4 estimate; only useful before the first real
67
+ * usage_update arrives (cold start or cross-process resume). On subsequent
68
+ * turns the UI already shows the previous turn's accurate ctx — re-seeding
69
+ * would visibly drop the bar on every submit.
70
+ */
71
+ ctxSeedSent = new Set();
72
+ /**
73
+ * Per-sid cache of "non-messages overhead" (system prompt + tool defs, in
74
+ * tokens). Survives across turns so each fresh TurnLoop instance can seed
75
+ * its first pre-llm emit with the right offset — without this the ctx bar
76
+ * visibly drops on every user submit (e.g. from ~20k → 3k) until the next
77
+ * LLM response arrives.
78
+ */
79
+ ctxOverheadBySid = new Map();
80
+ activePermission;
81
+ /** Public accessor so UI/clients can read the resolved per-model window. */
82
+ get maxContextTokens() {
83
+ return this.resolveMaxContextTokens();
84
+ }
85
+ resolveMaxContextTokens() {
86
+ const modelEntry = this.modelPool.get();
87
+ return modelEntry?.maxContextTokens ?? this.config.maxContextTokens ?? 200_000;
88
+ }
89
+ /**
90
+ * Emit a lifecycle hook with isSubAgent auto-merged into data so handlers
91
+ * can skip noisy injections for spawned children. All Engine-side hook
92
+ * emits should go through this wrapper to keep the context envelope
93
+ * uniform with TurnLoop.emitHook.
94
+ */
95
+ async emitHook(event, data = {}) {
96
+ return this.hooks.emit(event, {
97
+ ...data,
98
+ isSubAgent: this.config.isSubAgent === true,
99
+ });
100
+ }
101
+ /**
102
+ * Read settings.hooks and register a shell-runner wrapper handler per
103
+ * entry. Sub-agents skip shell hooks entirely — spawning child processes
104
+ * per emit for every sub-agent run would multiply token-side overhead
105
+ * for marginal value; explicit users who want sub-agent observability
106
+ * should register SDK-side handlers.
107
+ */
108
+ registerSettingsHooks() {
109
+ if (this.config.isSubAgent === true)
110
+ return;
111
+ let settings;
112
+ try {
113
+ settings = this.getSettingsManager().get();
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ const entries = settings.hooks ?? [];
119
+ for (const entry of entries) {
120
+ this.hooks.register(entry.event, async (ctx) => {
121
+ if (!shellHookMatches(entry, ctx))
122
+ return {};
123
+ return runShellHook(entry, ctx);
124
+ }, 50, `shell:${entry.event}:${entry.command.slice(0, 32)}`);
125
+ }
126
+ }
127
+ constructor(config) {
128
+ this.config = config;
129
+ // Wire shared runtime (adapter pattern — null when self-constructing).
130
+ this.runtime = config.runtime ?? null;
131
+ // Instance-level permission/plan mode fields.
132
+ this.permissionMode = config.permissionMode ?? "acceptEdits";
133
+ this.planMode = this.permissionMode === "plan";
134
+ this.preset = resolveAgentPreset(config.preset);
135
+ this.toolRegistry = config.runtime?.toolRegistry ?? new ToolRegistry({
136
+ builtinTools: resolveBuiltinToolNames({
137
+ preset: this.preset.name,
138
+ enabledBuiltinTools: config.enabledBuiltinTools,
139
+ disabledBuiltinTools: config.disabledBuiltinTools,
140
+ }),
141
+ });
142
+ this.hooks = new HookRegistry();
143
+ // Installed-plugin hooks — declared in each plugin's hooks/hooks.json.
144
+ // Registered first (priority 80) so user-authored hooks at lower
145
+ // priorities (settings: 50, SDK config: default 0) can post-process
146
+ // or stop a plugin's contribution. Sub-agents skip plugin hooks for
147
+ // the same reason they skip settings hooks: per-emit child-process
148
+ // overhead multiplied across sub-agents outweighs the value, and
149
+ // dispatched tasks should run with minimal surface area.
150
+ if (config.isSubAgent !== true) {
151
+ loadPluginHooks(this.hooks);
152
+ }
153
+ // settings.hooks → shell-command wrappers. Chain order:
154
+ // plugin (80) → shell (50) → code (default 0).
155
+ this.registerSettingsHooks();
156
+ for (const hook of config.hooks ?? []) {
157
+ this.hooks.register(hook.event, hook.handler, hook.priority, hook.name);
158
+ }
159
+ this.sessionManager = new SessionManager(config.sessionStorageDir);
160
+ // Initialize model pool — prefer runtime's shared pool, fall back to self-constructed.
161
+ this.modelPool = config.runtime?.modelPool ?? new ModelPool();
162
+ if (!config.runtime) {
163
+ this.populateModelPoolFromSettings();
164
+ }
165
+ }
166
+ /**
167
+ * Load models[] / providers[] from settings into the active ModelPool and
168
+ * resync this.config.llm with the matching entry. Called from the ctor and
169
+ * from reloadModelPool() (e.g. after onboarding writes new entries to disk).
170
+ */
171
+ populateModelPoolFromSettings() {
172
+ try {
173
+ const sm = this.getSettingsManager();
174
+ sm.invalidate();
175
+ const settings = sm.get();
176
+ if (settings.models?.length) {
177
+ for (const m of settings.models) {
178
+ this.modelPool.register({
179
+ key: m.key,
180
+ label: m.label,
181
+ provider: m.provider ?? "",
182
+ model: m.model,
183
+ baseUrl: m.baseUrl,
184
+ apiKey: m.apiKey,
185
+ maxOutputTokens: m.maxOutputTokens,
186
+ maxContextTokens: m.maxContextTokens,
187
+ providerKey: m.providerKey,
188
+ });
189
+ }
190
+ // Build catalog from settings.providers[] and attach to the pool
191
+ // so model entries can resolve baseUrl/apiKey from their provider.
192
+ if (settings.providers?.length) {
193
+ this.modelPool.setProviderCatalog(new ProviderCatalog(settings.providers));
194
+ }
195
+ this.modelPool.setCacheDir(defaultCacheDir());
196
+ this.modelPool.reloadCachedContextWindows();
197
+ // Resolve active entry. Priority:
198
+ // 1. settings.activeKey — primary source of truth (new shape).
199
+ // 2. Match settings.model.name against models[].model — legacy
200
+ // pre-activeKey configs and the migration path.
201
+ // We then switch the pool and write the resolved entry's credentials
202
+ // into config.llm, so the first run() uses the right endpoint instead
203
+ // of whatever env-derived fallback repl.ts seeded earlier.
204
+ const activeKey = settings.activeKey;
205
+ let match;
206
+ if (activeKey) {
207
+ match = settings.models.find((m) => m.key === activeKey);
208
+ }
209
+ if (!match) {
210
+ const currentModel = this.config.llm.model;
211
+ // OpenRouter stores entries as "provider/model-name"; the top-level
212
+ // settings.model.name is just "model-name". Match either form.
213
+ match = settings.models.find((m) => m.model === currentModel ||
214
+ (currentModel && m.model?.endsWith(`/${currentModel}`)));
215
+ }
216
+ if (match) {
217
+ const entry = this.modelPool.switch(match.key);
218
+ this.config = {
219
+ ...this.config,
220
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm),
221
+ };
222
+ }
223
+ }
224
+ else if (this.config.llm.apiKey) {
225
+ // Auto-populate pool from the configured API key when models[] is empty.
226
+ // This lets users who only set model.apiKey (without models[]) still
227
+ // use /model to switch between the provider's available models.
228
+ this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
229
+ }
230
+ }
231
+ catch {
232
+ // Settings not available — pool stays empty
233
+ }
234
+ }
235
+ /**
236
+ * Re-read settings and refresh the model pool. Used after onboarding /login
237
+ * writes new providers[] / models[] to disk so the running engine picks them
238
+ * up without a process restart. Existing pool entries are kept (re-registering
239
+ * the same key overwrites them), so callers don't need to clear first.
240
+ */
241
+ reloadModelPool() {
242
+ // When sharing a runtime, the owner of the runtime is responsible
243
+ // for populating the pool; reloading from settings here would
244
+ // blast that owner's contributions and affect every other Engine
245
+ // that shares this runtime.
246
+ if (!this.runtime) {
247
+ this.populateModelPoolFromSettings();
248
+ }
249
+ }
250
+ /**
251
+ * Auto-populate the model pool when settings.models[] is empty but
252
+ * the user has configured an API key. Detects the provider from the
253
+ * key prefix / baseUrl and registers all its known models.
254
+ */
255
+ autoPopulatePool(apiKey, baseUrl) {
256
+ const provider = detectProviderFromApiKey(apiKey, baseUrl);
257
+ if (!provider)
258
+ return;
259
+ const entries = buildModelPool(provider, apiKey);
260
+ for (const e of entries) {
261
+ this.modelPool.register(e);
262
+ }
263
+ // Activate the first registered model as the zero-config default.
264
+ // Users override via the onboarding wizard or `/model`.
265
+ const defaultEntry = entries[0];
266
+ if (defaultEntry) {
267
+ const entry = this.modelPool.switch(defaultEntry.key);
268
+ this.config = {
269
+ ...this.config,
270
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm),
271
+ };
272
+ }
273
+ }
274
+ /**
275
+ * Register a custom tool (from product adapter) into the tool registry.
276
+ * Must be called before run().
277
+ */
278
+ registerCustomTool(definition, executor) {
279
+ this.toolRegistry.registerTool(definition, executor);
280
+ }
281
+ /**
282
+ * Inject the askUser handler after construction. Used by AgentServer
283
+ * to wire its protocol-backed askUser into an Engine that was created
284
+ * before the server existed (chicken-and-egg: server takes engine in ctor).
285
+ */
286
+ setAskUser(fn) {
287
+ this.config.askUser = fn;
288
+ }
289
+ /**
290
+ * Run a task from start to finish.
291
+ */
292
+ async run(task, options) {
293
+ const cwd = options?.cwd ?? this.config.cwd ?? process.cwd();
294
+ // Wrap the caller's onStream so we can intercept `task_update`
295
+ // events emitted by TodoWrite and keep an in-engine snapshot.
296
+ // TaskGuard reads this snapshot at turn end to decide whether to
297
+ // nag about stale in_progress items. Without the wrapper we'd
298
+ // have no way to observe TodoWrite's emission — the canonical
299
+ // store is the transcript, but TaskGuard runs in-loop and can't
300
+ // afford a transcript scan per turn.
301
+ let latestTodos = [];
302
+ const userOnStream = options?.onStream;
303
+ const wrappedOnStream = (event) => {
304
+ if (event.type === "task_update") {
305
+ latestTodos = event.tasks;
306
+ }
307
+ userOnStream?.(event);
308
+ };
309
+ if (options)
310
+ options.onStream = wrappedOnStream;
311
+ // ── P2-6: image input ─────────────────────────────────────────────
312
+ // Parse `<codeshell-image>` blocks out of the raw task string before
313
+ // any other gate looks at it. Two concerns:
314
+ // 1. The noise detector below sees the raw base64 as gibberish and
315
+ // would reject the whole turn — split images out first so it
316
+ // only inspects the prose portion.
317
+ // 2. Models that don't accept vision must be refused immediately,
318
+ // with the image bytes intact for the user to retry on another
319
+ // model. Silent text-only fallback was the failure mode this
320
+ // gate is here to prevent.
321
+ let parsedTask;
322
+ try {
323
+ parsedTask = parseTaskWithImages(task);
324
+ }
325
+ catch (err) {
326
+ const msg = err.message;
327
+ logger.warn("engine.run.image_parse_failed", { error: msg });
328
+ return {
329
+ text: `ERROR: image attachment is malformed (${msg}). Drop the image and try again, or re-attach it.`,
330
+ reason: "image_error",
331
+ sessionId: options?.sessionId ?? "image-parse-failed",
332
+ turnCount: 0,
333
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
334
+ };
335
+ }
336
+ if (parsedTask.hasImages) {
337
+ const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
338
+ if (!cap.supportsVision) {
339
+ logger.warn("engine.run.vision_not_supported", {
340
+ provider: this.config.llm.provider,
341
+ model: this.config.llm.model,
342
+ imageCount: parsedTask.images.length,
343
+ });
344
+ return {
345
+ text: `ERROR: model "${this.config.llm.model}" does not accept image input. ` +
346
+ `Switch to a vision-capable model (e.g. gpt-4o, claude-sonnet, gemini-1.5-pro) and resend.`,
347
+ reason: "image_error",
348
+ sessionId: options?.sessionId ?? "vision-not-supported",
349
+ turnCount: 0,
350
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
351
+ };
352
+ }
353
+ }
354
+ // For downstream noise-detection + transcript persistence we want the
355
+ // *text* portion only — base64 bytes count as "noise" by the heuristic
356
+ // and would also bloat the transcript by megabytes per image. Image
357
+ // bytes ride in parsedTask.images and re-enter the message tree below.
358
+ const taskText = parsedTask.hasImages ? parsedTask.text : task;
359
+ const noise = detectPastedNoise(taskText);
360
+ if (noise.isNoise) {
361
+ const hint = `Your input looks like pasted terminal output (${noise.reason}). ` +
362
+ `I didn't start a task. What would you like to ask?` +
363
+ (noise.cleaned ? `\n\nExtracted text: ${noise.cleaned.slice(0, 200)}` : "");
364
+ logger.info("engine.run.rejected", { reason: noise.reason, len: task.length });
365
+ return {
366
+ text: hint,
367
+ reason: "completed",
368
+ sessionId: options?.sessionId ?? "noise-rejected",
369
+ turnCount: 0,
370
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
371
+ };
372
+ }
373
+ // Build the per-Engine ToolContext that will be threaded through every
374
+ // tool call. Replaces the old module-level singletons (setAskUserFn,
375
+ // setArenaLLMConfig, setSubAgentConfig, setToolSearchRegistry).
376
+ const subAgentSpawner = {
377
+ parentStream: options?.onStream,
378
+ describe: () => ({
379
+ cwd,
380
+ preset: this.preset.name,
381
+ permissionMode: this.config.permissionMode ?? "acceptEdits",
382
+ }),
383
+ spawn: async (req) => {
384
+ // No nested agents. Strip Agent / AgentStatus / AgentCancel from the
385
+ // child's tool pool so the LLM can't spawn grandchildren — matches
386
+ // Claude Code's ALL_AGENT_DISALLOWED_TOOLS approach. Without this
387
+ // guard a runaway model could fork-bomb sub-agents (token cost +
388
+ // background process explosion), and the sid / approval / dock
389
+ // model assumes a flat parent→children hierarchy. Layered with a
390
+ // runtime check in agent.ts as defense-in-depth.
391
+ const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel"];
392
+ const childDisabled = Array.from(new Set([
393
+ ...(this.config.disabledBuiltinTools ?? []),
394
+ ...NESTED_AGENT_TOOLS,
395
+ ]));
396
+ // If enabledBuiltinTools is set (explicit allow-list mode), strip
397
+ // the nested-agent tools from it too so the disable above isn't
398
+ // contradicted by an explicit allow.
399
+ const childEnabled = this.config.enabledBuiltinTools?.filter((t) => !NESTED_AGENT_TOOLS.includes(t));
400
+ const child = new Engine({
401
+ llm: { ...this.config.llm, retryMaxAttempts: 2 },
402
+ cwd,
403
+ permissionMode: this.config.permissionMode,
404
+ preset: this.preset.name,
405
+ enabledBuiltinTools: childEnabled,
406
+ disabledBuiltinTools: childDisabled,
407
+ customSystemPrompt: this.config.customSystemPrompt,
408
+ appendSystemPrompt: this.config.appendSystemPrompt,
409
+ maxTurns: req.maxTurns,
410
+ maxContextTokens: this.config.maxContextTokens ?? 200_000,
411
+ sessionStorageDir: this.config.sessionStorageDir,
412
+ headless: this.config.headless,
413
+ sandbox: this.config.sandbox,
414
+ isSubAgent: true,
415
+ });
416
+ // Where the spawned child Engine's stream events go. AgentTool's
417
+ // background path passes a `streamOverride` (transcriptSink) so the
418
+ // per-event detail is captured into the agent's transcript instead
419
+ // of flooding the main feed. Sync calls leave streamOverride unset
420
+ // and we fall back to the parent UI's onStream so synchronous
421
+ // sub-agents still render inline.
422
+ const destStream = req.streamOverride ?? options?.onStream;
423
+ const childStream = destStream
424
+ ? (event) => {
425
+ // Filter ctx-bar signals: the bar tracks the main conversation's
426
+ // prompt size, and a sub-agent's own session emits would clobber
427
+ // it (its sid is new, its messages are tiny, the rough char/4
428
+ // seed lands the bar at <1% mid-turn). Sub-agent token accounting
429
+ // lives in CostTracker (recordUsage), not the ctx bar.
430
+ //
431
+ // - session_started: would seed main ctx with sub-agent's prompt
432
+ // - usage_update: would overwrite main ctx with sub-agent's prompt
433
+ // - context_compact: would reset main ctx to sub-agent's post-compact
434
+ // value AND print a misleading "context compacted" boundary in
435
+ // the main chat (the main session didn't compact).
436
+ if (event.type === "usage_update" ||
437
+ event.type === "session_started" ||
438
+ event.type === "context_compact") {
439
+ return;
440
+ }
441
+ destStream({ ...event, agentId: req.agentId });
442
+ }
443
+ : undefined;
444
+ // child.run() establishes its own runWithSid scope internally, so
445
+ // child log lines route to the child's sid and parent's ALS
446
+ // binding is unaffected when control returns here.
447
+ const result = await child.run(req.prompt, { signal: req.signal, onStream: childStream });
448
+ return result.text;
449
+ },
450
+ };
451
+ const sandboxConfig = this.config.sandbox ??
452
+ defaultSandboxConfig(this.config.headless ? "auto" : "off");
453
+ // A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
454
+ // per standard §S4. resolveSandboxBackend throws when an explicit
455
+ // mode is unavailable on this host; we let it propagate. The
456
+ // previous behavior — catching the throw inside the hot turn and
457
+ // silently downgrading to "off" — was the leak A2 closes. The
458
+ // `auto` mode handles its own downgrade with a one-time warning
459
+ // inside resolveSandboxBackend; explicit modes do not.
460
+ //
461
+ // Backend is cached on EngineRuntime (when available) so the
462
+ // capability probe runs once per (mode, cwd) instead of every turn.
463
+ const sandboxBackend = this.runtime
464
+ ? await this.runtime.resolveSandbox(sandboxConfig, cwd)
465
+ : await resolveSandboxBackend(sandboxConfig, cwd);
466
+ // sessionId is filled in after the session bundle is resolved below
467
+ // (the session may be cold-started or resumed). Until then this is
468
+ // intentionally shaped as a mutable local; we treat it as immutable
469
+ // after the assignment.
470
+ const toolCtx = {
471
+ ...this.buildToolContext(),
472
+ subAgentSpawner,
473
+ sandbox: sandboxBackend,
474
+ cwd,
475
+ // TodoWrite reads this to push task_update events independently
476
+ // of its return value, so the UI's pinned task panel refreshes
477
+ // immediately rather than after the LLM next surfaces the
478
+ // snapshot. wrappedOnStream snoops the same channel to keep
479
+ // latestTodos current for TaskGuard.
480
+ streamCallback: options?.onStream,
481
+ };
482
+ logger.info("engine.run", {
483
+ task: taskText.slice(0, 200),
484
+ cwd,
485
+ model: this.config.llm.model,
486
+ preset: this.preset.name,
487
+ imageCount: parsedTask.images.length,
488
+ });
489
+ // Compose the user-turn payload once so resume + cold paths agree on
490
+ // shape. With images, content becomes a ContentBlock[] holding one
491
+ // text block (when prose is present) followed by one image block per
492
+ // attachment — the provider-specific clients translate this to OpenAI
493
+ // `image_url` or Anthropic `{type:image, source:base64}` downstream.
494
+ const userMessageContent = parsedTask.hasImages
495
+ ? [
496
+ ...(parsedTask.text
497
+ ? [{ type: "text", text: parsedTask.text }]
498
+ : []),
499
+ ...parsedTask.images.map((img) => ({
500
+ type: "image",
501
+ source: {
502
+ type: "base64",
503
+ media_type: img.mime,
504
+ data: img.base64,
505
+ },
506
+ })),
507
+ ]
508
+ : taskText;
509
+ // Create or resume session.
510
+ //
511
+ // Three valid shapes:
512
+ // 1. options.sessionId names an EXISTING on-disk session → resume
513
+ // 2. options.sessionId names a fresh sid the host wants materialized
514
+ // (ChatSessionManager's "tui-main" first-turn case) → create
515
+ // with that explicit sid so subsequent turns can resume cleanly
516
+ // 3. no sessionId → create with nanoid
517
+ //
518
+ // Shape (2) was previously broken — engine threw SessionError,
519
+ // surfacing as `[-32603] Session not found: <sid>` on the very first
520
+ // TUI turn. Detection now uses `sessionManager.exists()` (one stat
521
+ // call) instead of a try/catch on resume.
522
+ let session;
523
+ let messages;
524
+ if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
525
+ session = this.sessionManager.resume(options.sessionId);
526
+ messages = this.compactedMessagesBySession.get(options.sessionId)
527
+ ? [...this.compactedMessagesBySession.get(options.sessionId)]
528
+ : session.transcript.toMessages();
529
+ // If the previous run was Ctrl+C'd or crashed between an assistant
530
+ // tool_use and the matching tool_result being persisted, the
531
+ // loaded sequence is invalid for OpenAI (which 400s on dangling
532
+ // tool_calls). Patch synthetic tool_results so the next API call
533
+ // doesn't fail before the turn even starts.
534
+ const patched = patchOrphanedToolUses(messages);
535
+ if (patched.gapsPatched > 0) {
536
+ logger.warn("engine.resume.patched_orphaned_tool_uses", {
537
+ sessionId: options.sessionId,
538
+ gaps: patched.gapsPatched,
539
+ toolResults: patched.toolResultsInjected,
540
+ });
541
+ }
542
+ // Restore cost state from previous session, if the caller injected a store
543
+ if (session.state.costState && this.config.costStore) {
544
+ this.config.costStore.restore(session.state.costState);
545
+ }
546
+ // Append new user message
547
+ const userMsg = { role: "user", content: userMessageContent };
548
+ messages.push(userMsg);
549
+ session.transcript.appendMessage("user", userMessageContent);
550
+ // Flush "active" status to disk immediately. resume() set it in memory
551
+ // (session-manager.ts), but without this write the on-disk state.json
552
+ // still shows the previous run's terminal reason — so any external
553
+ // observer (another CLI process, /sid, the session list) would think
554
+ // the session is still errored/aborted while we're actually running.
555
+ this.sessionManager.saveState(session.state);
556
+ }
557
+ else {
558
+ // Cold start: shape (2) reuses the host-supplied sid; shape (3)
559
+ // lets sessionManager generate one with nanoid.
560
+ session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId);
561
+ messages = [{ role: "user", content: userMessageContent }];
562
+ session.transcript.appendMessage("user", userMessageContent);
563
+ // Save first user message as session summary — text only. The summary
564
+ // shows up in the session list; "[image]" is more informative than a
565
+ // truncated `[object Object]` when the prompt was purely visual.
566
+ const summarySrc = parsedTask.hasImages
567
+ ? parsedTask.text || `[image${parsedTask.images.length > 1 ? `s × ${parsedTask.images.length}` : ""}]`
568
+ : taskText;
569
+ session.state.summary = summarySrc.slice(0, 80).replace(/\n/g, " ");
570
+ this.sessionManager.saveState(session.state);
571
+ }
572
+ // Stamp the resolved session id for downstream logging.
573
+ //
574
+ // `setCurrentSid` updates the module-level fallback so any code path
575
+ // running outside an ALS scope (bootstrap, /sid before a run starts)
576
+ // still sees the latest sid.
577
+ //
578
+ // The real isolation comes from wrapping the rest of `run` in
579
+ // `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
580
+ // call inside that closure — including those inside `await`ed child
581
+ // Engine.run() calls — reads sid from this scope's
582
+ // AsyncLocalStorage binding, not the module global. Sibling Engines
583
+ // running concurrently each get their own scope; an `enterSid` inside
584
+ // a child mutates that child's scope only and doesn't leak back to
585
+ // the parent's chain after `await child.run(...)` returns.
586
+ setCurrentSid(session.state.sessionId);
587
+ // B2 / Gate 1: stamp the resolved sid onto the tool context so
588
+ // session-scoped side effects (background-agent completion
589
+ // notifications) attribute to the right session. toolCtx is created
590
+ // before the session bundle is resolved (see ~line 635), so this is
591
+ // the first point we can set it. After this assignment treat the
592
+ // field as immutable for the rest of the run.
593
+ toolCtx.sessionId = session.state.sessionId;
594
+ return runWithSid(session.state.sessionId, async () => {
595
+ recordSessionStart(session.state.sessionId, {
596
+ // Strip <codeshell-image> base64 payloads before they reach
597
+ // <repo>/log/. Reader still sees the marker + byte count, just
598
+ // not the bytes. Transcript persistence keeps the full payload.
599
+ task: sanitizeTaskString(task),
600
+ cwd,
601
+ model: this.config.llm.model,
602
+ provider: this.config.llm.provider,
603
+ permissionMode: this.config.permissionMode ?? "acceptEdits",
604
+ resumed: !!options?.sessionId,
605
+ });
606
+ // Session-level hook: fired once per Engine.run() entry, regardless of
607
+ // cold-start vs resume. Handlers can return `messages` to inject a
608
+ // <system-reminder> at the head of the conversation (between
609
+ // userContext and the new user prompt). Used by the built-in
610
+ // superpowers injector to surface the `using-superpowers` ruleset.
611
+ const sessionStartHook = await this.emitHook("on_session_start", {
612
+ sessionId: session.state.sessionId,
613
+ cwd,
614
+ resumed: !!options?.sessionId,
615
+ });
616
+ // Per-turn hook: fired every time a new user prompt enters the loop.
617
+ // Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
618
+ // reminders that should accompany each user turn (e.g. "skills
619
+ // available — check before acting").
620
+ const promptSubmitHook = await this.emitHook("user_prompt_submit", {
621
+ sessionId: session.state.sessionId,
622
+ // Pass the text-only portion. Handlers reading the prompt for keyword
623
+ // detection / classification (e.g. superpowers' "did the user ask
624
+ // about X?") don't gain anything from megabytes of base64 inlined here,
625
+ // and silently leaking attachment bytes through hooks is the kind of
626
+ // exfiltration risk a curious user-installed shell hook shouldn't carry.
627
+ prompt: taskText,
628
+ resumed: !!options?.sessionId,
629
+ });
630
+ // updatedPrompt: handler rewrote the user's prompt text. Replace the
631
+ // last user message we just pushed (cold-start: line ~511; resume:
632
+ // line ~500). Original prompt is in the transcript already — we log
633
+ // the rewrite so audit chains know a hook touched user input.
634
+ if (typeof promptSubmitHook.updatedPrompt === "string") {
635
+ const lastIdx = messages.length - 1;
636
+ const last = messages[lastIdx];
637
+ if (last && last.role === "user" && typeof last.content === "string") {
638
+ logger.info("hook.updated_prompt", {
639
+ sessionId: session.state.sessionId,
640
+ originalChars: last.content.length,
641
+ updatedChars: promptSubmitHook.updatedPrompt.length,
642
+ });
643
+ messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
644
+ }
645
+ }
646
+ // Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
647
+ // before the first real usage_update arrives. The authoritative count
648
+ // comes from `usage.promptTokens` after the first LLM response — this is
649
+ // just a display-friendly approximation for the first frame.
650
+ //
651
+ // Only seed once per (process, sid). On subsequent turns the UI already
652
+ // shows the previous turn's accurate ctx; overwriting it with this rough
653
+ // char/4 estimate would make the bar visibly drop on every submit.
654
+ const sid = session.state.sessionId;
655
+ const needsCtxSeed = !this.ctxSeedSent.has(sid);
656
+ const roughPromptTokens = needsCtxSeed
657
+ ? messages.reduce((sum, m) => {
658
+ const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
659
+ return sum + Math.ceil(text.length / 4);
660
+ }, 0)
661
+ : 0;
662
+ if (needsCtxSeed)
663
+ this.ctxSeedSent.add(sid);
664
+ // Tell the client the sid *now* instead of waiting for run() to resolve.
665
+ // The user wants `/sid` to work mid-turn; without this, the client only
666
+ // learns the sid when the run completes.
667
+ options?.onStream?.({
668
+ type: "session_started",
669
+ sessionId: sid,
670
+ promptTokens: roughPromptTokens,
671
+ });
672
+ // Replay the last TodoWrite snapshot on resume so the UI's pinned
673
+ // task panel re-hydrates without the LLM needing to call TodoWrite
674
+ // again. Scans the resumed transcript newest-first (and tolerates
675
+ // legacy TaskCreate/Update events for sessions recorded against
676
+ // the pre-2026-05-24 API). New sessions have no transcript yet so
677
+ // readLastTodoSnapshot returns null and nothing is emitted.
678
+ if (options?.sessionId) {
679
+ const snap = readLastTodoSnapshot(session.transcript.getEvents());
680
+ if (snap && snap.length > 0) {
681
+ latestTodos = snap;
682
+ options?.onStream?.({ type: "task_update", tasks: snap });
683
+ }
684
+ }
685
+ // Kick off LLM client creation early (network handshake)
686
+ const llmClientPromise = createLLMClient(this.config.llm);
687
+ const mode = this.config.permissionMode ?? "acceptEdits";
688
+ const { rules: defaultRules, backend: approvalBackend } = this.buildPermissionConfig(mode, cwd);
689
+ const permission = new PermissionClassifier(defaultRules, mode, approvalBackend);
690
+ this.activePermission = permission;
691
+ // If the backend is the interactive one, wire it for project-scope
692
+ // persistence: it needs cwd to find settings.local.json, and a callback
693
+ // to apply newly-saved rules to the live classifier so subsequent calls
694
+ // in this same session don't re-prompt. Headless/auto backends skip
695
+ // this — they don't prompt, so there are no project rules to persist.
696
+ if (approvalBackend instanceof InteractiveApprovalBackend) {
697
+ approvalBackend.setCwd(cwd);
698
+ approvalBackend.setOnProjectRules((rules) => {
699
+ // Prepend the *full* accumulated list of session-saved project rules
700
+ // so user approvals win over defaults and earlier approvals aren't
701
+ // dropped when later ones come in.
702
+ permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
703
+ });
704
+ }
705
+ const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
706
+ const investigationGuard = new InvestigationGuard();
707
+ if (this.config.headless)
708
+ investigationGuard.setSoftMode(true);
709
+ toolExecutor.setInvestigationGuard(investigationGuard);
710
+ toolExecutor.setTaskGuard(new TaskGuard(() => latestTodos));
711
+ // Wire abort signal for cascading cancellation + per-Engine ToolContext
712
+ toolExecutor.setSignal(options?.signal);
713
+ toolExecutor.setContext(toolCtx);
714
+ const contextManager = new ContextManager({
715
+ maxTokens: this.resolveMaxContextTokens(),
716
+ });
717
+ this.lastContextManager = contextManager;
718
+ const promptComposer = new PromptComposer({
719
+ cwd,
720
+ model: this.config.llm.model,
721
+ preset: this.preset,
722
+ customSystemPrompt: this.config.customSystemPrompt,
723
+ appendSystemPrompt: this.config.appendSystemPrompt,
724
+ disabledSkills: this.readDisabledSkills(),
725
+ });
726
+ // Connect MCP servers (if configured and not already connected).
727
+ // B1: prefer the Runtime-owned MCPManager so all sessions in a
728
+ // worker share one set of connections. Falling back to a
729
+ // per-Engine instance keeps the null-runtime path (tests, ad-hoc
730
+ // scripts) working.
731
+ const mcpServers = this.config.mcpServers ?? {};
732
+ if (Object.keys(mcpServers).length > 0 && !this.mcpManager) {
733
+ if (this.runtime) {
734
+ this.mcpManager = this.runtime.mcpPool;
735
+ }
736
+ else {
737
+ this.mcpManager = new MCPManager(this.toolRegistry);
738
+ }
739
+ await this.mcpManager.connectAll(mcpServers);
740
+ }
741
+ // Parallelize slow initialization:
742
+ // 1. createLLMClient — network handshake (started earlier)
743
+ // 2. buildSystemPrompt — includes git status (3 execSync calls)
744
+ // 3. buildSystemContext — reads environment context
745
+ const allToolDefs = this.toolRegistry.getToolDefinitions();
746
+ // In plan mode, only expose read-only tools so the model won't attempt writes
747
+ const planModeAllowed = new Set([
748
+ "EnterPlanMode",
749
+ "ExitPlanMode",
750
+ "Read",
751
+ "Glob",
752
+ "Grep",
753
+ "WebSearch",
754
+ "WebFetch",
755
+ "AskUserQuestion",
756
+ "Agent",
757
+ "ToolSearch",
758
+ "TaskCreate",
759
+ "TaskUpdate",
760
+ "TaskList",
761
+ "TaskGet",
762
+ "Bash", // Bash is included but executor filters non-read-only commands
763
+ ]);
764
+ const toolDefs = this.planMode
765
+ ? allToolDefs.filter((t) => planModeAllowed.has(t.name))
766
+ : allToolDefs;
767
+ const [llmClient, systemPrompt, systemContext] = await Promise.all([
768
+ llmClientPromise,
769
+ promptComposer.buildSystemPrompt(toolDefs),
770
+ promptComposer.buildSystemContext(),
771
+ ]);
772
+ const fullSystemPrompt = [systemPrompt, systemContext].filter(Boolean).join("\n\n");
773
+ // Prepend userContext (CLAUDE.md) as first message (sync, fast)
774
+ const userContextMsg = promptComposer.buildUserContextMessage();
775
+ if (userContextMsg) {
776
+ messages.unshift(userContextMsg);
777
+ }
778
+ // Inject hook-supplied reminders just before the most recent user task.
779
+ // Combined into one <system-reminder> block so a noisy handler chain
780
+ // doesn't turn into 3+ separate user turns in the API request.
781
+ const lifecycleReminder = wrapHookMessages([
782
+ ...(sessionStartHook.messages ?? []),
783
+ ...(promptSubmitHook.messages ?? []),
784
+ ]);
785
+ if (lifecycleReminder) {
786
+ // messages[length - 1] is the user task we just pushed above. Insert
787
+ // the reminder immediately before it so the model reads: CLAUDE.md →
788
+ // reminder → user request.
789
+ messages.splice(messages.length - 1, 0, lifecycleReminder);
790
+ }
791
+ this.lastSessionId = session.state.sessionId;
792
+ this.lastMessages = messages;
793
+ // Wire up LLM summarization for context compaction
794
+ // Uses a lightweight call without tools
795
+ contextManager.setTranscriptPath(session.transcript.getFilePath());
796
+ // Re-derive frozen persistence decisions from the messages we just
797
+ // loaded. Skipped on cold start (messages == [userContextMsg] only).
798
+ // Critical for resume — otherwise a result that was persisted last
799
+ // run would be evaluated fresh and might get a different replacement
800
+ // string than the one already in the message, breaking idempotency.
801
+ contextManager.initReplacementStateFromMessages(messages);
802
+ contextManager.setSummarizeFn(async (prompt) => {
803
+ const summaryResponse = await llmClient.createMessage({
804
+ systemPrompt: "You are a conversation summarizer. Be concise and factual.",
805
+ messages: [{ role: "user", content: prompt }],
806
+ tools: [],
807
+ maxTokens: 1024,
808
+ // Auxiliary call — no need to burn reasoning tokens. On DeepSeek V4
809
+ // this flips thinking off (~3x faster, fewer tokens); on every other
810
+ // OpenAI-compatible provider the field is ignored.
811
+ thinking: "disabled",
812
+ });
813
+ return summaryResponse.text;
814
+ });
815
+ // Create components (requires resolved llmClient)
816
+ const modelFacade = new ModelFacade(llmClient, session.transcript);
817
+ // Wire getOutputTokens for token budget tracking
818
+ modelFacade.getOutputTokens = () => {
819
+ const usage = llmClient.getUsage();
820
+ return usage.totalCompletionTokens;
821
+ };
822
+ // Wire summarize for tool use summaries (uses lightweight call).
823
+ // recordUsage=false keeps these auxiliary sub-calls out of the main usage
824
+ // tracker so session_end.cost reflects only the user-facing turns and
825
+ // turns/requestCount stay aligned.
826
+ modelFacade.summarize = async (sysPrompt, userMsg) => {
827
+ const resp = await llmClient.createMessage({
828
+ systemPrompt: sysPrompt,
829
+ messages: [{ role: "user", content: userMsg }],
830
+ tools: [],
831
+ maxTokens: 256,
832
+ recordUsage: false,
833
+ // Auxiliary call — see contextManager.setSummarizeFn above.
834
+ thinking: "disabled",
835
+ });
836
+ logger.debug("summarize.call", {
837
+ sysPromptLen: sysPrompt.length,
838
+ userMsgLen: userMsg.length,
839
+ userMsgPreview: userMsg.slice(0, 300),
840
+ completionLen: resp.text.length,
841
+ completionPreview: resp.text.slice(0, 300),
842
+ stopReason: resp.stopReason,
843
+ promptTokens: resp.usage?.promptTokens,
844
+ completionTokens: resp.usage?.completionTokens,
845
+ });
846
+ return resp.text;
847
+ };
848
+ // File history: auto-backup before Write/Edit
849
+ const sessionDir = join(this.config.sessionStorageDir ?? join(homedir(), ".code-shell", "sessions"), session.state.sessionId);
850
+ const fileHistory = FileHistory.loadFromDir(sessionDir);
851
+ this.hooks.register("on_tool_start", async (context) => {
852
+ const toolName = context.data?.toolName;
853
+ const args = context.data?.args;
854
+ if ((toolName === "Write" || toolName === "Edit") && args?.file_path) {
855
+ fileHistory.saveSnapshot(args.file_path);
856
+ }
857
+ return {};
858
+ }, 100, "file_history_backup");
859
+ // Hook: agent start
860
+ await this.emitHook("on_agent_start", {
861
+ sessionId: session.state.sessionId,
862
+ task,
863
+ model: this.config.llm.model,
864
+ });
865
+ // Surface compaction events to the UI so the user knows when context was trimmed.
866
+ // Buffer the most recent event so TurnLoop can drain it and emit the
867
+ // post_compact hook on the next turn (ContextManager itself doesn't
868
+ // know about HookRegistry — the buffer is the seam).
869
+ let pendingCompactInfo = null;
870
+ contextManager.setOnCompact((info) => {
871
+ pendingCompactInfo = info;
872
+ options?.onStream?.({ type: "context_compact", ...info });
873
+ });
874
+ // Run turn loop
875
+ const turnLoop = new TurnLoop({
876
+ model: modelFacade,
877
+ toolExecutor,
878
+ contextManager,
879
+ hooks: this.hooks,
880
+ transcript: session.transcript,
881
+ systemPrompt: fullSystemPrompt,
882
+ tools: toolDefs,
883
+ sessionId: sid,
884
+ isSubAgent: this.config.isSubAgent === true,
885
+ consumePendingCompactInfo: () => {
886
+ const info = pendingCompactInfo;
887
+ pendingCompactInfo = null;
888
+ return info;
889
+ },
890
+ ctxOverheadStore: {
891
+ get: (s) => this.ctxOverheadBySid.get(s) ?? 0,
892
+ set: (s, n) => {
893
+ this.ctxOverheadBySid.set(s, n);
894
+ },
895
+ },
896
+ }, {
897
+ maxTurns: this.config.maxTurns ?? 100,
898
+ maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 10,
899
+ onStream: options?.onStream,
900
+ signal: options?.signal,
901
+ // Heartbeat: flush turnCount + tokens to state.json after every turn
902
+ // so external observers (other CLI processes, /sid, the session list)
903
+ // see live progress instead of a stale snapshot from the last
904
+ // completed run.
905
+ onTurnBoundary: (turnCount) => {
906
+ session.state.turnCount = turnCount;
907
+ const u = modelFacade.getUsage();
908
+ session.state.tokenUsage = {
909
+ promptTokens: u.totalPromptTokens,
910
+ completionTokens: u.totalCompletionTokens,
911
+ totalTokens: u.totalTokens,
912
+ };
913
+ if (this.config.costStore) {
914
+ session.state.costState = this.config.costStore.serialize();
915
+ }
916
+ this.sessionManager.saveState(session.state);
917
+ },
918
+ });
919
+ const result = await turnLoop.run(messages);
920
+ this.lastMessages = result.messages;
921
+ this.compactedMessagesBySession.set(session.state.sessionId, this.stripUserContextMessage(result.messages, userContextMsg));
922
+ logger.info("engine.done", {
923
+ sessionId: session.state.sessionId,
924
+ reason: result.reason,
925
+ turns: turnLoop.currentTurn,
926
+ tokens: modelFacade.getUsage().totalTokens,
927
+ });
928
+ recordSessionEnd(session.state.sessionId, {
929
+ reason: result.reason,
930
+ turns: turnLoop.currentTurn,
931
+ cost: modelFacade.getUsage(),
932
+ });
933
+ // Session-level hook: fired symmetrically with on_session_start once
934
+ // the turn loop has resolved (completion, error, or abort). Handlers
935
+ // are notify-only — any returned messages are dropped because the run
936
+ // is already over and there's no next turn to inject into.
937
+ await this.emitHook("on_session_end", {
938
+ sessionId: session.state.sessionId,
939
+ reason: result.reason,
940
+ turnCount: turnLoop.currentTurn,
941
+ });
942
+ // Fire-and-forget memory pipeline: extract durable memories from the
943
+ // transcript, save a session summary, and conditionally trigger
944
+ // auto-dream consolidation. Doesn't block the Engine result.
945
+ void this.runMemoryPipeline(session.transcript, session.state.sessionId, cwd, llmClient);
946
+ // Update session state. Persist the raw terminal reason as the status so
947
+ // callers can distinguish user-cancelled (aborted_streaming) from real
948
+ // failures (model_error, prompt_too_long, ...) — previously every
949
+ // non-completed outcome collapsed to "errored", which threw away the
950
+ // distinction and misled anyone reading state.json.
951
+ session.state.turnCount = turnLoop.currentTurn;
952
+ session.state.status = result.reason;
953
+ const usage = modelFacade.getUsage();
954
+ session.state.tokenUsage = {
955
+ promptTokens: usage.totalPromptTokens,
956
+ completionTokens: usage.totalCompletionTokens,
957
+ totalTokens: usage.totalTokens,
958
+ };
959
+ if (this.config.costStore) {
960
+ session.state.costState = this.config.costStore.serialize();
961
+ }
962
+ this.sessionManager.saveState(session.state);
963
+ // Hook: agent end
964
+ await this.emitHook("on_agent_end", {
965
+ sessionId: session.state.sessionId,
966
+ reason: result.reason,
967
+ turnCount: turnLoop.currentTurn,
968
+ });
969
+ // Emit completion
970
+ options?.onStream?.({ type: "turn_complete", reason: result.reason });
971
+ return {
972
+ text: result.text,
973
+ reason: result.reason,
974
+ sessionId: session.state.sessionId,
975
+ turnCount: turnLoop.currentTurn,
976
+ usage: {
977
+ promptTokens: usage.totalPromptTokens,
978
+ completionTokens: usage.totalCompletionTokens,
979
+ totalTokens: usage.totalTokens,
980
+ },
981
+ };
982
+ });
983
+ }
984
+ /**
985
+ * Run the end-of-session memory pipeline as a fire-and-forget background
986
+ * task. Extracts durable memories from the transcript, saves a session
987
+ * summary, and conditionally triggers auto-dream consolidation.
988
+ */
989
+ async runMemoryPipeline(transcript, sessionId, cwd, llmClient) {
990
+ try {
991
+ // Only run memory extraction for substantive sessions. The previous
992
+ // threshold of 4 user+assistant messages was low enough that two-line
993
+ // exchanges ("what's the time?" / "noon") triggered a full LLM
994
+ // extraction, which then padded the memory store with low-signal
995
+ // entries. 8 messages is roughly "more than a single back-and-forth"
996
+ // — substantive enough to be worth a durable note.
997
+ const messages = transcript.toMessages().filter((m) => m.role === "user" || m.role === "assistant");
998
+ if (messages.length < 8)
999
+ return;
1000
+ // Memory orchestrator + dream-loop calls are auxiliary LLM calls
1001
+ // that don't (and shouldn't) carry image payloads. Sanitize before
1002
+ // stringify so we don't pump a 10 MB base64 string into the
1003
+ // summarization prompt — provider 400s on it, and it leaks bytes
1004
+ // into a downstream cost-tracking path we don't audit as carefully
1005
+ // as the primary turn.
1006
+ const plainMessages = messages.map((m) => {
1007
+ const safe = sanitizeContent(m.content);
1008
+ return {
1009
+ role: m.role,
1010
+ content: typeof safe === "string" ? safe : JSON.stringify(safe),
1011
+ };
1012
+ });
1013
+ const orchestrator = new MemoryOrchestrator({
1014
+ callLLM: async (sysPrompt, userMsg) => {
1015
+ // Use a lightweight auxiliary call (no tools, no streaming, no
1016
+ // reasoning tokens).
1017
+ const resp = await llmClient.createMessage({
1018
+ systemPrompt: sysPrompt,
1019
+ messages: [{ role: "user", content: userMsg }],
1020
+ tools: [],
1021
+ maxTokens: 1024,
1022
+ recordUsage: false,
1023
+ thinking: "disabled",
1024
+ });
1025
+ return resp.text;
1026
+ },
1027
+ runDream: async ({ systemPrompt, userPrompt, projectDir }) => this.runDreamLoop({ systemPrompt, userPrompt, projectDir, llmClient, sessionId }),
1028
+ projectDir: cwd,
1029
+ });
1030
+ await orchestrator.run(plainMessages, sessionId);
1031
+ }
1032
+ catch (err) {
1033
+ // Memory pipeline is best-effort — never surface errors to the user.
1034
+ logger.warn("engine.memory_pipeline_failed", {
1035
+ sessionId,
1036
+ error: err.message,
1037
+ });
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Drive the auto-dream tool-call loop.
1042
+ *
1043
+ * Runs the LLM with a whitelisted subset of memory tools (MemoryList,
1044
+ * MemoryRead, MemorySave, MemoryDelete). The loop is intentionally small
1045
+ * and offline:
1046
+ * - No streaming, no UI events — runs in the background after a session.
1047
+ * - No permission prompts — UI isn't attached, so we hard-reject any
1048
+ * attempt to Save/Delete in the "user" scope before dispatching. Dream
1049
+ * scope is the LLM's workspace and goes through freely.
1050
+ * - Capped at MAX_TURNS LLM round-trips and MAX_WRITES total
1051
+ * mutations to bound damage on misbehavior.
1052
+ *
1053
+ * Returns true if the loop ran (with or without writes); false if we
1054
+ * bailed before the first LLM call (e.g. registry missing the tools).
1055
+ */
1056
+ async runDreamLoop(opts) {
1057
+ const MAX_TURNS = 8;
1058
+ const MAX_WRITES = 10;
1059
+ const MEMORY_TOOL_NAMES = ["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"];
1060
+ const memoryTools = MEMORY_TOOL_NAMES
1061
+ .map((n) => this.toolRegistry.getTool(n))
1062
+ .filter((t) => t != null);
1063
+ if (memoryTools.length < MEMORY_TOOL_NAMES.length) {
1064
+ logger.warn("memory.auto_dream_missing_tools", {
1065
+ sessionId: opts.sessionId,
1066
+ found: memoryTools.map((t) => t.name),
1067
+ });
1068
+ return false;
1069
+ }
1070
+ // Strip RegisteredTool down to the shape createMessage expects.
1071
+ const toolDefs = memoryTools.map((t) => ({
1072
+ name: t.name,
1073
+ description: t.description,
1074
+ inputSchema: t.inputSchema,
1075
+ }));
1076
+ const toolCtx = {
1077
+ ...this.buildToolContext(),
1078
+ cwd: opts.projectDir ?? process.cwd(),
1079
+ };
1080
+ const messages = [{ role: "user", content: opts.userPrompt }];
1081
+ let writeBudget = MAX_WRITES;
1082
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
1083
+ const resp = await opts.llmClient.createMessage({
1084
+ systemPrompt: opts.systemPrompt,
1085
+ messages,
1086
+ tools: toolDefs,
1087
+ maxTokens: 2048,
1088
+ recordUsage: false,
1089
+ thinking: "disabled",
1090
+ });
1091
+ if (resp.toolCalls.length === 0) {
1092
+ logger.info("memory.auto_dream_finished", {
1093
+ sessionId: opts.sessionId,
1094
+ turn,
1095
+ finalText: resp.text.slice(0, 500),
1096
+ });
1097
+ return true;
1098
+ }
1099
+ // Echo the assistant turn back into the conversation so subsequent
1100
+ // turns see the tool_use ids they need to reference.
1101
+ const assistantContent = [];
1102
+ if (resp.text)
1103
+ assistantContent.push({ type: "text", text: resp.text });
1104
+ for (const tc of resp.toolCalls) {
1105
+ assistantContent.push({
1106
+ type: "tool_use",
1107
+ id: tc.id,
1108
+ name: tc.toolName,
1109
+ input: tc.args,
1110
+ });
1111
+ }
1112
+ messages.push({ role: "assistant", content: assistantContent });
1113
+ // Dispatch every tool call requested in this turn.
1114
+ const toolResults = [];
1115
+ for (const tc of resp.toolCalls) {
1116
+ const result = await this.dispatchDreamTool(tc, toolCtx, () => {
1117
+ if (writeBudget <= 0)
1118
+ return false;
1119
+ writeBudget--;
1120
+ return true;
1121
+ });
1122
+ toolResults.push({
1123
+ type: "tool_result",
1124
+ tool_use_id: tc.id,
1125
+ content: result,
1126
+ });
1127
+ }
1128
+ messages.push({ role: "user", content: toolResults });
1129
+ }
1130
+ logger.warn("memory.auto_dream_hit_turn_cap", {
1131
+ sessionId: opts.sessionId,
1132
+ maxTurns: MAX_TURNS,
1133
+ });
1134
+ return true;
1135
+ }
1136
+ /**
1137
+ * Execute one memory tool call inside the dream loop. Enforces the two
1138
+ * dream-loop invariants the prompt also states:
1139
+ * - Only the 4 memory tools are dispatchable.
1140
+ * - Save/Delete in "user" scope is refused (returned as a tool error)
1141
+ * because dream runs without an interactive permission backend.
1142
+ */
1143
+ async dispatchDreamTool(tc, ctx, consumeWriteBudget) {
1144
+ const allowed = new Set(["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"]);
1145
+ if (!allowed.has(tc.toolName)) {
1146
+ return `Error: tool "${tc.toolName}" is not available in the dream loop`;
1147
+ }
1148
+ const isWrite = tc.toolName === "MemorySave" || tc.toolName === "MemoryDelete";
1149
+ if (isWrite) {
1150
+ const scope = tc.args?.scope;
1151
+ if (scope !== "dream") {
1152
+ return (`Error: dream loop may only write to scope "dream", got "${scope}". ` +
1153
+ `User-scope changes require interactive permission, which is not available here.`);
1154
+ }
1155
+ if (!consumeWriteBudget()) {
1156
+ return "Error: dream write budget exhausted — stop calling write tools and summarize instead.";
1157
+ }
1158
+ }
1159
+ try {
1160
+ const result = await this.toolRegistry.executeTool(tc.toolName, tc.args, { ctx });
1161
+ if (result.isError)
1162
+ return result.error ?? `Error executing ${tc.toolName}`;
1163
+ return result.result ?? "";
1164
+ }
1165
+ catch (err) {
1166
+ return `Error executing ${tc.toolName}: ${err.message}`;
1167
+ }
1168
+ }
1169
+ getToolRegistry() {
1170
+ return this.toolRegistry;
1171
+ }
1172
+ /**
1173
+ * Switch the active model by pool key. Takes effect on the next run() call.
1174
+ * Returns the new model entry.
1175
+ *
1176
+ * Persists settings.activeKey (and a legacy settings.model.* mirror) so the
1177
+ * next process startup defaults to the same model — without this, switches
1178
+ * only live in memory and every restart reverts to the previously persisted
1179
+ * activeKey.
1180
+ */
1181
+ switchModel(key) {
1182
+ const entry = this.modelPool.switch(key);
1183
+ const nextLlm = this.modelPool.toLLMConfig(entry, this.config.llm);
1184
+ this.config = { ...this.config, llm: nextLlm };
1185
+ this.persistActiveModel(entry, nextLlm);
1186
+ return entry;
1187
+ }
1188
+ /**
1189
+ * Write the active model selection to ~/.code-shell/settings.json.
1190
+ *
1191
+ * We mirror into the legacy settings.model.* block (provider/name/apiKey/
1192
+ * baseUrl) because boot paths in cli/main.ts, repl.ts, run.ts still read it.
1193
+ * The mirror uses resolved llm values (not raw entry.*) so credentials that
1194
+ * live on settings.providers[] flow through correctly — entry.apiKey is
1195
+ * undefined when the entry resolves via providerCatalog.
1196
+ */
1197
+ persistActiveModel(entry, llm) {
1198
+ try {
1199
+ const dir = join(homedir(), ".code-shell");
1200
+ const file = join(dir, "settings.json");
1201
+ mkdirSync(dir, { recursive: true });
1202
+ let existing = {};
1203
+ if (existsSync(file)) {
1204
+ try {
1205
+ existing = JSON.parse(readFileSync(file, "utf-8"));
1206
+ }
1207
+ catch {
1208
+ // corrupt file — bail rather than clobber the user's config
1209
+ return;
1210
+ }
1211
+ }
1212
+ const prevModel = typeof existing.model === "object" && existing.model
1213
+ ? existing.model
1214
+ : {};
1215
+ const updated = {
1216
+ ...existing,
1217
+ activeKey: entry.key,
1218
+ model: {
1219
+ ...prevModel,
1220
+ provider: llm.provider,
1221
+ name: entry.model,
1222
+ apiKey: llm.apiKey,
1223
+ baseUrl: llm.baseUrl,
1224
+ },
1225
+ };
1226
+ const tmp = `${file}.${process.pid}.tmp`;
1227
+ const payload = JSON.stringify(updated, null, 2) + "\n";
1228
+ writeFileSync(tmp, payload, "utf-8");
1229
+ try {
1230
+ renameSync(tmp, file);
1231
+ }
1232
+ catch {
1233
+ writeFileSync(file, payload, "utf-8");
1234
+ }
1235
+ }
1236
+ catch (err) {
1237
+ logger.warn(`persistActiveModel failed: ${err.message}`);
1238
+ }
1239
+ }
1240
+ /** Get the model pool. */
1241
+ getModelPool() {
1242
+ return this.modelPool;
1243
+ }
1244
+ /** Get the current model name (full path). */
1245
+ getCurrentModel() {
1246
+ return this.config.llm.model;
1247
+ }
1248
+ getHookRegistry() {
1249
+ return this.hooks;
1250
+ }
1251
+ getSessionManager() {
1252
+ return this.sessionManager;
1253
+ }
1254
+ getConfig() {
1255
+ return this.config;
1256
+ }
1257
+ /**
1258
+ * Inject context into a session's transcript without triggering a LLM turn.
1259
+ * The injected content appears as an assistant message so the LLM can see it
1260
+ * in subsequent conversations. The transcript auto-flushes to disk.
1261
+ *
1262
+ * Also updates the in-memory compacted message cache so the next
1263
+ * engine.run() call for this session picks up the injected content
1264
+ * instead of a stale snapshot from the previous run.
1265
+ */
1266
+ injectContext(sessionId, content) {
1267
+ const session = this.sessionManager.resume(sessionId);
1268
+ session.transcript.appendMessage("assistant", content);
1269
+ // Keep the compacted cache in sync so the next engine.run() call
1270
+ // (which reads from compactedMessagesBySession first) sees the
1271
+ // injected content rather than a stale pre-inject snapshot.
1272
+ const cached = this.compactedMessagesBySession.get(sessionId);
1273
+ if (cached) {
1274
+ cached.push({ role: "assistant", content });
1275
+ }
1276
+ }
1277
+ /**
1278
+ * Force context compaction on the current session.
1279
+ * Returns token stats before/after.
1280
+ */
1281
+ forceCompact() {
1282
+ const sessionId = this.lastSessionId;
1283
+ if (!this.lastContextManager || !sessionId) {
1284
+ return { before: 0, after: 0, strategy: "none (no active session)" };
1285
+ }
1286
+ const { estimateTokens } = require("../context/compaction.js");
1287
+ const sourceMessages = this.compactedMessagesBySession.get(sessionId) ??
1288
+ this.sessionManager.resume(sessionId).transcript.toMessages();
1289
+ const before = estimateTokens(sourceMessages);
1290
+ const compacted = this.lastContextManager.manage(sourceMessages);
1291
+ const after = estimateTokens(compacted);
1292
+ this.compactedMessagesBySession.set(sessionId, compacted);
1293
+ this.lastMessages = compacted;
1294
+ return {
1295
+ before,
1296
+ after,
1297
+ strategy: before === after ? "no compaction needed" : "compacted",
1298
+ };
1299
+ }
1300
+ stripUserContextMessage(messages, userContextMsg) {
1301
+ if (!userContextMsg || messages[0] !== userContextMsg) {
1302
+ return [...messages];
1303
+ }
1304
+ return messages.slice(1);
1305
+ }
1306
+ getSettingsManager() {
1307
+ if (!this.settingsManager) {
1308
+ this.settingsManager = new SettingsManager(this.config.cwd);
1309
+ }
1310
+ return this.settingsManager;
1311
+ }
1312
+ /**
1313
+ * Update a config setting at runtime.
1314
+ */
1315
+ updateConfig(key, value) {
1316
+ this.getSettingsManager().saveUserSetting(key, value);
1317
+ }
1318
+ /**
1319
+ * Read a settings value by dotted key (e.g. "arena.participants").
1320
+ * Returns undefined if any segment is missing.
1321
+ */
1322
+ readSetting(key) {
1323
+ const settings = this.getSettingsManager().get();
1324
+ const parts = key.split(".");
1325
+ let target = settings;
1326
+ for (const p of parts) {
1327
+ if (target == null || typeof target !== "object")
1328
+ return undefined;
1329
+ target = target[p];
1330
+ }
1331
+ return target;
1332
+ }
1333
+ buildPermissionConfig(mode, cwd) {
1334
+ const rules = [...this.preset.defaultPermissionRules];
1335
+ // Memory tools: dream scope is the LLM's own workspace, so save/delete
1336
+ // there go through without prompting. user-scope save/delete fall through
1337
+ // to the tool's permissionDefault ("ask"), forcing the user to confirm
1338
+ // any modification of memories they own. Read tools are listed in the
1339
+ // tool definition as permissionDefault: "allow" — no rule needed here.
1340
+ rules.push({
1341
+ tool: "MemorySave",
1342
+ argsPattern: { scope: "^dream$" },
1343
+ decision: "allow",
1344
+ reason: "Dream scope is the LLM's auto-consolidation workspace",
1345
+ });
1346
+ rules.push({
1347
+ tool: "MemoryDelete",
1348
+ argsPattern: { scope: "^dream$" },
1349
+ decision: "allow",
1350
+ reason: "Dream scope is the LLM's auto-consolidation workspace",
1351
+ });
1352
+ if (mode === "acceptEdits" || mode === "bypassPermissions") {
1353
+ rules.push({ tool: "Write", decision: "allow" });
1354
+ rules.push({ tool: "Edit", decision: "allow" });
1355
+ }
1356
+ if (mode === "bypassPermissions") {
1357
+ rules.push({ tool: "Bash", decision: "allow" });
1358
+ }
1359
+ try {
1360
+ const settingsManager = new SettingsManager(cwd);
1361
+ const settings = settingsManager.get();
1362
+ if (settings.permissions?.rules?.length) {
1363
+ rules.unshift(...settings.permissions.rules);
1364
+ }
1365
+ }
1366
+ catch {
1367
+ // Settings not available — defaults only
1368
+ }
1369
+ let backend;
1370
+ if (this.config.approvalBackend) {
1371
+ backend =
1372
+ mode === "auto"
1373
+ ? new AutoApprovalBackend(this.config.approvalBackend)
1374
+ : this.config.approvalBackend;
1375
+ }
1376
+ else if (mode === "auto") {
1377
+ backend = new AutoApprovalBackend();
1378
+ }
1379
+ else {
1380
+ // If a host installed an InteractiveApprovalBackend prompt fn
1381
+ // (agent-server-stdio does this on boot via setInteractiveApprovalFn),
1382
+ // use it so the UI gets a chance to approve/deny. Without this,
1383
+ // every `ask` permission silently fell through to deny-all and
1384
+ // the user saw "Permission denied by user" with NO modal — exactly
1385
+ // the bug that motivated this fix.
1386
+ const interactive = getInteractiveApprovalBackend();
1387
+ if (interactive.hasPromptFn()) {
1388
+ backend = interactive;
1389
+ }
1390
+ else {
1391
+ backend = new HeadlessApprovalBackend(mode === "bypassPermissions" ? "approve-all" : mode === "dontAsk" ? "deny-all" : "deny-all");
1392
+ }
1393
+ }
1394
+ return { rules, backend };
1395
+ }
1396
+ /**
1397
+ * Switch permission mode at runtime. Takes effect immediately for any
1398
+ * in-flight ToolExecutor (which holds a reference to the same classifier),
1399
+ * and the new mode is used for any subsequent run() calls.
1400
+ * Session-only — does not persist to settings.
1401
+ */
1402
+ setPermissionMode(mode) {
1403
+ this.config = { ...this.config, permissionMode: mode };
1404
+ this.permissionMode = mode;
1405
+ this.planMode = mode === "plan";
1406
+ if (this.activePermission) {
1407
+ const cwd = this.config.cwd ?? process.cwd();
1408
+ const { rules, backend } = this.buildPermissionConfig(mode, cwd);
1409
+ this.activePermission.reconfigure(mode, backend, rules);
1410
+ }
1411
+ }
1412
+ getPermissionMode() {
1413
+ return this.config.permissionMode ?? "acceptEdits";
1414
+ }
1415
+ /**
1416
+ * Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
1417
+ * Also syncs permissionMode to keep both fields consistent.
1418
+ */
1419
+ setPlanMode(value) {
1420
+ if (value) {
1421
+ this.setPermissionMode("plan");
1422
+ }
1423
+ else if (this.permissionMode === "plan") {
1424
+ // Leaving plan mode: drop back to the default.
1425
+ this.setPermissionMode("acceptEdits");
1426
+ }
1427
+ else {
1428
+ this.planMode = value;
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Build a base ToolContext for this Engine. Used by run() (which then
1433
+ * overlays turn-specific fields like sandbox and subAgentSpawner) and
1434
+ * by tests that want a ToolContext without a full run() cycle.
1435
+ */
1436
+ buildToolContext() {
1437
+ return {
1438
+ cwd: this.config.cwd ?? process.cwd(),
1439
+ llmConfig: this.config.llm,
1440
+ modelPool: this.modelPool,
1441
+ toolRegistry: this.toolRegistry,
1442
+ askUser: this.config.askUser,
1443
+ isSubAgent: this.config.isSubAgent === true,
1444
+ hooks: this.hooks,
1445
+ planMode: this.planMode,
1446
+ engine: this,
1447
+ disabledSkills: this.readDisabledSkills(),
1448
+ };
1449
+ }
1450
+ /**
1451
+ * Read settings.disabledSkills. Sub-agents skip this for the same
1452
+ * reason they skip settings.hooks / plugin hooks (registerSettingsHooks
1453
+ * at ~line 237): they run with a minimal surface area. Defaults to []
1454
+ * so callers don't have to null-check.
1455
+ */
1456
+ readDisabledSkills() {
1457
+ if (this.config.isSubAgent === true)
1458
+ return [];
1459
+ try {
1460
+ const settings = this.getSettingsManager().get();
1461
+ return settings.disabledSkills ?? [];
1462
+ }
1463
+ catch {
1464
+ return [];
1465
+ }
1466
+ }
1467
+ }