@chances-ai/engine 24.0.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 (389) hide show
  1. package/dist/agents/discover.d.ts +30 -0
  2. package/dist/agents/discover.d.ts.map +1 -0
  3. package/dist/agents/discover.js +183 -0
  4. package/dist/agents/discover.js.map +1 -0
  5. package/dist/agents/index.d.ts +20 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/index.js +52 -0
  8. package/dist/agents/index.js.map +1 -0
  9. package/dist/agents/parse.d.ts +61 -0
  10. package/dist/agents/parse.d.ts.map +1 -0
  11. package/dist/agents/parse.js +527 -0
  12. package/dist/agents/parse.js.map +1 -0
  13. package/dist/agents/types.d.ts +52 -0
  14. package/dist/agents/types.d.ts.map +1 -0
  15. package/dist/agents/types.js +8 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
  18. package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
  19. package/dist/ai/adapters/ai-sdk-stream.js +125 -0
  20. package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
  21. package/dist/ai/adapters/ai-sdk.d.ts +56 -0
  22. package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
  23. package/dist/ai/adapters/ai-sdk.js +112 -0
  24. package/dist/ai/adapters/ai-sdk.js.map +1 -0
  25. package/dist/ai/adapters/mock.d.ts +13 -0
  26. package/dist/ai/adapters/mock.d.ts.map +1 -0
  27. package/dist/ai/adapters/mock.js +54 -0
  28. package/dist/ai/adapters/mock.js.map +1 -0
  29. package/dist/ai/adapters/openai-compatible.d.ts +23 -0
  30. package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
  31. package/dist/ai/adapters/openai-compatible.js +45 -0
  32. package/dist/ai/adapters/openai-compatible.js.map +1 -0
  33. package/dist/ai/cost.d.ts +3 -0
  34. package/dist/ai/cost.d.ts.map +1 -0
  35. package/dist/ai/cost.js +5 -0
  36. package/dist/ai/cost.js.map +1 -0
  37. package/dist/ai/index.d.ts +12 -0
  38. package/dist/ai/index.d.ts.map +1 -0
  39. package/dist/ai/index.js +11 -0
  40. package/dist/ai/index.js.map +1 -0
  41. package/dist/ai/known-models.d.ts +20 -0
  42. package/dist/ai/known-models.d.ts.map +1 -0
  43. package/dist/ai/known-models.js +129 -0
  44. package/dist/ai/known-models.js.map +1 -0
  45. package/dist/ai/registry.d.ts +12 -0
  46. package/dist/ai/registry.d.ts.map +1 -0
  47. package/dist/ai/registry.js +24 -0
  48. package/dist/ai/registry.js.map +1 -0
  49. package/dist/ai/retry.d.ts +11 -0
  50. package/dist/ai/retry.d.ts.map +1 -0
  51. package/dist/ai/retry.js +14 -0
  52. package/dist/ai/retry.js.map +1 -0
  53. package/dist/ai/router.d.ts +25 -0
  54. package/dist/ai/router.d.ts.map +1 -0
  55. package/dist/ai/router.js +36 -0
  56. package/dist/ai/router.js.map +1 -0
  57. package/dist/ai/setup.d.ts +23 -0
  58. package/dist/ai/setup.d.ts.map +1 -0
  59. package/dist/ai/setup.js +47 -0
  60. package/dist/ai/setup.js.map +1 -0
  61. package/dist/ai/summarizer.d.ts +24 -0
  62. package/dist/ai/summarizer.d.ts.map +1 -0
  63. package/dist/ai/summarizer.js +56 -0
  64. package/dist/ai/summarizer.js.map +1 -0
  65. package/dist/ai/types.d.ts +83 -0
  66. package/dist/ai/types.d.ts.map +1 -0
  67. package/dist/ai/types.js +2 -0
  68. package/dist/ai/types.js.map +1 -0
  69. package/dist/core/compaction/circuit-breaker.d.ts +32 -0
  70. package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
  71. package/dist/core/compaction/circuit-breaker.js +42 -0
  72. package/dist/core/compaction/circuit-breaker.js.map +1 -0
  73. package/dist/core/compaction/compactor.d.ts +75 -0
  74. package/dist/core/compaction/compactor.d.ts.map +1 -0
  75. package/dist/core/compaction/compactor.js +261 -0
  76. package/dist/core/compaction/compactor.js.map +1 -0
  77. package/dist/core/compaction/estimate.d.ts +39 -0
  78. package/dist/core/compaction/estimate.d.ts.map +1 -0
  79. package/dist/core/compaction/estimate.js +74 -0
  80. package/dist/core/compaction/estimate.js.map +1 -0
  81. package/dist/core/compaction/index.d.ts +5 -0
  82. package/dist/core/compaction/index.d.ts.map +1 -0
  83. package/dist/core/compaction/index.js +5 -0
  84. package/dist/core/compaction/index.js.map +1 -0
  85. package/dist/core/compaction/prune.d.ts +43 -0
  86. package/dist/core/compaction/prune.d.ts.map +1 -0
  87. package/dist/core/compaction/prune.js +51 -0
  88. package/dist/core/compaction/prune.js.map +1 -0
  89. package/dist/core/engine.d.ts +268 -0
  90. package/dist/core/engine.d.ts.map +1 -0
  91. package/dist/core/engine.js +767 -0
  92. package/dist/core/engine.js.map +1 -0
  93. package/dist/core/index.d.ts +6 -0
  94. package/dist/core/index.d.ts.map +1 -0
  95. package/dist/core/index.js +6 -0
  96. package/dist/core/index.js.map +1 -0
  97. package/dist/core/task-tool.d.ts +175 -0
  98. package/dist/core/task-tool.d.ts.map +1 -0
  99. package/dist/core/task-tool.js +901 -0
  100. package/dist/core/task-tool.js.map +1 -0
  101. package/dist/core/workspace-query.d.ts +83 -0
  102. package/dist/core/workspace-query.d.ts.map +1 -0
  103. package/dist/core/workspace-query.js +217 -0
  104. package/dist/core/workspace-query.js.map +1 -0
  105. package/dist/core/worktree/active-marker.d.ts +31 -0
  106. package/dist/core/worktree/active-marker.d.ts.map +1 -0
  107. package/dist/core/worktree/active-marker.js +109 -0
  108. package/dist/core/worktree/active-marker.js.map +1 -0
  109. package/dist/core/worktree/create.d.ts +40 -0
  110. package/dist/core/worktree/create.d.ts.map +1 -0
  111. package/dist/core/worktree/create.js +121 -0
  112. package/dist/core/worktree/create.js.map +1 -0
  113. package/dist/core/worktree/errors.d.ts +7 -0
  114. package/dist/core/worktree/errors.d.ts.map +1 -0
  115. package/dist/core/worktree/errors.js +11 -0
  116. package/dist/core/worktree/errors.js.map +1 -0
  117. package/dist/core/worktree/gc.d.ts +39 -0
  118. package/dist/core/worktree/gc.d.ts.map +1 -0
  119. package/dist/core/worktree/gc.js +146 -0
  120. package/dist/core/worktree/gc.js.map +1 -0
  121. package/dist/core/worktree/git.d.ts +53 -0
  122. package/dist/core/worktree/git.d.ts.map +1 -0
  123. package/dist/core/worktree/git.js +166 -0
  124. package/dist/core/worktree/git.js.map +1 -0
  125. package/dist/core/worktree/index.d.ts +8 -0
  126. package/dist/core/worktree/index.d.ts.map +1 -0
  127. package/dist/core/worktree/index.js +8 -0
  128. package/dist/core/worktree/index.js.map +1 -0
  129. package/dist/core/worktree/paths.d.ts +26 -0
  130. package/dist/core/worktree/paths.d.ts.map +1 -0
  131. package/dist/core/worktree/paths.js +57 -0
  132. package/dist/core/worktree/paths.js.map +1 -0
  133. package/dist/core/worktree/slug.d.ts +6 -0
  134. package/dist/core/worktree/slug.d.ts.map +1 -0
  135. package/dist/core/worktree/slug.js +21 -0
  136. package/dist/core/worktree/slug.js.map +1 -0
  137. package/dist/local-vault/file-store.d.ts +64 -0
  138. package/dist/local-vault/file-store.d.ts.map +1 -0
  139. package/dist/local-vault/file-store.js +225 -0
  140. package/dist/local-vault/file-store.js.map +1 -0
  141. package/dist/local-vault/index.d.ts +57 -0
  142. package/dist/local-vault/index.d.ts.map +1 -0
  143. package/dist/local-vault/index.js +68 -0
  144. package/dist/local-vault/index.js.map +1 -0
  145. package/dist/local-vault/keychain.d.ts +58 -0
  146. package/dist/local-vault/keychain.d.ts.map +1 -0
  147. package/dist/local-vault/keychain.js +200 -0
  148. package/dist/local-vault/keychain.js.map +1 -0
  149. package/dist/local-vault/mutex.d.ts +20 -0
  150. package/dist/local-vault/mutex.d.ts.map +1 -0
  151. package/dist/local-vault/mutex.js +44 -0
  152. package/dist/local-vault/mutex.js.map +1 -0
  153. package/dist/local-vault/passphrase.d.ts +30 -0
  154. package/dist/local-vault/passphrase.d.ts.map +1 -0
  155. package/dist/local-vault/passphrase.js +72 -0
  156. package/dist/local-vault/passphrase.js.map +1 -0
  157. package/dist/lsp/config.d.ts +34 -0
  158. package/dist/lsp/config.d.ts.map +1 -0
  159. package/dist/lsp/config.js +68 -0
  160. package/dist/lsp/config.js.map +1 -0
  161. package/dist/lsp/detect.d.ts +7 -0
  162. package/dist/lsp/detect.d.ts.map +1 -0
  163. package/dist/lsp/detect.js +78 -0
  164. package/dist/lsp/detect.js.map +1 -0
  165. package/dist/lsp/errors.d.ts +11 -0
  166. package/dist/lsp/errors.d.ts.map +1 -0
  167. package/dist/lsp/errors.js +11 -0
  168. package/dist/lsp/errors.js.map +1 -0
  169. package/dist/lsp/formatters.d.ts +147 -0
  170. package/dist/lsp/formatters.d.ts.map +1 -0
  171. package/dist/lsp/formatters.js +259 -0
  172. package/dist/lsp/formatters.js.map +1 -0
  173. package/dist/lsp/index.d.ts +31 -0
  174. package/dist/lsp/index.d.ts.map +1 -0
  175. package/dist/lsp/index.js +31 -0
  176. package/dist/lsp/index.js.map +1 -0
  177. package/dist/lsp/instance.d.ts +72 -0
  178. package/dist/lsp/instance.d.ts.map +1 -0
  179. package/dist/lsp/instance.js +489 -0
  180. package/dist/lsp/instance.js.map +1 -0
  181. package/dist/lsp/lazy-load.d.ts +27 -0
  182. package/dist/lsp/lazy-load.d.ts.map +1 -0
  183. package/dist/lsp/lazy-load.js +57 -0
  184. package/dist/lsp/lazy-load.js.map +1 -0
  185. package/dist/lsp/manager.d.ts +59 -0
  186. package/dist/lsp/manager.d.ts.map +1 -0
  187. package/dist/lsp/manager.js +242 -0
  188. package/dist/lsp/manager.js.map +1 -0
  189. package/dist/lsp/ops.d.ts +13 -0
  190. package/dist/lsp/ops.d.ts.map +1 -0
  191. package/dist/lsp/ops.js +225 -0
  192. package/dist/lsp/ops.js.map +1 -0
  193. package/dist/lsp/rpc.d.ts +47 -0
  194. package/dist/lsp/rpc.d.ts.map +1 -0
  195. package/dist/lsp/rpc.js +134 -0
  196. package/dist/lsp/rpc.js.map +1 -0
  197. package/dist/lsp/safe-uri.d.ts +18 -0
  198. package/dist/lsp/safe-uri.d.ts.map +1 -0
  199. package/dist/lsp/safe-uri.js +96 -0
  200. package/dist/lsp/safe-uri.js.map +1 -0
  201. package/dist/lsp/types.d.ts +70 -0
  202. package/dist/lsp/types.d.ts.map +1 -0
  203. package/dist/lsp/types.js +16 -0
  204. package/dist/lsp/types.js.map +1 -0
  205. package/dist/mcp/bridge.d.ts +57 -0
  206. package/dist/mcp/bridge.d.ts.map +1 -0
  207. package/dist/mcp/bridge.js +98 -0
  208. package/dist/mcp/bridge.js.map +1 -0
  209. package/dist/mcp/category.d.ts +22 -0
  210. package/dist/mcp/category.d.ts.map +1 -0
  211. package/dist/mcp/category.js +11 -0
  212. package/dist/mcp/category.js.map +1 -0
  213. package/dist/mcp/client.d.ts +228 -0
  214. package/dist/mcp/client.d.ts.map +1 -0
  215. package/dist/mcp/client.js +352 -0
  216. package/dist/mcp/client.js.map +1 -0
  217. package/dist/mcp/content.d.ts +86 -0
  218. package/dist/mcp/content.d.ts.map +1 -0
  219. package/dist/mcp/content.js +147 -0
  220. package/dist/mcp/content.js.map +1 -0
  221. package/dist/mcp/env.d.ts +19 -0
  222. package/dist/mcp/env.d.ts.map +1 -0
  223. package/dist/mcp/env.js +50 -0
  224. package/dist/mcp/env.js.map +1 -0
  225. package/dist/mcp/host.d.ts +199 -0
  226. package/dist/mcp/host.d.ts.map +1 -0
  227. package/dist/mcp/host.js +530 -0
  228. package/dist/mcp/host.js.map +1 -0
  229. package/dist/mcp/index.d.ts +18 -0
  230. package/dist/mcp/index.d.ts.map +1 -0
  231. package/dist/mcp/index.js +17 -0
  232. package/dist/mcp/index.js.map +1 -0
  233. package/dist/mcp/load-mcp-host.d.ts +32 -0
  234. package/dist/mcp/load-mcp-host.d.ts.map +1 -0
  235. package/dist/mcp/load-mcp-host.js +49 -0
  236. package/dist/mcp/load-mcp-host.js.map +1 -0
  237. package/dist/mcp/oauth/callback-server.d.ts +73 -0
  238. package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
  239. package/dist/mcp/oauth/callback-server.js +280 -0
  240. package/dist/mcp/oauth/callback-server.js.map +1 -0
  241. package/dist/mcp/oauth/config-hash.d.ts +24 -0
  242. package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
  243. package/dist/mcp/oauth/config-hash.js +55 -0
  244. package/dist/mcp/oauth/config-hash.js.map +1 -0
  245. package/dist/mcp/oauth/error-normalize.d.ts +39 -0
  246. package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
  247. package/dist/mcp/oauth/error-normalize.js +91 -0
  248. package/dist/mcp/oauth/error-normalize.js.map +1 -0
  249. package/dist/mcp/oauth/provider.d.ts +190 -0
  250. package/dist/mcp/oauth/provider.d.ts.map +1 -0
  251. package/dist/mcp/oauth/provider.js +305 -0
  252. package/dist/mcp/oauth/provider.js.map +1 -0
  253. package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
  254. package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
  255. package/dist/mcp/oauth/refresh-coalescer.js +77 -0
  256. package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
  257. package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
  258. package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
  259. package/dist/mcp/oauth/sdk-shapes.js +21 -0
  260. package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
  261. package/dist/mcp/parse.d.ts +28 -0
  262. package/dist/mcp/parse.d.ts.map +1 -0
  263. package/dist/mcp/parse.js +209 -0
  264. package/dist/mcp/parse.js.map +1 -0
  265. package/dist/mcp/prompts.d.ts +31 -0
  266. package/dist/mcp/prompts.d.ts.map +1 -0
  267. package/dist/mcp/prompts.js +71 -0
  268. package/dist/mcp/prompts.js.map +1 -0
  269. package/dist/mcp/redact.d.ts +62 -0
  270. package/dist/mcp/redact.d.ts.map +1 -0
  271. package/dist/mcp/redact.js +87 -0
  272. package/dist/mcp/redact.js.map +1 -0
  273. package/dist/mcp/resources.d.ts +70 -0
  274. package/dist/mcp/resources.d.ts.map +1 -0
  275. package/dist/mcp/resources.js +170 -0
  276. package/dist/mcp/resources.js.map +1 -0
  277. package/dist/mcp/types.d.ts +123 -0
  278. package/dist/mcp/types.d.ts.map +1 -0
  279. package/dist/mcp/types.js +2 -0
  280. package/dist/mcp/types.js.map +1 -0
  281. package/dist/memory/frontmatter.d.ts +18 -0
  282. package/dist/memory/frontmatter.d.ts.map +1 -0
  283. package/dist/memory/frontmatter.js +81 -0
  284. package/dist/memory/frontmatter.js.map +1 -0
  285. package/dist/memory/index.d.ts +5 -0
  286. package/dist/memory/index.d.ts.map +1 -0
  287. package/dist/memory/index.js +5 -0
  288. package/dist/memory/index.js.map +1 -0
  289. package/dist/memory/store.d.ts +44 -0
  290. package/dist/memory/store.d.ts.map +1 -0
  291. package/dist/memory/store.js +237 -0
  292. package/dist/memory/store.js.map +1 -0
  293. package/dist/memory/tools.d.ts +11 -0
  294. package/dist/memory/tools.d.ts.map +1 -0
  295. package/dist/memory/tools.js +159 -0
  296. package/dist/memory/tools.js.map +1 -0
  297. package/dist/memory/types.d.ts +32 -0
  298. package/dist/memory/types.d.ts.map +1 -0
  299. package/dist/memory/types.js +20 -0
  300. package/dist/memory/types.js.map +1 -0
  301. package/dist/plugin-api/index.d.ts +167 -0
  302. package/dist/plugin-api/index.d.ts.map +1 -0
  303. package/dist/plugin-api/index.js +151 -0
  304. package/dist/plugin-api/index.js.map +1 -0
  305. package/dist/plugin-logger/index.d.ts +21 -0
  306. package/dist/plugin-logger/index.d.ts.map +1 -0
  307. package/dist/plugin-logger/index.js +59 -0
  308. package/dist/plugin-logger/index.js.map +1 -0
  309. package/dist/session/index.d.ts +125 -0
  310. package/dist/session/index.d.ts.map +1 -0
  311. package/dist/session/index.js +202 -0
  312. package/dist/session/index.js.map +1 -0
  313. package/dist/tools/approval.d.ts +33 -0
  314. package/dist/tools/approval.d.ts.map +1 -0
  315. package/dist/tools/approval.js +53 -0
  316. package/dist/tools/approval.js.map +1 -0
  317. package/dist/tools/builtins/_shared.d.ts +94 -0
  318. package/dist/tools/builtins/_shared.d.ts.map +1 -0
  319. package/dist/tools/builtins/_shared.js +246 -0
  320. package/dist/tools/builtins/_shared.js.map +1 -0
  321. package/dist/tools/builtins/ask-user-question.d.ts +27 -0
  322. package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
  323. package/dist/tools/builtins/ask-user-question.js +191 -0
  324. package/dist/tools/builtins/ask-user-question.js.map +1 -0
  325. package/dist/tools/builtins/bash.d.ts +3 -0
  326. package/dist/tools/builtins/bash.d.ts.map +1 -0
  327. package/dist/tools/builtins/bash.js +158 -0
  328. package/dist/tools/builtins/bash.js.map +1 -0
  329. package/dist/tools/builtins/diff.d.ts +3 -0
  330. package/dist/tools/builtins/diff.d.ts.map +1 -0
  331. package/dist/tools/builtins/diff.js +83 -0
  332. package/dist/tools/builtins/diff.js.map +1 -0
  333. package/dist/tools/builtins/edit.d.ts +3 -0
  334. package/dist/tools/builtins/edit.d.ts.map +1 -0
  335. package/dist/tools/builtins/edit.js +40 -0
  336. package/dist/tools/builtins/edit.js.map +1 -0
  337. package/dist/tools/builtins/glob.d.ts +3 -0
  338. package/dist/tools/builtins/glob.d.ts.map +1 -0
  339. package/dist/tools/builtins/glob.js +37 -0
  340. package/dist/tools/builtins/glob.js.map +1 -0
  341. package/dist/tools/builtins/grep.d.ts +3 -0
  342. package/dist/tools/builtins/grep.d.ts.map +1 -0
  343. package/dist/tools/builtins/grep.js +81 -0
  344. package/dist/tools/builtins/grep.js.map +1 -0
  345. package/dist/tools/builtins/lsp.d.ts +3 -0
  346. package/dist/tools/builtins/lsp.d.ts.map +1 -0
  347. package/dist/tools/builtins/lsp.js +102 -0
  348. package/dist/tools/builtins/lsp.js.map +1 -0
  349. package/dist/tools/builtins/pty.d.ts +64 -0
  350. package/dist/tools/builtins/pty.d.ts.map +1 -0
  351. package/dist/tools/builtins/pty.js +536 -0
  352. package/dist/tools/builtins/pty.js.map +1 -0
  353. package/dist/tools/builtins/read.d.ts +3 -0
  354. package/dist/tools/builtins/read.d.ts.map +1 -0
  355. package/dist/tools/builtins/read.js +18 -0
  356. package/dist/tools/builtins/read.js.map +1 -0
  357. package/dist/tools/builtins/web-fetch.d.ts +4 -0
  358. package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
  359. package/dist/tools/builtins/web-fetch.js +353 -0
  360. package/dist/tools/builtins/web-fetch.js.map +1 -0
  361. package/dist/tools/builtins/write.d.ts +3 -0
  362. package/dist/tools/builtins/write.d.ts.map +1 -0
  363. package/dist/tools/builtins/write.js +48 -0
  364. package/dist/tools/builtins/write.js.map +1 -0
  365. package/dist/tools/builtins.d.ts +9 -0
  366. package/dist/tools/builtins.d.ts.map +1 -0
  367. package/dist/tools/builtins.js +29 -0
  368. package/dist/tools/builtins.js.map +1 -0
  369. package/dist/tools/diff.d.ts +18 -0
  370. package/dist/tools/diff.d.ts.map +1 -0
  371. package/dist/tools/diff.js +57 -0
  372. package/dist/tools/diff.js.map +1 -0
  373. package/dist/tools/index.d.ts +10 -0
  374. package/dist/tools/index.d.ts.map +1 -0
  375. package/dist/tools/index.js +9 -0
  376. package/dist/tools/index.js.map +1 -0
  377. package/dist/tools/permission.d.ts +120 -0
  378. package/dist/tools/permission.d.ts.map +1 -0
  379. package/dist/tools/permission.js +208 -0
  380. package/dist/tools/permission.js.map +1 -0
  381. package/dist/tools/registry.d.ts +12 -0
  382. package/dist/tools/registry.d.ts.map +1 -0
  383. package/dist/tools/registry.js +19 -0
  384. package/dist/tools/registry.js.map +1 -0
  385. package/dist/tools/types.d.ts +244 -0
  386. package/dist/tools/types.d.ts.map +1 -0
  387. package/dist/tools/types.js +15 -0
  388. package/dist/tools/types.js.map +1 -0
  389. package/package.json +109 -0
@@ -0,0 +1,767 @@
1
+ import { AppError, ErrorCode, ModelSelection, createId, runWithCwd, } from "@chances-ai/runtime";
2
+ import { refreshActiveMarker } from "./worktree/index.js";
3
+ import { classifyProviderError, defaultRetryConfig, estimateCost, } from "../ai/index.js";
4
+ import { ASK_USER_QUESTION_TOOL_NAME, READONLY_CATEGORIES } from "../tools/index.js";
5
+ /**
6
+ * (3.5 — codex Round-1 SHOULD-FIX #4) Anthropic-only overflow
7
+ * detection. Three patterns from pi's overflow catalogue
8
+ * (`pi/packages/ai/src/utils/overflow.ts:11`). The other 10
9
+ * providers we ship stay deferred until real-world telemetry shows
10
+ * a hit; their error shapes are less stable and a wrong regex would
11
+ * surface as silent overflow loops.
12
+ */
13
+ function isAnthropicOverflowError(adapterId, message) {
14
+ if (adapterId !== "anthropic")
15
+ return false;
16
+ return (/prompt is too long/i.test(message) ||
17
+ /request_too_large/i.test(message) ||
18
+ /maximum.*context.*length/i.test(message));
19
+ }
20
+ /** Engine default when no caller-supplied or config-supplied value applies. */
21
+ export const DEFAULT_MAX_TURNS = 12;
22
+ /** Default base prompt the engine uses when no `systemBaseOverride` is set.
23
+ * Exported so tests can assert "is this the default or an agent override?" and
24
+ * so the doc + plugin authors can read the exact text. */
25
+ export const DEFAULT_BASE_PROMPT = [
26
+ "You are chances, a terminal coding agent. Use the provided tools to inspect and modify the workspace. Be concise.",
27
+ "",
28
+ "Persistent memory:",
29
+ "- Use memory_save to remember facts that will still matter in *future* conversations: user role/preferences (type: user), corrections you should not need twice (type: feedback), ongoing project context not derivable from code (type: project), or pointers to external systems (type: reference).",
30
+ "- Use scope 'global' for cross-project facts about the user; use 'project' for this-repo context.",
31
+ "- Before saving, check the indexes below (or call memory_list) — if a related entry already exists, update it rather than creating a duplicate. Duplicates waste context budget.",
32
+ "- Use memory_delete to remove outdated entries.",
33
+ "- Do NOT save: code patterns, file paths, git history, debugging recipes, or task-specific scratch — those are derivable from the repo.",
34
+ ].join("\n");
35
+ /**
36
+ * Mediator over ai/session/tools/memory. Depends only on interfaces (ports), so
37
+ * any ProviderAdapter or Tool plugs in unchanged. Communicates outward purely by
38
+ * emitting events on the bus — it never imports the TUI.
39
+ */
40
+ export class AgentEngine {
41
+ opts;
42
+ selection;
43
+ constructor(opts) {
44
+ this.opts = opts;
45
+ // Prefer the caller-supplied selection so they retain a handle for
46
+ // mid-session mutation; fall back to the deprecated scalars for back-compat.
47
+ this.selection =
48
+ opts.selection ??
49
+ new ModelSelection({ provider: opts.preferredProvider, model: opts.preferredModel });
50
+ }
51
+ /**
52
+ * Exposes the mutable selection so callers — typically slash commands like
53
+ * `/model` — can update it between turns. Returning the live instance (not a
54
+ * copy) is intentional: writes must propagate back.
55
+ */
56
+ getSelection() {
57
+ return this.selection;
58
+ }
59
+ /** Bus-emit wrapper. Three responsibilities (3.4):
60
+ * 1. Suppress lifecycle frames (`turn:*`, `error`) when the engine is a
61
+ * child (`suppressLifecycleEvents=true`). Codex Round-1 MUST-FIX #2.
62
+ * 2. Stamp `agentId/agentName` on tagged event variants when an
63
+ * `agentContext` was supplied. Skipped silently when undefined.
64
+ * 3. Forward to `bus.emit` otherwise — fully transparent for parent engines.
65
+ */
66
+ emit(event) {
67
+ if (this.opts.suppressLifecycleEvents &&
68
+ (event.type === "turn:start" || event.type === "turn:end" || event.type === "error")) {
69
+ return;
70
+ }
71
+ const ctx = this.opts.agentContext;
72
+ if (ctx) {
73
+ switch (event.type) {
74
+ case "assistant:delta":
75
+ case "assistant:message":
76
+ case "assistant:reset":
77
+ case "tool:call":
78
+ case "tool:permission":
79
+ case "tool:result":
80
+ case "usage":
81
+ // Conditional spread on agentId so sync subagents (which have
82
+ // no registry-issued id — see `agentContext` doc above) emit
83
+ // events without an `agentId: undefined` own-property. Cleaner
84
+ // for subscribers like the 3.6 OTel exporter that introspect
85
+ // keys to decide whether to stamp an attribute.
86
+ this.opts.bus.emit({
87
+ ...event,
88
+ ...(ctx.agentId !== undefined ? { agentId: ctx.agentId } : {}),
89
+ agentName: ctx.agentName,
90
+ });
91
+ return;
92
+ default:
93
+ break;
94
+ }
95
+ }
96
+ this.opts.bus.emit(event);
97
+ }
98
+ async runTurn(prompt, token, opts = {}) {
99
+ // (4.1) Isolated subagents: every descendant `await` inside this
100
+ // turn sees `worktreeCwd` as its ambient cwd. Tools resolve
101
+ // `safePath` against it, bash spawns inside it. The outer body
102
+ // delegates straight into the existing implementation; nothing
103
+ // about the rest of `runTurn` needs to know about isolation
104
+ // beyond the `worktreeCwd` peek in `runTool`.
105
+ if (this.opts.worktreeCwd !== undefined) {
106
+ const cwd = this.opts.worktreeCwd;
107
+ void refreshActiveMarker(cwd).catch(() => {
108
+ /* heartbeat is best-effort; failure means the marker was
109
+ * already removed (e.g. parent torn down the worktree) which
110
+ * we surface through the regular tool-error path on the next
111
+ * fs call. */
112
+ });
113
+ return runWithCwd(cwd, () => this.runTurnImpl(prompt, token, opts.expandMentions !== false));
114
+ }
115
+ return this.runTurnImpl(prompt, token, opts.expandMentions !== false);
116
+ }
117
+ async runTurnImpl(prompt, token, expandMentions) {
118
+ const { router, tools, gate, session, plugins, backgroundTasks } = this.opts;
119
+ const turnId = createId("turn");
120
+ // (3.6) Carry the active session id on `turn:start` so the OTel
121
+ // exporter can stamp `chances.gen_ai.session.id` correctly across
122
+ // `/resume` (which swaps SessionManager instances mid-process).
123
+ // Subscribers that don't care about session correlation ignore the
124
+ // field (it's optional in the event union).
125
+ this.emit({ type: "turn:start", turnId, prompt, sessionId: session.id });
126
+ await plugins?.runHook("beforePrompt", { prompt });
127
+ const toolDefs = tools.list().map((t) => ({
128
+ name: t.name,
129
+ description: t.description,
130
+ parameters: t.parameters,
131
+ }));
132
+ const system = this.composeSystem();
133
+ // 3.4: peek pending background task notifications BEFORE composing the
134
+ // turn messages. Each notification becomes part of a single synthetic
135
+ // user-role message prepended to the user's actual prompt so the model
136
+ // sees them at the start of this turn. FIFO ordering. Notifications
137
+ // queued during this turn's stream go to the NEXT turn (deliberate:
138
+ // never inject mid-stream).
139
+ //
140
+ // Codex Round-2 MUST-FIX #3: we PEEK rather than DRAIN here. If the
141
+ // turn cancels / throws before `session.appendTurn` below, the queue
142
+ // is preserved and the next turn re-delivers — the model never sees
143
+ // the notification twice because the prior cancelled turn was not
144
+ // persisted to `session.messages()`. Acknowledgement (queue removal)
145
+ // happens immediately after `session.appendTurn(turnMessages)`
146
+ // succeeds.
147
+ const notifications = backgroundTasks?.peekPendingNotifications() ?? [];
148
+ const notificationIds = notifications.map((n) => n.taskId);
149
+ const turnMessages = [];
150
+ if (notifications.length > 0) {
151
+ const xml = notifications.map(renderTaskNotificationXml).join("\n");
152
+ turnMessages.push({
153
+ role: "user",
154
+ content: [{ type: "text", text: xml }],
155
+ });
156
+ }
157
+ // (5.4) Expand `@<server>:<uri>` mentions in directly-typed user input into
158
+ // a synthetic resource message before the prompt. Skipped for submitted /
159
+ // prompt-command text (`expandMentions === false`, codex R1 M3 — that text
160
+ // can be untrusted MCP-server output and must not drive ungated reads). The
161
+ // resolver only resolves exact cached resources (R1 S2) and never throws on
162
+ // a bad ref. Cancellation still propagates (it rethrows AppError(Cancelled)).
163
+ if (expandMentions && this.opts.resolveMcpMentions) {
164
+ const resolved = await this.opts.resolveMcpMentions(prompt, token.signal);
165
+ const xml = renderMcpResourcesXml(resolved);
166
+ if (xml)
167
+ turnMessages.push({ role: "user", content: [{ type: "text", text: xml }] });
168
+ }
169
+ turnMessages.push({ role: "user", content: [{ type: "text", text: prompt }] });
170
+ const result = { text: "", inputTokens: 0, outputTokens: 0, costUsd: 0 };
171
+ const maxTurns = this.opts.maxTurns ?? this.opts.maxIterations ?? DEFAULT_MAX_TURNS;
172
+ let resolved = false;
173
+ // (3.5 — codex Round-1 MUST-FIX #1) `result.inputTokens` aggregates
174
+ // every `usage` event across the multi-step tool loop. The compactor's
175
+ // threshold check needs the LAST stream's input only — that's what
176
+ // the provider will count for the NEXT request, plus the new user
177
+ // prompt. Tracked separately here; emitted via `usage:turn`.
178
+ let lastRequestInputTokens = 0;
179
+ // (3.5 — codex Round-1 SHOULD-FIX #4) Per-turn flag. Anthropic
180
+ // overflow recovery fires AT MOST ONCE per turn — a second 413 after
181
+ // we already compacted is an actual ceiling we can't paper over.
182
+ let recoveredFromOverflow = false;
183
+ // (3.5) Tracked at the outer scope so the post-turn compaction check
184
+ // can read `route.model`. The for-loop reuses the variable across
185
+ // iterations; we just need the most recent value to query the model
186
+ // descriptor for `contextWindow`.
187
+ let lastRoute;
188
+ for (let i = 0; i < maxTurns; i++) {
189
+ token.throwIfCancelled();
190
+ // Re-read selection per turn so a `/model` switch between turns lands on
191
+ // the next request without rebuilding the engine.
192
+ const choice = this.selection.get();
193
+ const route = router.pick({
194
+ preferredModel: choice.model,
195
+ preferredProvider: choice.provider,
196
+ needsTools: toolDefs.length > 0,
197
+ });
198
+ lastRoute = route;
199
+ const retry = this.opts.retry ?? defaultRetryConfig;
200
+ let textBuffer = "";
201
+ let calls = [];
202
+ let attempt = 0;
203
+ while (true) {
204
+ token.throwIfCancelled();
205
+ textBuffer = "";
206
+ calls = [];
207
+ // (3.5) Reset per attempt — only the LAST successful stream's
208
+ // last `usage.inputTokens` carries forward into the post-turn
209
+ // compactor check.
210
+ let attemptLastInputTokens = 0;
211
+ // (6.5b review) Stage usage in attempt-local accumulators instead of
212
+ // folding it straight into the turn-level `result`. A retryable
213
+ // mid-stream error (e.g. ECONNRESET after a partial stream) discards
214
+ // the attempt and restreams; folding here would double-count tokens
215
+ // and double-emit `usage`. We only merge into `result` + emit once
216
+ // the stream completes (`streamError === null`, below).
217
+ let attemptInputTokens = 0;
218
+ let attemptOutputTokens = 0;
219
+ let attemptCostUsd = 0;
220
+ let streamError = null;
221
+ const stream = route.adapter.stream({ model: route.model.id, system, messages: [...session.messages(), ...turnMessages], tools: toolDefs }, token.signal);
222
+ for await (const event of stream) {
223
+ // Enforce cancellation per-event so a provider that ignores or
224
+ // queues past the AbortSignal can't keep dripping text/tool-calls
225
+ // into a turn the user already abandoned. Particularly important
226
+ // for subagents: the parent's abort must stop the child instantly,
227
+ // not wait until the child stream naturally ends.
228
+ token.throwIfCancelled();
229
+ switch (event.type) {
230
+ case "text-delta":
231
+ textBuffer += event.text;
232
+ this.emit({ type: "assistant:delta", turnId, text: event.text });
233
+ break;
234
+ case "tool-call":
235
+ // Defer the `tool:call` bus emit until the execution loop
236
+ // below — pairs each emit atomically with its matching
237
+ // `tool:result`. Emitting here would leave orphan call frames
238
+ // on the bus whenever the turn aborts between stream-end and
239
+ // tool execution (Ctrl-C) or a retry attempt discards the
240
+ // collected calls and tries again on attempt N+1.
241
+ calls.push(event.call);
242
+ break;
243
+ case "usage": {
244
+ const costUsd = estimateCost(route.model, event.usage);
245
+ // (6.5b review) Accumulate into attempt-local totals; the merge
246
+ // into `result` + the `usage` emit happen once the stream
247
+ // succeeds, so a discarded retry attempt can't double-count.
248
+ attemptInputTokens += event.usage.inputTokens;
249
+ attemptOutputTokens += event.usage.outputTokens;
250
+ attemptCostUsd += costUsd;
251
+ // (3.5) Track most recent stream's last input count for the
252
+ // post-turn compaction threshold check. NOT the aggregate.
253
+ attemptLastInputTokens = event.usage.inputTokens;
254
+ break;
255
+ }
256
+ case "error":
257
+ // Defer the bus emit until after cancellation check — if the
258
+ // user just hit Ctrl-C, the SDK's abort path surfaces as a
259
+ // stream error and we shouldn't shout "PROVIDER" at them.
260
+ streamError = event.message;
261
+ break;
262
+ case "done":
263
+ break;
264
+ }
265
+ if (streamError !== null)
266
+ break;
267
+ }
268
+ if (streamError === null) {
269
+ // Stream completed successfully — NOW fold this attempt's usage
270
+ // into the turn-level `result` and emit the (aggregated) `usage`
271
+ // frame. Deferred to here so a discarded retry attempt's partial
272
+ // usage never double-counts (6.5b review).
273
+ result.inputTokens += attemptInputTokens;
274
+ result.outputTokens += attemptOutputTokens;
275
+ result.costUsd += attemptCostUsd;
276
+ if (attemptInputTokens > 0 || attemptOutputTokens > 0 || attemptCostUsd > 0) {
277
+ this.emit({
278
+ type: "usage",
279
+ model: route.model.id,
280
+ inputTokens: attemptInputTokens,
281
+ outputTokens: attemptOutputTokens,
282
+ costUsd: attemptCostUsd,
283
+ });
284
+ }
285
+ // Persist this attempt's last input-token count for the post-turn
286
+ // compaction check.
287
+ lastRequestInputTokens = attemptLastInputTokens;
288
+ break;
289
+ }
290
+ // If the abort signal fired during the stream, the error we just
291
+ // captured is the SDK reacting to the cancellation — treat it as
292
+ // Cancelled rather than misclassifying as a provider error.
293
+ token.throwIfCancelled();
294
+ const decision = classifyProviderError(streamError);
295
+ const terminal = !decision.retryable || attempt >= retry.delaysMs.length;
296
+ if (terminal) {
297
+ // (3.5 — codex Round-1 SHOULD-FIX #4) Anthropic-only reactive
298
+ // overflow recovery. Catches the 413 BEFORE the terminal throw,
299
+ // runs compaction with `reason: "overflow"` (bypasses circuit
300
+ // breaker), and retries the stream once with the compacted
301
+ // history. Wider 10-provider catalogue stays deferred until
302
+ // telemetry shows a real-world miss.
303
+ if (this.opts.compactor &&
304
+ !recoveredFromOverflow &&
305
+ isAnthropicOverflowError(route.adapter.id, streamError)) {
306
+ recoveredFromOverflow = true;
307
+ try {
308
+ const recovery = await this.opts.compactor.compact("overflow", token.signal);
309
+ if (recovery.ok) {
310
+ // Reset the attempt counter so we get the full retry
311
+ // budget against the now-smaller request, AND clear the
312
+ // accumulated message buffer that the failed attempt
313
+ // wrote into `turnMessages`. The retry rebuilds from
314
+ // `session.messages()` (which now reflects compaction)
315
+ // plus this turn's prepended user/notification messages.
316
+ // (6.5b follow-up) Mirror the normal retry path's partial-undo:
317
+ // if this attempt streamed partial text before the overflow, drop
318
+ // it so the post-compaction restream doesn't append onto a stale
319
+ // partial. A 413 usually precedes any delta, so this is typically
320
+ // a no-op — emitted only when text was actually shown.
321
+ if (textBuffer.length > 0) {
322
+ this.emit({ type: "assistant:reset", turnId });
323
+ }
324
+ attempt = 0;
325
+ continue;
326
+ }
327
+ }
328
+ catch (e) {
329
+ // Compactor.compact never throws by contract, but be defensive:
330
+ // a malformed Compactor implementation shouldn't break the
331
+ // original error path.
332
+ this.emit({
333
+ type: "log",
334
+ level: "warn",
335
+ message: `overflow compaction unexpectedly threw: ${e.message ?? e}`,
336
+ });
337
+ }
338
+ }
339
+ // Emit the bus `error` ONLY when we're about to throw. Emitting on
340
+ // every retry attempt would cause `runPrompt`'s `lastError`
341
+ // listener to record a transient failure as the turn's exit code
342
+ // even after a later attempt succeeded (codex re-review finding).
343
+ // Subagent engines suppress this emit — see `suppressTerminalErrors`.
344
+ if (!this.opts.suppressTerminalErrors) {
345
+ this.emit({ type: "error", code: "PROVIDER", message: streamError });
346
+ }
347
+ throw new AppError(ErrorCode.Provider, `Provider error (${decision.reason}): ${streamError}`);
348
+ }
349
+ const delayMs = retry.delaysMs[attempt] ?? 0;
350
+ this.emit({
351
+ type: "log",
352
+ level: "warn",
353
+ message: `provider stream errored (${decision.reason}); retry ${attempt + 1}/${retry.delaysMs.length} after ${delayMs}ms; original: ${streamError}`,
354
+ });
355
+ // (6.5b review) If this attempt already streamed partial assistant text
356
+ // to the bus, the upcoming restream would APPEND a fresh copy on top
357
+ // (consumers don't replace) — duplicating it on screen. Tell consumers
358
+ // to drop the in-flight partial first. `usage`/`tool-call` are deferred
359
+ // (not yet on the bus), so only `textBuffer` needs undoing.
360
+ if (textBuffer.length > 0) {
361
+ this.emit({ type: "assistant:reset", turnId });
362
+ }
363
+ attempt += 1;
364
+ await sleepCancellable(delayMs, token);
365
+ }
366
+ if (calls.length === 0) {
367
+ const content = [{ type: "text", text: textBuffer }];
368
+ turnMessages.push({ role: "assistant", content });
369
+ result.text = textBuffer;
370
+ this.emit({ type: "assistant:message", turnId, text: textBuffer });
371
+ await safeRunHook(plugins, "afterResponse", { text: textBuffer }, this.opts.bus);
372
+ resolved = true;
373
+ break;
374
+ }
375
+ // Record the assistant message that requested the tools.
376
+ const assistantContent = [];
377
+ if (textBuffer)
378
+ assistantContent.push({ type: "text", text: textBuffer });
379
+ for (const call of calls) {
380
+ assistantContent.push({ type: "tool-call", callId: call.callId, name: call.name, args: call.args });
381
+ }
382
+ turnMessages.push({ role: "assistant", content: assistantContent });
383
+ // Execute each tool through the permission gate, then feed results back.
384
+ for (const call of calls) {
385
+ // Check cancellation before each tool — a long batch of tool-calls
386
+ // from one model turn shouldn't keep running after the user aborts.
387
+ token.throwIfCancelled();
388
+ // Emit `tool:call` here (not in the stream loop) so each call is
389
+ // paired with its `tool:result` from runTool — keeps the bus
390
+ // observably balanced for subscribers (TUI, NDJSON, telemetry).
391
+ this.emit({ type: "tool:call", callId: call.callId, name: call.name, args: call.args });
392
+ const outcome = await this.runTool(call, token);
393
+ turnMessages.push({
394
+ role: "tool",
395
+ content: [{ type: "tool-result", callId: call.callId, name: call.name, output: outcome.output, ok: outcome.ok }],
396
+ });
397
+ // Round 3 codex SHOULD-FIX: check cancellation AFTER each tool
398
+ // result too. A tool that catches cancellation internally and
399
+ // returns `ok:false` (e.g. `bash` returning `(cancelled)`) does
400
+ // NOT re-throw, so without this check the loop would continue
401
+ // to the next turn and could exhaust `maxTurns`, surfacing as
402
+ // a misleading `PROVIDER: Reached maximum number of turns`
403
+ // instead of the user's actual `Cancelled` intent.
404
+ token.throwIfCancelled();
405
+ }
406
+ }
407
+ session.appendTurn(turnMessages);
408
+ // Codex Round-2 MUST-FIX #3: ack notifications AFTER appendTurn so a
409
+ // cancellation between peek and persist leaves the queue intact.
410
+ if (notificationIds.length > 0) {
411
+ backgroundTasks?.acknowledgeNotifications(notificationIds);
412
+ }
413
+ // (3.5) Per-turn aggregate event. Lifecycle suppression honored —
414
+ // child engines (with `suppressLifecycleEvents`) skip this too.
415
+ // `lastRequestInputTokens` feeds the post-turn compaction check
416
+ // (codex Round-1 MUST-FIX #1) AND 3.6 OTel one-span-per-turn.
417
+ if (!this.opts.suppressLifecycleEvents) {
418
+ this.opts.bus.emit({
419
+ type: "usage:turn",
420
+ turnId,
421
+ inputTokens: result.inputTokens,
422
+ outputTokens: result.outputTokens,
423
+ costUsd: result.costUsd,
424
+ lastRequestInputTokens,
425
+ });
426
+ }
427
+ // (3.5 — codex Round-1 MUST-FIX #4) Threshold-triggered compaction.
428
+ // Runs BEFORE `turn:end` so TUI subscribers don't see the turn as
429
+ // "over" while session is still mutating. Compaction emits its own
430
+ // `compaction:start` / `compaction:end` frames inside this await.
431
+ // The compactor itself swallows all failures into ok:false (never
432
+ // throws by contract); `Cancelled` propagates as `cancelled` reason.
433
+ if (this.opts.compactor && resolved && lastRoute) {
434
+ const should = this.opts.compactor.shouldCompact({
435
+ lastRequestInputTokens,
436
+ model: lastRoute.model,
437
+ });
438
+ if (should) {
439
+ await this.opts.compactor.compact("threshold", token.signal);
440
+ // (codex Round-2 MUST-FIX #2) The compactor swallows all
441
+ // failures into `ok:false`, including a `cancelled` reason
442
+ // when `signal.aborted` fired during summarization. Without
443
+ // a `throwIfCancelled` here the cancelled compaction would
444
+ // be invisible — the engine would emit `turn:end` and
445
+ // return normally, defeating the user's Ctrl-C. Re-checking
446
+ // the token after the await propagates Cancelled to the
447
+ // caller exactly like an in-stream cancellation would.
448
+ token.throwIfCancelled();
449
+ }
450
+ else {
451
+ // Reset the compactor's "compacted-recently" latch so the
452
+ // NEXT turn's threshold check can re-evaluate.
453
+ this.opts.compactor.noteNonCompactionTurn();
454
+ }
455
+ }
456
+ this.emit({ type: "turn:end", turnId });
457
+ if (!resolved) {
458
+ // Loop exhausted the turn budget without the model returning a final
459
+ // answer. Match claude-code's terminal-error pattern (`QueryEngine.ts:914`)
460
+ // — emit a bus error and throw so the caller sees a concrete signal
461
+ // instead of an empty result. The turn is still persisted above so
462
+ // `/resume` can pick up the partial work.
463
+ // Subagent engines suppress this emit — see `suppressTerminalErrors`.
464
+ const message = `Reached maximum number of turns (${maxTurns})`;
465
+ if (!this.opts.suppressTerminalErrors) {
466
+ this.emit({ type: "error", code: "PROVIDER", message });
467
+ }
468
+ throw new AppError(ErrorCode.Provider, message);
469
+ }
470
+ return result;
471
+ }
472
+ async runTool(call, token) {
473
+ const { bus, tools, gate, plugins, workspaceRoot } = this.opts;
474
+ const tool = tools.get(call.name);
475
+ if (!tool) {
476
+ const output = `Unknown tool: ${call.name}`;
477
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
478
+ return { ok: false, output };
479
+ }
480
+ // (4.1) Inside an isolated subagent's `runTurn`, `runWithCwd` has
481
+ // pushed `worktreeCwd` onto AsyncLocalStorage. Both `workspaceRoot`
482
+ // and `cwd` must flip together so `safePath` resolves against the
483
+ // worktree tree, not the parent's. When no override is active, the
484
+ // value is `undefined` and we fall back to the parent's
485
+ // `workspaceRoot` (unchanged 3.x behaviour).
486
+ //
487
+ // `parentWorkspaceRoot` (Round-2 SHOULD-FIX #4) carries the
488
+ // non-isolated workspace through so artifact persistence paths
489
+ // (`bash` oversized-output dump, etc.) land in the parent's
490
+ // `.chances/` rather than the worktree's — otherwise a
491
+ // clean-removed worktree would invalidate the path the tool just
492
+ // surfaced to the model.
493
+ const effectiveRoot = this.opts.worktreeCwd ?? workspaceRoot;
494
+ const ctx = {
495
+ workspaceRoot: effectiveRoot,
496
+ cwd: effectiveRoot,
497
+ signal: token.signal,
498
+ parentWorkspaceRoot: workspaceRoot,
499
+ lsp: this.opts.lsp, // (4.2) codex Round-1 MUST-FIX #1
500
+ callId: call.callId, // (5.7) artifact correlation — see ToolContext.callId
501
+ };
502
+ try {
503
+ await plugins?.runHook("beforeToolCall", { name: call.name, args: call.args });
504
+ // (5.3 codex Round-1 MUST-FIX #1) Session mode-block (plan's read-only
505
+ // floor) is enforced at the CATEGORY level here, BEFORE the per-op
506
+ // `permission()` hook. This is the only place that catches an op which
507
+ // returns `null` to bypass the gate (e.g. `pty.read`): without this,
508
+ // plan mode could be silently bypassed by such ops. Auto-approve /
509
+ // deny-floor / prompt all stay inside `gate.evaluate()` below.
510
+ const modeBlock = gate.blockByMode(tool.category);
511
+ if (modeBlock) {
512
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: modeBlock.reason });
513
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output: modeBlock.reason }, bus);
514
+ return { ok: false, output: modeBlock.reason };
515
+ }
516
+ // (5.1 codex Round-2 MUST-FIX #1) — per-op permission hook. When
517
+ // a tool implements `permission()`, it can:
518
+ // - return `null` to bypass the gate entirely (e.g. `pty.read` /
519
+ // `pty.resize` / `pty.list` — already approved at session
520
+ // start),
521
+ // - return a `PermissionRequest` whose `cacheKey` scopes the
522
+ // positive-decision cache below `tool.name` (e.g.
523
+ // `pty.kill.SIGTERM/<sid>` so approving SIGTERM doesn't
524
+ // pre-approve SIGKILL).
525
+ // When unimplemented (the legacy path used by all 10 v5.0
526
+ // builtins), fall back to the original {name, category,
527
+ // summarize(args)} shape.
528
+ const decision = tool.permission ? tool.permission(call.args, ctx) : undefined;
529
+ if (decision === null) {
530
+ // Explicit bypass — skip the `tool:permission` event AND the
531
+ // gate. Subscribers (TUI, OTel) still see `tool:call` →
532
+ // `tool:result` for the op; only the permission frame is
533
+ // suppressed because there isn't one.
534
+ const outcome = await tool.execute(call.args, ctx);
535
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
536
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
537
+ return outcome;
538
+ }
539
+ // (5.10b) QUESTION channel — fail-closed. A tool's `permission()` may emit
540
+ // a `question` request (the `ask_user_question` tool does), which skips
541
+ // `gate.evaluate()`'s allow/deny precedence. That bypass is safe ONLY for
542
+ // the dedicated read-only asker; any OTHER tool reaching here with a
543
+ // question request would be escaping authorization, so we refuse WITHOUT
544
+ // executing (codex R1 MUST-FIX #1). Double-guard: exact name + read-only
545
+ // category.
546
+ if (decision?.kind === "question") {
547
+ if (tool.name !== ASK_USER_QUESTION_TOOL_NAME || !READONLY_CATEGORIES.has(tool.category)) {
548
+ const output = "refused: the question channel is reserved for the read-only ask tool";
549
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
550
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
551
+ return { ok: false, output };
552
+ }
553
+ this.emit({
554
+ type: "tool:permission",
555
+ callId: call.callId,
556
+ name: call.name,
557
+ summary: `${decision.questions.length} question(s)`,
558
+ });
559
+ // The answers come back through the resolver and ride into `execute` on
560
+ // the ctx (never serialized by the engine — the tool owns the wording).
561
+ const questionDecision = await gate.ask({ ...decision, callId: call.callId });
562
+ const outcome = await tool.execute(call.args, { ...ctx, questionDecision });
563
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
564
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
565
+ return outcome;
566
+ }
567
+ const summary = decision?.summary ?? tool.summarize(call.args, ctx);
568
+ this.emit({ type: "tool:permission", callId: call.callId, name: call.name, summary });
569
+ // (5.2 codex Round-1 MUST-FIX #3) Stamp the call id onto the request
570
+ // so an out-of-process resolver (RPC / ACP host) can ship a
571
+ // self-contained `request_permission` frame the client joins to the
572
+ // preceding `tool_call.callId`. In-process resolvers ignore it.
573
+ const req = {
574
+ ...(decision ?? {
575
+ name: tool.name,
576
+ category: tool.category,
577
+ summary,
578
+ args: call.args,
579
+ }),
580
+ callId: call.callId,
581
+ };
582
+ // (5.3) `evaluate()` (not `check()`) so a denial can carry the user's
583
+ // deny-with-feedback text or a policy/mode reason into the tool result —
584
+ // the model then adapts instead of blindly retrying.
585
+ const evaluation = await gate.evaluate(req);
586
+ if (!evaluation.allow) {
587
+ const output = evaluation.feedback
588
+ ? `Permission denied by user. User feedback: ${evaluation.feedback}`
589
+ : (evaluation.reason ?? "Permission denied by policy/user");
590
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
591
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
592
+ return { ok: false, output };
593
+ }
594
+ const outcome = await tool.execute(call.args, ctx);
595
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: outcome.ok, output: outcome.output });
596
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: outcome.ok, output: outcome.output }, bus);
597
+ return outcome;
598
+ }
599
+ catch (e) {
600
+ // Cancellation must terminate the whole turn — never swallow it as a tool
601
+ // error. But we DO emit a synthetic `tool:result` so the bus stays paired
602
+ // (every `tool:call` event has its matching result, even on Ctrl-C).
603
+ if (e instanceof AppError && e.code === ErrorCode.Cancelled) {
604
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: "Cancelled" });
605
+ throw e;
606
+ }
607
+ if (token.isCancelled) {
608
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output: "Cancelled" });
609
+ throw new AppError(ErrorCode.Cancelled, "Operation cancelled");
610
+ }
611
+ const output = formatToolError(e);
612
+ this.emit({ type: "tool:result", callId: call.callId, name: call.name, ok: false, output });
613
+ this.emit({ type: "log", level: "warn", message: `tool ${call.name} failed: ${output}` });
614
+ await safeRunHook(plugins, "afterToolCall", { name: call.name, ok: false, output }, bus);
615
+ return { ok: false, output };
616
+ }
617
+ }
618
+ composeSystem() {
619
+ const base = this.opts.systemBaseOverride ?? DEFAULT_BASE_PROMPT;
620
+ const memoryContext = this.opts.memory?.asSystemContext();
621
+ const isolation = this.opts.worktreeCwd ? ISOLATED_WORKTREE_NOTICE : undefined;
622
+ // (5.3) Plan mode is read-only — without this notice the model would keep
623
+ // attempting mutations and hitting denials. Read per-turn so toggling plan
624
+ // on/off (Shift+Tab) lands on the next turn, mirroring model selection.
625
+ const plan = this.opts.getApprovalMode?.() === "plan" ? PLAN_MODE_NOTICE : undefined;
626
+ const parts = [base];
627
+ if (memoryContext)
628
+ parts.push(memoryContext);
629
+ if (isolation)
630
+ parts.push(isolation);
631
+ if (plan)
632
+ parts.push(plan);
633
+ return parts.join("\n\n");
634
+ }
635
+ }
636
+ /** (5.3) Inserted into the system prompt while the session approval mode is
637
+ * `plan`. Tells the model it is read-only and should produce a plan instead of
638
+ * editing — the same posture claude-code's plan mode enforces. */
639
+ export const PLAN_MODE_NOTICE = [
640
+ "Approval mode: PLAN (read-only).",
641
+ "- You may read files, search, and inspect, but file writes, shell commands, MCP tools, and network fetches are BLOCKED and will be denied.",
642
+ "- Do not attempt mutating tools. Investigate, then present a concise, actionable plan for the user to review.",
643
+ "- The user exits plan mode with Shift+Tab (or /approval) when ready to execute.",
644
+ ].join("\n");
645
+ /** (4.1) Inserted at the END of the child engine's system prompt when
646
+ * `worktreeCwd` is set. Tells the model two load-bearing facts: the
647
+ * worktree starts from HEAD (parent's uncommitted state is invisible),
648
+ * and writes are sandboxed (parent sees them as a diff after the task
649
+ * completes, not live). Round-1 SHOULD-FIX #2 wording. */
650
+ export const ISOLATED_WORKTREE_NOTICE = [
651
+ "Isolation: you are running in an isolated git worktree.",
652
+ "- This worktree starts from the parent agent's current HEAD; any uncommitted edits in the parent's working tree are NOT visible here.",
653
+ "- Files you write/edit/bash-modify here are sandboxed and not visible to the parent until your task completes; the parent will see your final diff once you return.",
654
+ ].join("\n");
655
+ function formatToolError(e) {
656
+ if (e instanceof AppError)
657
+ return `${e.code}: ${e.message}`;
658
+ if (e instanceof Error)
659
+ return e.message || e.name || "Error";
660
+ return String(e);
661
+ }
662
+ /** Maximum bytes of `result` text included verbatim in an injected
663
+ * notification XML block. Beyond this, we truncate and tell the model the
664
+ * full result is still in the registry by id. Counted in UTF-16 code
665
+ * units rather than bytes — close enough for a 16 KiB-ish budget, and
666
+ * cheap to compute without a separate encoder pass. */
667
+ const NOTIFICATION_RESULT_BUDGET = 16 * 1024;
668
+ /** Control characters to strip from any text we inject. Keeps the XML
669
+ * payload terminal-safe and matches the C0+DEL strip used in
670
+ * `@chances-ai/agents`'s `renderCatalogForToolDescription`. */
671
+ const NOTIFICATION_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
672
+ /** Renders one `<task-notification>` XML block, fully entity-escaped so
673
+ * `result`/`summary` text containing `<`, `>`, `&`, `"`, `'` can't break
674
+ * the surrounding tag structure or impersonate a different role.
675
+ *
676
+ * This is the format the model receives at the top of its next turn
677
+ * after a background subagent completes. The shape matches the one we
678
+ * (chances-cli's own host) observe receiving from background research
679
+ * subagents — claude-code's `<task_notification>` convention, with our
680
+ * hyphen-separated tag style.
681
+ */
682
+ export function renderTaskNotificationXml(n) {
683
+ let result = n.result.replace(NOTIFICATION_CONTROL_RE, "");
684
+ let resultSuffix = "";
685
+ if (result.length > NOTIFICATION_RESULT_BUDGET) {
686
+ result = result.slice(0, NOTIFICATION_RESULT_BUDGET);
687
+ resultSuffix = `\n[…truncated, full result still in registry at task_id=${n.taskId}]`;
688
+ }
689
+ const summary = n.summary.replace(NOTIFICATION_CONTROL_RE, "");
690
+ return [
691
+ "<task-notification>",
692
+ ` <task-id>${xmlEscape(n.taskId)}</task-id>`,
693
+ ` <name>${xmlEscape(n.name)}</name>`,
694
+ ` <status>${xmlEscape(n.status)}</status>`,
695
+ ` <summary>${xmlEscape(summary)}</summary>`,
696
+ ` <result>${xmlEscape(result)}${xmlEscape(resultSuffix)}</result>`,
697
+ ` <duration-ms>${n.durationMs.toString()}</duration-ms>`,
698
+ "</task-notification>",
699
+ ].join("\n");
700
+ }
701
+ function xmlEscape(s) {
702
+ return s
703
+ .replace(/&/g, "&amp;")
704
+ .replace(/</g, "&lt;")
705
+ .replace(/>/g, "&gt;")
706
+ .replace(/"/g, "&quot;")
707
+ .replace(/'/g, "&apos;");
708
+ }
709
+ /**
710
+ * (5.4) Build the synthetic resource message for `@<server>:<uri>` mentions.
711
+ * EVERY inserted field — server, uri, body, note — is XML-escaped (codex R1 M5:
712
+ * a malicious resource could otherwise close the tag and inject instructions),
713
+ * exactly like `renderTaskNotificationXml`. The resolver already stripped
714
+ * controls + redacted the body. Returns "" when nothing resolved (the caller
715
+ * skips the message entirely).
716
+ */
717
+ export function renderMcpResourcesXml(resolved) {
718
+ if (resolved.length === 0)
719
+ return "";
720
+ const blocks = resolved.map((r) => {
721
+ const attrs = `server="${xmlEscape(r.server)}" uri="${xmlEscape(r.uri)}"`;
722
+ const body = r.text !== undefined ? xmlEscape(r.text) : `[${xmlEscape(r.note ?? "unavailable")}]`;
723
+ return ` <mcp-resource ${attrs}>\n${body}\n </mcp-resource>`;
724
+ });
725
+ return ["<mcp-resources>", ...blocks, "</mcp-resources>"].join("\n");
726
+ }
727
+ async function sleepCancellable(ms, token) {
728
+ if (ms <= 0) {
729
+ token.throwIfCancelled();
730
+ return;
731
+ }
732
+ await new Promise((resolve, reject) => {
733
+ let timer;
734
+ const abort = () => {
735
+ clearTimeout(timer);
736
+ token.signal.removeEventListener("abort", abort);
737
+ reject(new AppError(ErrorCode.Cancelled, "Operation cancelled"));
738
+ };
739
+ timer = setTimeout(() => {
740
+ token.signal.removeEventListener("abort", abort);
741
+ resolve();
742
+ }, ms);
743
+ if (token.signal.aborted)
744
+ abort();
745
+ else
746
+ token.signal.addEventListener("abort", abort);
747
+ });
748
+ }
749
+ /**
750
+ * Runs a plugin hook without letting plugin bugs derail the agent. Failures are
751
+ * logged on the bus and swallowed — plugins must use their hook return semantics
752
+ * (e.g. throw from beforeToolCall) to block; once a tool has run, an afterHook
753
+ * crash should never undo it.
754
+ */
755
+ async function safeRunHook(plugins, name, input, bus) {
756
+ if (!plugins)
757
+ return;
758
+ try {
759
+ await plugins.runHook(name, input);
760
+ }
761
+ catch (e) {
762
+ if (e instanceof AppError && e.code === ErrorCode.Cancelled)
763
+ throw e;
764
+ bus.emit({ type: "log", level: "warn", message: `plugin hook ${String(name)} threw: ${formatToolError(e)}` });
765
+ }
766
+ }
767
+ //# sourceMappingURL=engine.js.map