apex-code 0.5.0 → 0.5.2

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 (512) hide show
  1. package/CHANGELOG.md +263 -5
  2. package/README.md +624 -0
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.d.ts.map +1 -1
  5. package/dist/bun/cli.js +3 -9
  6. package/dist/bun/cli.js.map +1 -1
  7. package/dist/bun/runtime-setup.d.ts +2 -0
  8. package/dist/bun/runtime-setup.d.ts.map +1 -0
  9. package/dist/bun/runtime-setup.js +9 -0
  10. package/dist/bun/runtime-setup.js.map +1 -0
  11. package/dist/bun/sandbox-env-setup.d.ts +2 -0
  12. package/dist/bun/sandbox-env-setup.d.ts.map +1 -0
  13. package/dist/bun/sandbox-env-setup.js +4 -0
  14. package/dist/bun/sandbox-env-setup.js.map +1 -0
  15. package/dist/cli/args.d.ts.map +1 -1
  16. package/dist/cli/args.js +16 -5
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/cli/config-selector.d.ts.map +1 -1
  19. package/dist/cli/config-selector.js +2 -1
  20. package/dist/cli/config-selector.js.map +1 -1
  21. package/dist/cli/file-processor.d.ts +1 -1
  22. package/dist/cli/file-processor.d.ts.map +1 -1
  23. package/dist/cli/file-processor.js.map +1 -1
  24. package/dist/cli/session-picker.d.ts +1 -1
  25. package/dist/cli/session-picker.d.ts.map +1 -1
  26. package/dist/cli/session-picker.js.map +1 -1
  27. package/dist/cli/setup.d.ts +2 -0
  28. package/dist/cli/setup.d.ts.map +1 -0
  29. package/dist/cli/setup.js +13 -0
  30. package/dist/cli/setup.js.map +1 -0
  31. package/dist/config.d.ts +1 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +1 -0
  34. package/dist/config.js.map +1 -1
  35. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  36. package/dist/core/agent-session-runtime.js +16 -8
  37. package/dist/core/agent-session-runtime.js.map +1 -1
  38. package/dist/core/agent-session.d.ts +85 -15
  39. package/dist/core/agent-session.d.ts.map +1 -1
  40. package/dist/core/agent-session.js +685 -266
  41. package/dist/core/agent-session.js.map +1 -1
  42. package/dist/core/auth-storage.d.ts +4 -0
  43. package/dist/core/auth-storage.d.ts.map +1 -1
  44. package/dist/core/auth-storage.js +55 -0
  45. package/dist/core/auth-storage.js.map +1 -1
  46. package/dist/core/bug-report.d.ts +176 -0
  47. package/dist/core/bug-report.d.ts.map +1 -0
  48. package/dist/core/bug-report.js +291 -0
  49. package/dist/core/bug-report.js.map +1 -0
  50. package/dist/core/cache-stats.d.ts.map +1 -1
  51. package/dist/core/cache-stats.js +12 -1
  52. package/dist/core/cache-stats.js.map +1 -1
  53. package/dist/core/cache-warmer.d.ts +103 -0
  54. package/dist/core/cache-warmer.d.ts.map +1 -0
  55. package/dist/core/cache-warmer.js +356 -0
  56. package/dist/core/cache-warmer.js.map +1 -0
  57. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  58. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  59. package/dist/core/compaction/branch-summarization.js +4 -3
  60. package/dist/core/compaction/branch-summarization.js.map +1 -1
  61. package/dist/core/compaction/compaction.d.ts +5 -3
  62. package/dist/core/compaction/compaction.d.ts.map +1 -1
  63. package/dist/core/compaction/compaction.js +163 -66
  64. package/dist/core/compaction/compaction.js.map +1 -1
  65. package/dist/core/context/pipeline.d.ts +8 -8
  66. package/dist/core/context/pipeline.d.ts.map +1 -1
  67. package/dist/core/context/pipeline.js +21 -12
  68. package/dist/core/context/pipeline.js.map +1 -1
  69. package/dist/core/crash-log.d.ts +27 -0
  70. package/dist/core/crash-log.d.ts.map +1 -0
  71. package/dist/core/crash-log.js +139 -0
  72. package/dist/core/crash-log.js.map +1 -0
  73. package/dist/core/export-html/template.js +6 -1
  74. package/dist/core/extensions/index.d.ts +1 -1
  75. package/dist/core/extensions/index.d.ts.map +1 -1
  76. package/dist/core/extensions/index.js.map +1 -1
  77. package/dist/core/extensions/jiti-loader.d.ts +2 -0
  78. package/dist/core/extensions/jiti-loader.d.ts.map +1 -0
  79. package/dist/core/extensions/jiti-loader.js +4 -0
  80. package/dist/core/extensions/jiti-loader.js.map +1 -0
  81. package/dist/core/extensions/jiti-static-loader.d.ts +2 -0
  82. package/dist/core/extensions/jiti-static-loader.d.ts.map +1 -0
  83. package/dist/core/extensions/jiti-static-loader.js +4 -0
  84. package/dist/core/extensions/jiti-static-loader.js.map +1 -0
  85. package/dist/core/extensions/loader.d.ts.map +1 -1
  86. package/dist/core/extensions/loader.js +40 -59
  87. package/dist/core/extensions/loader.js.map +1 -1
  88. package/dist/core/extensions/runner.d.ts +24 -8
  89. package/dist/core/extensions/runner.d.ts.map +1 -1
  90. package/dist/core/extensions/runner.js +195 -72
  91. package/dist/core/extensions/runner.js.map +1 -1
  92. package/dist/core/extensions/types.d.ts +142 -55
  93. package/dist/core/extensions/types.d.ts.map +1 -1
  94. package/dist/core/extensions/types.js.map +1 -1
  95. package/dist/core/extensions/virtual-modules.d.ts +3 -0
  96. package/dist/core/extensions/virtual-modules.d.ts.map +1 -0
  97. package/dist/core/extensions/virtual-modules.js +41 -0
  98. package/dist/core/extensions/virtual-modules.js.map +1 -0
  99. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  100. package/dist/core/extensions/wrapper.js +1 -20
  101. package/dist/core/extensions/wrapper.js.map +1 -1
  102. package/dist/core/http-dispatcher.d.ts.map +1 -1
  103. package/dist/core/http-dispatcher.js +2 -0
  104. package/dist/core/http-dispatcher.js.map +1 -1
  105. package/dist/core/index.d.ts +2 -1
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js.map +1 -1
  108. package/dist/core/keybindings.d.ts +6 -1
  109. package/dist/core/keybindings.d.ts.map +1 -1
  110. package/dist/core/keybindings.js +5 -1
  111. package/dist/core/keybindings.js.map +1 -1
  112. package/dist/core/messages.d.ts +1 -1
  113. package/dist/core/messages.d.ts.map +1 -1
  114. package/dist/core/messages.js +1 -0
  115. package/dist/core/messages.js.map +1 -1
  116. package/dist/core/model-config.d.ts +168 -20
  117. package/dist/core/model-config.d.ts.map +1 -1
  118. package/dist/core/model-config.js +44 -18
  119. package/dist/core/model-config.js.map +1 -1
  120. package/dist/core/model-registry.d.ts +5 -1
  121. package/dist/core/model-registry.d.ts.map +1 -1
  122. package/dist/core/model-registry.js +8 -0
  123. package/dist/core/model-registry.js.map +1 -1
  124. package/dist/core/model-resolver.d.ts.map +1 -1
  125. package/dist/core/model-resolver.js +4 -3
  126. package/dist/core/model-resolver.js.map +1 -1
  127. package/dist/core/model-runtime.d.ts +1 -0
  128. package/dist/core/model-runtime.d.ts.map +1 -1
  129. package/dist/core/model-runtime.js +12 -6
  130. package/dist/core/model-runtime.js.map +1 -1
  131. package/dist/core/permissions/store.d.ts.map +1 -1
  132. package/dist/core/permissions/store.js +11 -8
  133. package/dist/core/permissions/store.js.map +1 -1
  134. package/dist/core/prompt-templates.d.ts +6 -1
  135. package/dist/core/prompt-templates.d.ts.map +1 -1
  136. package/dist/core/prompt-templates.js +61 -35
  137. package/dist/core/prompt-templates.js.map +1 -1
  138. package/dist/core/provider-composer.d.ts +4 -2
  139. package/dist/core/provider-composer.d.ts.map +1 -1
  140. package/dist/core/provider-composer.js +29 -2
  141. package/dist/core/provider-composer.js.map +1 -1
  142. package/dist/core/radius.d.ts +3 -0
  143. package/dist/core/radius.d.ts.map +1 -1
  144. package/dist/core/radius.js +6 -0
  145. package/dist/core/radius.js.map +1 -1
  146. package/dist/core/resource-loader.d.ts.map +1 -1
  147. package/dist/core/resource-loader.js +6 -2
  148. package/dist/core/resource-loader.js.map +1 -1
  149. package/dist/core/sdk.d.ts.map +1 -1
  150. package/dist/core/sdk.js +63 -42
  151. package/dist/core/sdk.js.map +1 -1
  152. package/dist/core/session-export.d.ts +6 -2
  153. package/dist/core/session-export.d.ts.map +1 -1
  154. package/dist/core/session-export.js +14 -13
  155. package/dist/core/session-export.js.map +1 -1
  156. package/dist/core/session-manager.d.ts +66 -15
  157. package/dist/core/session-manager.d.ts.map +1 -1
  158. package/dist/core/session-manager.js +295 -115
  159. package/dist/core/session-manager.js.map +1 -1
  160. package/dist/core/settings-manager.d.ts +27 -4
  161. package/dist/core/settings-manager.d.ts.map +1 -1
  162. package/dist/core/settings-manager.js +55 -7
  163. package/dist/core/settings-manager.js.map +1 -1
  164. package/dist/core/skills.d.ts +1 -1
  165. package/dist/core/skills.d.ts.map +1 -1
  166. package/dist/core/skills.js +5 -4
  167. package/dist/core/skills.js.map +1 -1
  168. package/dist/core/slash-commands.d.ts.map +1 -1
  169. package/dist/core/slash-commands.js +1 -0
  170. package/dist/core/slash-commands.js.map +1 -1
  171. package/dist/core/system-prompt.d.ts +49 -6
  172. package/dist/core/system-prompt.d.ts.map +1 -1
  173. package/dist/core/system-prompt.js +140 -79
  174. package/dist/core/system-prompt.js.map +1 -1
  175. package/dist/core/tools/bash.d.ts +7 -8
  176. package/dist/core/tools/bash.d.ts.map +1 -1
  177. package/dist/core/tools/bash.js +20 -115
  178. package/dist/core/tools/bash.js.map +1 -1
  179. package/dist/core/tools/edit.d.ts +1 -12
  180. package/dist/core/tools/edit.d.ts.map +1 -1
  181. package/dist/core/tools/edit.js +6 -204
  182. package/dist/core/tools/edit.js.map +1 -1
  183. package/dist/core/tools/find.d.ts.map +1 -1
  184. package/dist/core/tools/find.js +4 -55
  185. package/dist/core/tools/find.js.map +1 -1
  186. package/dist/core/tools/grep.d.ts.map +1 -1
  187. package/dist/core/tools/grep.js +4 -60
  188. package/dist/core/tools/grep.js.map +1 -1
  189. package/dist/core/tools/ls.d.ts.map +1 -1
  190. package/dist/core/tools/ls.js +4 -49
  191. package/dist/core/tools/ls.js.map +1 -1
  192. package/dist/core/tools/read.d.ts +4 -1
  193. package/dist/core/tools/read.d.ts.map +1 -1
  194. package/dist/core/tools/read.js +10 -125
  195. package/dist/core/tools/read.js.map +1 -1
  196. package/dist/core/tools/renderers/bash.d.ts +18 -0
  197. package/dist/core/tools/renderers/bash.d.ts.map +1 -0
  198. package/dist/core/tools/renderers/bash.js +126 -0
  199. package/dist/core/tools/renderers/bash.js.map +1 -0
  200. package/dist/core/tools/renderers/edit.d.ts +23 -0
  201. package/dist/core/tools/renderers/edit.d.ts.map +1 -0
  202. package/dist/core/tools/renderers/edit.js +207 -0
  203. package/dist/core/tools/renderers/edit.js.map +1 -0
  204. package/dist/core/tools/renderers/find.d.ts +10 -0
  205. package/dist/core/tools/renderers/find.d.ts.map +1 -0
  206. package/dist/core/tools/renderers/find.js +64 -0
  207. package/dist/core/tools/renderers/find.js.map +1 -0
  208. package/dist/core/tools/renderers/grep.d.ts +10 -0
  209. package/dist/core/tools/renderers/grep.d.ts.map +1 -0
  210. package/dist/core/tools/renderers/grep.js +69 -0
  211. package/dist/core/tools/renderers/grep.js.map +1 -0
  212. package/dist/core/tools/renderers/index.d.ts +34 -0
  213. package/dist/core/tools/renderers/index.d.ts.map +1 -0
  214. package/dist/core/tools/renderers/index.js +47 -0
  215. package/dist/core/tools/renderers/index.js.map +1 -0
  216. package/dist/core/tools/renderers/ls.d.ts +10 -0
  217. package/dist/core/tools/renderers/ls.d.ts.map +1 -0
  218. package/dist/core/tools/renderers/ls.js +58 -0
  219. package/dist/core/tools/renderers/ls.js.map +1 -0
  220. package/dist/core/tools/renderers/read.d.ts +11 -0
  221. package/dist/core/tools/renderers/read.d.ts.map +1 -0
  222. package/dist/core/tools/renderers/read.js +132 -0
  223. package/dist/core/tools/renderers/read.js.map +1 -0
  224. package/dist/core/tools/renderers/write.d.ts +10 -0
  225. package/dist/core/tools/renderers/write.d.ts.map +1 -0
  226. package/dist/core/tools/renderers/write.js +152 -0
  227. package/dist/core/tools/renderers/write.js.map +1 -0
  228. package/dist/core/tools/write.d.ts.map +1 -1
  229. package/dist/core/tools/write.js +6 -146
  230. package/dist/core/tools/write.js.map +1 -1
  231. package/dist/core/usage-totals.d.ts +1 -1
  232. package/dist/core/usage-totals.d.ts.map +1 -1
  233. package/dist/core/usage-totals.js +5 -1
  234. package/dist/core/usage-totals.js.map +1 -1
  235. package/dist/extensions/llama/client.d.ts +2 -0
  236. package/dist/extensions/llama/client.d.ts.map +1 -1
  237. package/dist/extensions/llama/client.js +7 -3
  238. package/dist/extensions/llama/client.js.map +1 -1
  239. package/dist/extensions/llama/provider.d.ts.map +1 -1
  240. package/dist/extensions/llama/provider.js +20 -5
  241. package/dist/extensions/llama/provider.js.map +1 -1
  242. package/dist/index.d.ts +5 -4
  243. package/dist/index.d.ts.map +1 -1
  244. package/dist/index.js +1 -1
  245. package/dist/index.js.map +1 -1
  246. package/dist/main.d.ts.map +1 -1
  247. package/dist/main.js +21 -13
  248. package/dist/main.js.map +1 -1
  249. package/dist/modes/interactive/bug-report.d.ts +16 -0
  250. package/dist/modes/interactive/bug-report.d.ts.map +1 -0
  251. package/dist/modes/interactive/bug-report.js +167 -0
  252. package/dist/modes/interactive/bug-report.js.map +1 -0
  253. package/dist/modes/interactive/chat-viewport.d.ts +20 -0
  254. package/dist/modes/interactive/chat-viewport.d.ts.map +1 -0
  255. package/dist/modes/interactive/chat-viewport.js +28 -0
  256. package/dist/modes/interactive/chat-viewport.js.map +1 -0
  257. package/dist/modes/interactive/components/assistant-message.d.ts +7 -0
  258. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  259. package/dist/modes/interactive/components/assistant-message.js +63 -17
  260. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  261. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  263. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  264. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  265. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  266. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  267. package/dist/modes/interactive/components/custom-editor.d.ts +12 -0
  268. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  269. package/dist/modes/interactive/components/custom-editor.js +38 -0
  270. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  271. package/dist/modes/interactive/components/error-summary.d.ts +11 -0
  272. package/dist/modes/interactive/components/error-summary.d.ts.map +1 -0
  273. package/dist/modes/interactive/components/error-summary.js +31 -0
  274. package/dist/modes/interactive/components/error-summary.js.map +1 -0
  275. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  276. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  277. package/dist/modes/interactive/components/extension-editor.js +6 -1
  278. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  279. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  280. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/extension-input.js +6 -0
  282. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  283. package/dist/modes/interactive/components/extension-selector.d.ts +2 -0
  284. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  285. package/dist/modes/interactive/components/extension-selector.js +4 -0
  286. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  287. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  288. package/dist/modes/interactive/components/footer.js +4 -1
  289. package/dist/modes/interactive/components/footer.js.map +1 -1
  290. package/dist/modes/interactive/components/index.d.ts +1 -1
  291. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  292. package/dist/modes/interactive/components/index.js.map +1 -1
  293. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  294. package/dist/modes/interactive/components/model-selector.js +3 -3
  295. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  296. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  297. package/dist/modes/interactive/components/scoped-models-selector.js +13 -15
  298. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  299. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  300. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  301. package/dist/modes/interactive/components/session-selector.js +74 -48
  302. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  303. package/dist/modes/interactive/components/settings-selector.d.ts +5 -1
  304. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  305. package/dist/modes/interactive/components/settings-selector.js +35 -10
  306. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  307. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  308. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  309. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  310. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  311. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -1
  312. package/dist/modes/interactive/components/status-indicator.js +10 -3
  313. package/dist/modes/interactive/components/status-indicator.js.map +1 -1
  314. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  315. package/dist/modes/interactive/components/thinking-selector.js +6 -6
  316. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  317. package/dist/modes/interactive/components/tool-execution.d.ts +27 -5
  318. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  319. package/dist/modes/interactive/components/tool-execution.js +64 -42
  320. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  321. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  322. package/dist/modes/interactive/components/tree-selector.js +9 -0
  323. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  324. package/dist/modes/interactive/components/trust-selector.d.ts.map +1 -1
  325. package/dist/modes/interactive/components/trust-selector.js +2 -2
  326. package/dist/modes/interactive/components/trust-selector.js.map +1 -1
  327. package/dist/modes/interactive/interactive-mode.d.ts +43 -29
  328. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  329. package/dist/modes/interactive/interactive-mode.js +431 -166
  330. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  331. package/dist/modes/interactive/theme/dark.json +2 -1
  332. package/dist/modes/interactive/theme/light.json +2 -1
  333. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  334. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  335. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  336. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  337. package/dist/modes/interactive/theme/theme-json.d.ts +84 -0
  338. package/dist/modes/interactive/theme/theme-json.d.ts.map +1 -0
  339. package/dist/modes/interactive/theme/theme-json.js +130 -0
  340. package/dist/modes/interactive/theme/theme-json.js.map +1 -0
  341. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  342. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  343. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  344. package/dist/modes/interactive/theme/theme.js +18 -119
  345. package/dist/modes/interactive/theme/theme.js.map +1 -1
  346. package/dist/modes/interactive/tui-renderer.d.ts +21 -0
  347. package/dist/modes/interactive/tui-renderer.d.ts.map +1 -0
  348. package/dist/modes/interactive/tui-renderer.js +66 -0
  349. package/dist/modes/interactive/tui-renderer.js.map +1 -0
  350. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  351. package/dist/modes/rpc/rpc-mode.js +2 -2
  352. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  353. package/dist/package-manager-cli.d.ts.map +1 -1
  354. package/dist/package-manager-cli.js +31 -72
  355. package/dist/package-manager-cli.js.map +1 -1
  356. package/dist/testing/replay/recorded-provider.d.ts +2 -2
  357. package/dist/testing/replay/recorded-provider.d.ts.map +1 -1
  358. package/dist/testing/replay/recorded-provider.js +2 -5
  359. package/dist/testing/replay/recorded-provider.js.map +1 -1
  360. package/dist/testing/replay/runner.d.ts.map +1 -1
  361. package/dist/testing/replay/runner.js +0 -1
  362. package/dist/testing/replay/runner.js.map +1 -1
  363. package/dist/utils/clipboard-command.d.ts +7 -0
  364. package/dist/utils/clipboard-command.d.ts.map +1 -0
  365. package/dist/utils/clipboard-command.js +45 -0
  366. package/dist/utils/clipboard-command.js.map +1 -0
  367. package/dist/utils/clipboard-image.d.ts.map +1 -1
  368. package/dist/utils/clipboard-image.js +53 -81
  369. package/dist/utils/clipboard-image.js.map +1 -1
  370. package/dist/utils/clipboard.d.ts.map +1 -1
  371. package/dist/utils/clipboard.js +111 -121
  372. package/dist/utils/clipboard.js.map +1 -1
  373. package/dist/utils/exif-orientation.d.ts.map +1 -1
  374. package/dist/utils/exif-orientation.js +2 -3
  375. package/dist/utils/exif-orientation.js.map +1 -1
  376. package/dist/utils/mime.d.ts.map +1 -1
  377. package/dist/utils/mime.js +1 -1
  378. package/dist/utils/mime.js.map +1 -1
  379. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  380. package/dist/utils/syntax-highlight.js +21 -21
  381. package/dist/utils/syntax-highlight.js.map +1 -1
  382. package/dist/utils/tool-result-images.d.ts +3 -1
  383. package/dist/utils/tool-result-images.d.ts.map +1 -1
  384. package/dist/utils/tool-result-images.js +4 -1
  385. package/dist/utils/tool-result-images.js.map +1 -1
  386. package/dist/utils/tools-manager.d.ts.map +1 -1
  387. package/dist/utils/tools-manager.js +11 -2
  388. package/dist/utils/tools-manager.js.map +1 -1
  389. package/dist/utils/wsl.d.ts +3 -0
  390. package/dist/utils/wsl.d.ts.map +1 -0
  391. package/dist/utils/wsl.js +15 -0
  392. package/dist/utils/wsl.js.map +1 -0
  393. package/dist/utils/zip.d.ts +7 -0
  394. package/dist/utils/zip.d.ts.map +1 -0
  395. package/dist/utils/zip.js +60 -0
  396. package/dist/utils/zip.js.map +1 -0
  397. package/docs/cli-integration.md +107 -0
  398. package/docs/cli.md +269 -0
  399. package/docs/compaction.md +73 -24
  400. package/docs/configuration.md +45 -0
  401. package/docs/containerization.md +22 -21
  402. package/docs/custom-provider.md +21 -12
  403. package/docs/development.md +19 -0
  404. package/docs/docs.json +139 -95
  405. package/docs/extensions.md +243 -371
  406. package/docs/how-pi-works.md +49 -0
  407. package/docs/images/interactive-mode.png +0 -0
  408. package/docs/index.md +3 -3
  409. package/docs/json.md +7 -3
  410. package/docs/keybindings.md +57 -54
  411. package/docs/llama-cpp.md +2 -2
  412. package/docs/message-types.md +261 -0
  413. package/docs/models.md +103 -49
  414. package/docs/packages.md +49 -47
  415. package/docs/prompt-templates.md +29 -56
  416. package/docs/providers.md +91 -139
  417. package/docs/quickstart.md +42 -14
  418. package/docs/rpc-commands.md +854 -0
  419. package/docs/rpc-extension-ui.md +200 -0
  420. package/docs/rpc.md +121 -223
  421. package/docs/sdk.md +99 -124
  422. package/docs/security.md +7 -7
  423. package/docs/session-format.md +85 -82
  424. package/docs/sessions.md +41 -57
  425. package/docs/settings.md +94 -87
  426. package/docs/shell-aliases.md +68 -3
  427. package/docs/skills.md +62 -49
  428. package/docs/slash-commands.md +60 -0
  429. package/docs/terminal-setup.md +86 -55
  430. package/docs/termux.md +65 -75
  431. package/docs/themes.md +64 -86
  432. package/docs/tmux.md +29 -7
  433. package/docs/tui.md +67 -122
  434. package/docs/usage.md +31 -42
  435. package/docs/windows.md +41 -13
  436. package/examples/README.md +16 -2
  437. package/examples/extensions/README.md +0 -1
  438. package/examples/extensions/custom-provider-anthropic/index.ts +18 -12
  439. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  440. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  441. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -2
  442. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  443. package/examples/extensions/dynamic-resources/dynamic.json +2 -0
  444. package/examples/extensions/gondolin/package-lock.json +2 -2
  445. package/examples/extensions/gondolin/package.json +1 -1
  446. package/examples/extensions/prompt-customizer.ts +19 -67
  447. package/examples/extensions/sandbox/package-lock.json +2 -2
  448. package/examples/extensions/sandbox/package.json +1 -1
  449. package/examples/extensions/with-deps/package-lock.json +2 -2
  450. package/examples/extensions/with-deps/package.json +1 -1
  451. package/examples/plugins/pi-example-plugin/README.md +38 -0
  452. package/examples/plugins/pi-example-plugin/package.json +10 -0
  453. package/examples/plugins/pi-example-plugin/src/contract.ts +13 -0
  454. package/examples/plugins/pi-example-plugin/src/session.ts +24 -0
  455. package/examples/plugins/pi-example-plugin/src/tui.ts +31 -0
  456. package/examples/rpc-client.ts +35 -0
  457. package/examples/rpc-extension-ui.ts +25 -5
  458. package/examples/sdk/README.md +1 -1
  459. package/npm-shrinkwrap.json +861 -513
  460. package/package.json +27 -18
  461. package/dist/bun/register-bedrock.d.ts +0 -2
  462. package/dist/bun/register-bedrock.d.ts.map +0 -1
  463. package/dist/bun/register-bedrock.js +0 -4
  464. package/dist/bun/register-bedrock.js.map +0 -1
  465. package/dist/cli/experimental/auth.d.ts +0 -16
  466. package/dist/cli/experimental/auth.d.ts.map +0 -1
  467. package/dist/cli/experimental/auth.js +0 -13
  468. package/dist/cli/experimental/auth.js.map +0 -1
  469. package/dist/cli/experimental/cli.d.ts +0 -6
  470. package/dist/cli/experimental/cli.d.ts.map +0 -1
  471. package/dist/cli/experimental/cli.js +0 -5
  472. package/dist/cli/experimental/cli.js.map +0 -1
  473. package/dist/cli/experimental/command-options.d.ts +0 -17
  474. package/dist/cli/experimental/command-options.d.ts.map +0 -1
  475. package/dist/cli/experimental/command-options.js +0 -35
  476. package/dist/cli/experimental/command-options.js.map +0 -1
  477. package/dist/cli/experimental/command.d.ts +0 -63
  478. package/dist/cli/experimental/command.d.ts.map +0 -1
  479. package/dist/cli/experimental/command.js +0 -130
  480. package/dist/cli/experimental/command.js.map +0 -1
  481. package/dist/cli/experimental/commands/client.d.ts +0 -13
  482. package/dist/cli/experimental/commands/client.d.ts.map +0 -1
  483. package/dist/cli/experimental/commands/client.js +0 -25
  484. package/dist/cli/experimental/commands/client.js.map +0 -1
  485. package/dist/cli/experimental/commands/pi.d.ts +0 -15
  486. package/dist/cli/experimental/commands/pi.d.ts.map +0 -1
  487. package/dist/cli/experimental/commands/pi.js +0 -28
  488. package/dist/cli/experimental/commands/pi.js.map +0 -1
  489. package/dist/cli/experimental/commands/server.d.ts +0 -13
  490. package/dist/cli/experimental/commands/server.d.ts.map +0 -1
  491. package/dist/cli/experimental/commands/server.js +0 -25
  492. package/dist/cli/experimental/commands/server.js.map +0 -1
  493. package/dist/cli/experimental/transport-address.d.ts +0 -10
  494. package/dist/cli/experimental/transport-address.d.ts.map +0 -1
  495. package/dist/cli/experimental/transport-address.js +0 -38
  496. package/dist/cli/experimental/transport-address.js.map +0 -1
  497. package/dist/client/index.d.ts +0 -3
  498. package/dist/client/index.d.ts.map +0 -1
  499. package/dist/client/index.js +0 -3
  500. package/dist/client/index.js.map +0 -1
  501. package/dist/client/remote-session.d.ts +0 -53
  502. package/dist/client/remote-session.d.ts.map +0 -1
  503. package/dist/client/remote-session.js +0 -340
  504. package/dist/client/remote-session.js.map +0 -1
  505. package/dist/client/transcript.d.ts +0 -12
  506. package/dist/client/transcript.d.ts.map +0 -1
  507. package/dist/client/transcript.js +0 -98
  508. package/dist/client/transcript.js.map +0 -1
  509. package/dist/utils/clipboard-native.d.ts +0 -11
  510. package/dist/utils/clipboard-native.d.ts.map +0 -1
  511. package/dist/utils/clipboard-native.js +0 -20
  512. package/dist/utils/clipboard-native.js.map +0 -1
@@ -14,16 +14,18 @@
14
14
  */
15
15
  import { readFileSync } from "node:fs";
16
16
  import { basename, dirname, isAbsolute, relative } from "node:path";
17
- import { contentText } from "@earendil-works/pi-ai";
17
+ import { contentText, getCurrentSystemMessage, retryDelayMs } from "@earendil-works/pi-ai";
18
18
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, } from "@earendil-works/pi-ai/compat";
19
19
  import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
20
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
21
+ import { processImage } from "../utils/image-process.js";
21
22
  import { sleep } from "../utils/sleep.js";
22
23
  import { normalizeToolResultImages } from "../utils/tool-result-images.js";
23
24
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
24
25
  import { executeBashWithOperations } from "./bash-executor.js";
26
+ import { generateBugReportSummary } from "./bug-report.js";
25
27
  import { createSessionCheckpoints } from "./checkpoints/session-checkpoints.js";
26
- import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
28
+ import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
27
29
  import { evictionBudget, installContextPipeline, isDefaultStreamFunction } from "./context/pipeline.js";
28
30
  import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
29
31
  import { RESUME_CHILD_PROMPT, runDelegation } from "./delegation/runtime.js";
@@ -33,6 +35,7 @@ import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
33
35
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
34
36
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
35
37
  import { runFormatterCommand } from "./formatter-lifecycle.js";
38
+ import { convertToLlm } from "./messages.js";
36
39
  import { ModelRegistry } from "./model-registry.js";
37
40
  import { evaluateToolCall } from "./permissions/gate.js";
38
41
  import { authorizeConfiguredCommand } from "./permissions/policy-command.js";
@@ -41,10 +44,10 @@ import { resolveEffectiveModeWithOrigin } from "./permissions/startup.js";
41
44
  import { loadPolicyConfiguration } from "./policy-loader.js";
42
45
  import { expandPromptTemplate } from "./prompt-templates.js";
43
46
  import { exportSessionToJsonl } from "./session-export.js";
44
- import { getLatestCompactionEntry, TODO_CUSTOM_ENTRY_TYPE } from "./session-manager.js";
47
+ import { getLatestCompactionEntry, SessionManager, TODO_CUSTOM_ENTRY_TYPE, } from "./session-manager.js";
45
48
  import { slugifySkillCommandName } from "./skills.js";
46
49
  import { createSyntheticSourceInfo } from "./source-info.js";
47
- import { buildSystemPrompt } from "./system-prompt.js";
50
+ import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
48
51
  import { createLocalBashOperations } from "./tools/bash.js";
49
52
  import { buildToolContractSnapshot, isUnclassifiedTool, } from "./tools/contract-snapshot.js";
50
53
  import { createAllToolDefinitions } from "./tools/index.js";
@@ -103,6 +106,7 @@ export class AgentSession {
103
106
  _unsubscribeAgent;
104
107
  _eventListeners = [];
105
108
  _isAgentRunActive = false;
109
+ _agentRunAbortRequested = false;
106
110
  _idleWaitPromise;
107
111
  _resolveIdleWait;
108
112
  /** Tracks pending steering messages for UI display. Removed when delivered. */
@@ -129,6 +133,15 @@ export class AgentSession {
129
133
  // Extension system
130
134
  _extensionRunner;
131
135
  _turnIndex = 0;
136
+ _entryIdsByMessage = new WeakMap();
137
+ _boundaryDispatchedMessages = new WeakSet();
138
+ _lastAssistantMessage;
139
+ _lastAssistantToolResults = [];
140
+ _lastActivityOutcome = "completed";
141
+ _isBeforeSettle = false;
142
+ _abortDuringBeforeSettle = false;
143
+ _isEmittingAgentSettled = false;
144
+ _deferredSettledActions = [];
132
145
  _resourceLoader;
133
146
  _customTools;
134
147
  _baseToolDefinitions = new Map();
@@ -183,16 +196,16 @@ export class AgentSession {
183
196
  _extensionErrorListener;
184
197
  _extensionErrorUnsubscriber;
185
198
  _modelRuntime;
199
+ _cacheWarmer;
186
200
  // Tool registry for extension getTools/setTools
187
201
  _toolRegistry = new Map();
188
202
  _toolDefinitions = new Map();
189
203
  _toolPromptSnippets = new Map();
190
204
  _toolPromptGuidelines = new Map();
191
205
  _loadedDeferredSchemas = new Set();
192
- // Base system prompt (without extension appends) - used to apply fresh appends each turn
193
- _baseSystemPrompt = "";
194
206
  _baseSystemPromptOptions;
195
- _systemPromptOverride;
207
+ /** Prompt options after before_agent_start mutations for the active run. */
208
+ _runSystemPromptOptions;
196
209
  constructor(config) {
197
210
  this.agent = config.agent;
198
211
  this.sessionManager = config.sessionManager;
@@ -202,6 +215,10 @@ export class AgentSession {
202
215
  this._customTools = config.customTools ?? [];
203
216
  this._cwd = config.cwd;
204
217
  this._modelRuntime = config.modelRuntime;
218
+ this._cacheWarmer = config.cacheWarmer;
219
+ if (this._cacheWarmer) {
220
+ this._cacheWarmer.onWarmed = (entry) => this._emit({ type: "entry_appended", entry });
221
+ }
205
222
  this._extensionRunnerRef = config.extensionRunnerRef;
206
223
  this._initialActiveToolNames = config.initialActiveToolNames;
207
224
  this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
@@ -245,6 +262,9 @@ export class AgentSession {
245
262
  this._installAgentNextTurnRefresh();
246
263
  this._installMidRunCompactionHook();
247
264
  this._installContextPipeline();
265
+ this._installAgentRequestProjection();
266
+ this._installAgentBoundaryHooks();
267
+ this._installAgentForcedPromptProjection();
248
268
  // Declarative hooks: session_start is observe-only and must neither delay
249
269
  // nor fail construction, so it fires detached (emitObserve swallows
250
270
  // handler failures already).
@@ -258,14 +278,16 @@ export class AgentSession {
258
278
  activeToolNames: this._initialActiveToolNames,
259
279
  includeAllExtensionTools: true,
260
280
  });
281
+ if (this._initialActiveToolNames === undefined)
282
+ this._restoreToolsFromTranscript();
261
283
  }
262
284
  get modelRuntime() {
263
285
  return this._modelRuntime;
264
286
  }
265
- async _getRequiredRequestAuth(model) {
287
+ async _getRequiredRequestAuth(model, signal) {
266
288
  let result;
267
289
  try {
268
- result = await this._modelRuntime.getAuth(model);
290
+ result = await this._modelRuntime.getAuth(model, { signal });
269
291
  }
270
292
  catch (error) {
271
293
  const cause = error instanceof Error ? error.cause : undefined;
@@ -291,12 +313,12 @@ export class AgentSession {
291
313
  }
292
314
  throw new Error(formatNoApiKeyFoundMessage(model.provider));
293
315
  }
294
- async _getSummarizationRequestAuth(model) {
316
+ async _getSummarizationRequestAuth(model, signal) {
295
317
  if (isDefaultStreamFunction(this.agent.streamFunction)) {
296
- return this._getRequiredRequestAuth(model);
318
+ return this._getRequiredRequestAuth(model, signal);
297
319
  }
298
320
  try {
299
- const result = await this._modelRuntime.getAuth(model);
321
+ const result = await this._modelRuntime.getAuth(model, { signal });
300
322
  if (!result)
301
323
  return { model };
302
324
  const requestModel = result.auth.baseUrl ? { ...model, baseUrl: result.auth.baseUrl } : model;
@@ -307,7 +329,9 @@ export class AgentSession {
307
329
  env: result.env,
308
330
  };
309
331
  }
310
- catch {
332
+ catch (error) {
333
+ if (signal?.aborted)
334
+ throw error;
311
335
  return { model };
312
336
  }
313
337
  }
@@ -419,8 +443,10 @@ export class AgentSession {
419
443
  }
420
444
  const content = hookResult?.content ?? result.content ?? [];
421
445
  // Runs after the extension hook so images injected or replaced by extensions are normalized too.
446
+ const resizeOptions = this.model?.inputLimits?.images?.resize;
422
447
  const normalizedContent = await normalizeToolResultImages(content, {
423
448
  autoResizeImages: this.settingsManager.getImageAutoResize(),
449
+ ...(resizeOptions ? { resizeOptions } : {}),
424
450
  });
425
451
  if (!hookResult && normalizedContent === content) {
426
452
  return undefined;
@@ -435,16 +461,84 @@ export class AgentSession {
435
461
  }
436
462
  async _compactBeforeNextAssistantResponse(context) {
437
463
  const model = this.model;
438
- const settings = this.settingsManager.getCompactionSettings();
464
+ const settings = this.settingsManager.getCompactionSettings(model);
465
+ const projection = this.sessionManager.buildSessionProjection();
439
466
  if (!model ||
440
467
  model.contextWindow <= 0 ||
441
- !shouldCompact(estimateContextTokens(context.messages).tokens, model.contextWindow, settings)) {
442
- return context;
468
+ !shouldCompact(estimateProjectedContextTokens(projection, this.sessionManager.getBranch()).tokens, model.contextWindow, settings)) {
469
+ return { ...context, messages: projection.messages };
443
470
  }
444
471
  await this._runAutoCompaction("threshold", false);
445
- return {
446
- ...context,
447
- messages: this.agent.state.messages.slice(),
472
+ return { ...context, messages: this.sessionManager.buildSessionProjection().messages };
473
+ }
474
+ _installAgentRequestProjection() {
475
+ const previousPrepareRequest = this.agent.prepareRequest;
476
+ this.agent.prepareRequest = async (request, signal) => {
477
+ const canonicalContext = {
478
+ ...request.context,
479
+ messages: this.sessionManager.buildSessionProjection().messages,
480
+ // Messages declare the provider-visible loadout; context.tools keeps executable implementations.
481
+ tools: this.agent.state.tools.slice(),
482
+ };
483
+ const previous = await previousPrepareRequest?.({
484
+ ...request,
485
+ context: canonicalContext,
486
+ model: this.agent.state.model,
487
+ thinkingLevel: this.agent.state.thinkingLevel,
488
+ }, signal);
489
+ return {
490
+ ...previous,
491
+ context: previous?.context ?? canonicalContext,
492
+ model: previous?.model ?? this.agent.state.model,
493
+ thinkingLevel: previous?.thinkingLevel ?? this.agent.state.thinkingLevel,
494
+ };
495
+ };
496
+ }
497
+ async _dispatchTurnEndBoundary(message, toolResults) {
498
+ this._lastActivityOutcome =
499
+ message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
500
+ const messageEntryId = this._findPersistedMessageEntryId(message);
501
+ if (!this._extensionRunner.hasHandlers("turn_end"))
502
+ return false;
503
+ if (!messageEntryId) {
504
+ this._extensionRunner.emitError({
505
+ extensionPath: "<boundary>",
506
+ event: "turn_end",
507
+ error: "turn_end could not resolve the persisted assistant entry ID",
508
+ });
509
+ return false;
510
+ }
511
+ const toolResultEntryIds = toolResults.flatMap((result) => {
512
+ const entryId = this._findPersistedMessageEntryId(result);
513
+ return entryId ? [entryId] : [];
514
+ });
515
+ const boundary = await this._extensionRunner.emitBoundary({
516
+ type: "turn_end",
517
+ turnIndex: this._turnIndex,
518
+ message,
519
+ toolResults,
520
+ messageEntryId,
521
+ toolResultEntryIds,
522
+ outcome: this._lastActivityOutcome,
523
+ }, (entries) => this._buildBoundaryContext(entries, "turn_end"));
524
+ this._commitBoundaryDrafts(boundary.entries);
525
+ if (boundary.continue && !this._buildBoundaryContext([], "turn_end").canContinue) {
526
+ this._reportInvalidBoundaryContinuation("turn_end");
527
+ return false;
528
+ }
529
+ return boundary.continue;
530
+ }
531
+ _installAgentBoundaryHooks() {
532
+ const previousFinishTurn = this.agent.finishTurn;
533
+ this.agent.finishTurn = async (turn, signal) => {
534
+ this._boundaryDispatchedMessages.add(turn.message);
535
+ const extensionContinue = await this._dispatchTurnEndBoundary(turn.message, turn.toolResults);
536
+ const previousDecision = await previousFinishTurn?.(turn, signal);
537
+ if (previousDecision?.action === "end")
538
+ return previousDecision;
539
+ if (extensionContinue || previousDecision?.action === "continue")
540
+ return { action: "continue" };
541
+ return undefined;
448
542
  };
449
543
  }
450
544
  _installAgentNextTurnRefresh() {
@@ -453,16 +547,31 @@ export class AgentSession {
453
547
  ? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
454
548
  : undefined);
455
549
  this.agent.prepareNextTurnWithContext = async (turn, signal) => {
456
- const context = await this._compactBeforeNextAssistantResponse(turn.context);
550
+ const context = await this._compactBeforeNextAssistantResponse({
551
+ ...turn.context,
552
+ messages: this.sessionManager.buildSessionProjection().messages,
553
+ });
457
554
  const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
458
555
  const nextContext = previousSnapshot?.context ?? context;
556
+ const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
557
+ const options = normalizeBuildSystemPromptOptions({
558
+ ...runOptions,
559
+ selectedTools: this.getActiveToolNames(),
560
+ toolSnippets: { ...this._baseSystemPromptOptions.toolSnippets, ...runOptions.toolSnippets },
561
+ toolGuidelines: { ...this._baseSystemPromptOptions.toolGuidelines, ...runOptions.toolGuidelines },
562
+ });
563
+ const updateMessage = this._preparePromptAndToolLoadout(options, nextContext.messages);
564
+ // Keep session.systemPrompt and ctx.getSystemPrompt() in step with what the provider sees.
565
+ this._runSystemPromptOptions = options;
459
566
  return {
460
567
  ...previousSnapshot,
461
568
  context: {
462
569
  ...nextContext,
463
- systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
464
570
  tools: this.agent.state.tools.slice(),
465
571
  },
572
+ messages: updateMessage
573
+ ? [...(previousSnapshot?.messages ?? []), updateMessage]
574
+ : previousSnapshot?.messages,
466
575
  model: this.agent.state.model,
467
576
  thinkingLevel: this.agent.state.thinkingLevel,
468
577
  };
@@ -540,6 +649,84 @@ export class AgentSession {
540
649
  // =========================================================================
541
650
  // Event Subscription
542
651
  // =========================================================================
652
+ _refreshFinalizedContext() {
653
+ const projection = this.sessionManager.buildSessionProjection();
654
+ for (const entry of projection.entries) {
655
+ for (const message of entry.messages)
656
+ this._entryIdsByMessage.set(message, entry.sourceEntry.id);
657
+ }
658
+ this.agent.state.messages = projection.messages;
659
+ }
660
+ _applyBoundaryDrafts(manager, drafts) {
661
+ const appended = [];
662
+ for (const draft of drafts) {
663
+ let entryId;
664
+ switch (draft.type) {
665
+ case "custom":
666
+ entryId = manager.appendCustomEntry(draft.customType, draft.data);
667
+ break;
668
+ case "custom_message":
669
+ entryId = manager.appendCustomMessageEntry(draft.customType, draft.content, draft.display, draft.details);
670
+ break;
671
+ case "context_edit":
672
+ entryId = manager.appendContextEdit(draft.targetId, draft.replacement);
673
+ break;
674
+ case "compaction": {
675
+ const tokensBefore = estimateProjectedContextTokens(manager.buildSessionProjection(), manager.getBranch()).tokens;
676
+ entryId = manager.appendCompaction(draft.summary, draft.firstKeptEntryId, tokensBefore, draft.details, true, draft.usage);
677
+ break;
678
+ }
679
+ }
680
+ const entry = manager.getEntry(entryId);
681
+ if (entry)
682
+ appended.push(entry);
683
+ }
684
+ return appended;
685
+ }
686
+ _createBoundaryPreviewManager(drafts) {
687
+ const header = this.sessionManager.getHeader();
688
+ if (!header)
689
+ throw new Error("Session header is missing");
690
+ const manager = SessionManager.inMemory(this._cwd, undefined, [header, ...this.sessionManager.getBranch()]);
691
+ this._applyBoundaryDrafts(manager, drafts);
692
+ return manager;
693
+ }
694
+ _getPendingBoundaryMessages() {
695
+ return [...this.agent.peekQueuedMessages(), ...this._pendingCustomMessages];
696
+ }
697
+ _buildBoundaryContext(drafts, boundary) {
698
+ const projection = this._createBoundaryPreviewManager(drafts).buildSessionProjection();
699
+ const pendingMessages = this._getPendingBoundaryMessages();
700
+ const llmMessages = convertToLlm(projection.messages);
701
+ const finalRole = llmMessages[llmMessages.length - 1]?.role;
702
+ const hasNonSystemContext = llmMessages.some((message) => message.role !== "system");
703
+ const contextCanContinue = hasNonSystemContext && finalRole !== "assistant";
704
+ const pendingCustomContext = this._pendingCustomMessages.length > 0;
705
+ return {
706
+ contextEntries: projection.entries,
707
+ contextMessages: projection.messages,
708
+ llmMessages,
709
+ pendingMessages,
710
+ canContinue: contextCanContinue ||
711
+ pendingCustomContext ||
712
+ (boundary === "turn_end"
713
+ ? this.agent.hasQueuedMessages()
714
+ : finalRole === "assistant" && this.agent.hasQueuedMessages()),
715
+ };
716
+ }
717
+ _commitBoundaryDrafts(drafts) {
718
+ const appended = this._applyBoundaryDrafts(this.sessionManager, drafts);
719
+ this._refreshFinalizedContext();
720
+ for (const entry of appended)
721
+ this._emit({ type: "entry_appended", entry });
722
+ }
723
+ _reportInvalidBoundaryContinuation(event) {
724
+ this._extensionRunner.emitError({
725
+ extensionPath: "<boundary>",
726
+ event,
727
+ error: `${event} requested continuation without runnable model context`,
728
+ });
729
+ }
543
730
  /** Emit an event to all listeners */
544
731
  _emit(event) {
545
732
  for (const l of this._eventListeners) {
@@ -567,7 +754,7 @@ export class AgentSession {
567
754
  return this._idleWaitPromise;
568
755
  }
569
756
  _resolveIdleWaitIfIdle() {
570
- if (this._isAgentRunActive || !this._resolveIdleWait) {
757
+ if (!this.isIdle || !this._resolveIdleWait) {
571
758
  return;
572
759
  }
573
760
  const resolve = this._resolveIdleWait;
@@ -576,17 +763,29 @@ export class AgentSession {
576
763
  resolve();
577
764
  }
578
765
  async _emitAgentSettled() {
766
+ this._cacheWarmer?.onAgentSettled();
579
767
  this._isAgentRunActive = false;
768
+ this._isEmittingAgentSettled = true;
580
769
  try {
581
770
  await this._extensionRunner.emit({ type: "agent_settled" });
582
771
  this._emit({ type: "agent_settled" });
583
772
  }
584
773
  finally {
585
- this._resolveIdleWaitIfIdle();
774
+ this._isEmittingAgentSettled = false;
586
775
  }
776
+ const deferred = this._deferredSettledActions.splice(0);
777
+ if (deferred.length > 0) {
778
+ try {
779
+ for (const action of deferred)
780
+ await action();
781
+ }
782
+ finally {
783
+ this._resolveIdleWaitIfIdle();
784
+ }
785
+ return;
786
+ }
787
+ this._resolveIdleWaitIfIdle();
587
788
  }
588
- // Track last assistant message for auto-compaction check
589
- _lastAssistantMessage = undefined;
590
789
  /** Internal handler for agent events - shared by subscribe and reconnect */
591
790
  _handleAgentEvent = async (event) => {
592
791
  // When a user message starts, check if it's from either queue and remove it BEFORE emitting
@@ -611,28 +810,30 @@ export class AgentSession {
611
810
  }
612
811
  }
613
812
  }
614
- // Emit to extensions first
813
+ // Emit to extensions first, then notify public listeners.
615
814
  await this._emitExtensionEvent(event);
616
- // Notify all listeners
617
815
  this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
618
816
  // Handle session persistence
619
817
  if (event.type === "message_end") {
818
+ let entryId;
620
819
  // Check if this is a custom message from extensions
621
820
  if (event.message.role === "custom") {
622
821
  // Persist as CustomMessageEntry
623
- this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
822
+ entryId = this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
624
823
  }
625
- else if (event.message.role === "user" ||
824
+ else if (event.message.role === "system" ||
825
+ event.message.role === "user" ||
626
826
  event.message.role === "assistant" ||
627
827
  event.message.role === "toolResult") {
628
828
  // Regular LLM message - persist as SessionMessageEntry
629
- this.sessionManager.appendMessage(event.message);
829
+ entryId = this.sessionManager.appendMessage(event.message);
630
830
  }
831
+ if (entryId)
832
+ this._entryIdsByMessage.set(event.message, entryId);
631
833
  // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
632
- // Track assistant message for auto-compaction (checked on agent_end)
633
834
  if (event.message.role === "assistant") {
634
- this._lastAssistantMessage = event.message;
635
835
  const assistantMsg = event.message;
836
+ this._lastAssistantMessage = assistantMsg;
636
837
  if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
637
838
  this._overflowRecoveryAttempted = false;
638
839
  }
@@ -654,10 +855,13 @@ export class AgentSession {
654
855
  // extension and listener dispatch above also picks up messages that turn_end
655
856
  // handlers queued.
656
857
  if (event.type === "turn_end") {
858
+ this._lastAssistantToolResults = event.toolResults;
657
859
  this._flushPendingCustomMessages();
658
860
  }
659
861
  };
660
862
  _willRetryAfterAgentEnd(event) {
863
+ if (this._agentRunAbortRequested)
864
+ return false;
661
865
  const settings = this.settingsManager.getRetrySettings();
662
866
  if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
663
867
  return false;
@@ -670,6 +874,47 @@ export class AgentSession {
670
874
  }
671
875
  return false;
672
876
  }
877
+ _findPersistedMessageEntryId(message) {
878
+ const mapped = this._entryIdsByMessage.get(message);
879
+ if (mapped)
880
+ return mapped;
881
+ for (const entry of [...this.sessionManager.getBranch()].reverse()) {
882
+ if (entry.type === "message" && entry.message === message)
883
+ return entry.id;
884
+ }
885
+ const messageIndex = this.agent.state.messages.indexOf(message);
886
+ if (messageIndex < 0)
887
+ return undefined;
888
+ const projection = this.sessionManager.buildSessionProjection();
889
+ let projectedIndex = 0;
890
+ for (const entry of projection.entries) {
891
+ for (let i = 0; i < entry.messages.length; i++) {
892
+ if (projectedIndex === messageIndex) {
893
+ this._entryIdsByMessage.set(message, entry.sourceEntry.id);
894
+ return entry.sourceEntry.id;
895
+ }
896
+ projectedIndex++;
897
+ }
898
+ }
899
+ return undefined;
900
+ }
901
+ _omitRecoveryAttempt(message, toolResults = []) {
902
+ const targets = [message, ...toolResults];
903
+ const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));
904
+ const unresolvedProjectedTarget = targets.some((target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target));
905
+ if (unresolvedProjectedTarget) {
906
+ throw new Error("Cannot persist recovery omission because a projected message has no source entry");
907
+ }
908
+ for (const targetId of targetIds) {
909
+ if (!targetId)
910
+ continue;
911
+ const editId = this.sessionManager.appendContextEdit(targetId, null);
912
+ const entry = this.sessionManager.getEntry(editId);
913
+ if (entry)
914
+ this._emit({ type: "entry_appended", entry });
915
+ }
916
+ this._refreshFinalizedContext();
917
+ }
673
918
  /** Find the last assistant message in agent state (including aborted ones) */
674
919
  _findLastAssistantMessage() {
675
920
  const messages = this.agent.state.messages;
@@ -725,13 +970,9 @@ export class AgentSession {
725
970
  await this._extensionRunner.emit(extensionEvent);
726
971
  }
727
972
  else if (event.type === "turn_end") {
728
- const extensionEvent = {
729
- type: "turn_end",
730
- turnIndex: this._turnIndex,
731
- message: event.message,
732
- toolResults: event.toolResults,
733
- };
734
- await this._extensionRunner.emit(extensionEvent);
973
+ if (event.message.role === "assistant" && !this._boundaryDispatchedMessages.delete(event.message)) {
974
+ await this._dispatchTurnEndBoundary(event.message, event.toolResults);
975
+ }
735
976
  if (this._hookRuntime?.hasHandlers("turn_end")) {
736
977
  await this._hookRuntime.emitObserve("turn_end", { type: "turn_end" });
737
978
  }
@@ -1041,15 +1282,32 @@ export class AgentSession {
1041
1282
  // Every connected MCP server is a child process this session started. Without
1042
1283
  // this, one leaks per server per session, because nothing else owns them.
1043
1284
  this._mcpRuntime?.close().catch(() => undefined);
1285
+ if (this._cacheWarmer) {
1286
+ this._cacheWarmer.onWarmed = undefined;
1287
+ this._cacheWarmer.cancel();
1288
+ }
1044
1289
  cleanupSessionResources(this.sessionId);
1045
1290
  }
1046
1291
  // =========================================================================
1047
1292
  // Read-only State Access
1048
1293
  // =========================================================================
1294
+ /** Refresh the public finalized transcript from the canonical session projection. */
1295
+ refreshContext() {
1296
+ this._refreshFinalizedContext();
1297
+ }
1049
1298
  /** Full agent state */
1050
1299
  get state() {
1051
1300
  return this.agent.state;
1052
1301
  }
1302
+ /** Current cache-warming state and the policy inputs that produced it. */
1303
+ get cacheWarmingStatus() {
1304
+ return this._cacheWarmer?.status;
1305
+ }
1306
+ /** Persist the cache-warming mode and immediately reconcile active warming. */
1307
+ setCacheWarmingMode(mode) {
1308
+ this.settingsManager.setCacheWarmingMode(mode);
1309
+ this._cacheWarmer?.onModeChanged();
1310
+ }
1053
1311
  /** Current model (may be undefined if not yet selected) */
1054
1312
  get model() {
1055
1313
  return this.agent.state.model;
@@ -1062,13 +1320,13 @@ export class AgentSession {
1062
1320
  get isStreaming() {
1063
1321
  return this._isAgentRunActive;
1064
1322
  }
1065
- /** Whether the session has no active agent run, retry, auto-compaction, or queued continuation. */
1323
+ /** Whether the session has no active agent run, compaction, branch summary, retry, or queued continuation. */
1066
1324
  get isIdle() {
1067
- return !this._isAgentRunActive;
1325
+ return !this._isAgentRunActive && !this.isCompacting;
1068
1326
  }
1069
- /** Current effective system prompt (includes any per-turn extension modifications) */
1327
+ /** Current effective system prompt, including changes not yet sent to the model. */
1070
1328
  get systemPrompt() {
1071
- return this.agent.state.systemPrompt;
1329
+ return buildSystemPrompt(this._runSystemPromptOptions ?? this._baseSystemPromptOptions);
1072
1330
  }
1073
1331
  /** Current retry attempt (0 if not retrying) */
1074
1332
  get retryAttempt() {
@@ -1124,9 +1382,7 @@ export class AgentSession {
1124
1382
  }
1125
1383
  }
1126
1384
  this.agent.state.tools = tools;
1127
- // Rebuild base system prompt with new tool set
1128
- this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
1129
- this.agent.state.systemPrompt = this._systemPromptOverride ?? this._baseSystemPrompt;
1385
+ this._rebuildSystemPrompt(validToolNames);
1130
1386
  }
1131
1387
  /** Whether compaction or branch summarization is currently running */
1132
1388
  get isCompacting() {
@@ -1195,23 +1451,17 @@ export class AgentSession {
1195
1451
  _rebuildSystemPrompt(toolNames) {
1196
1452
  const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
1197
1453
  const toolSnippets = {};
1198
- const promptGuidelines = [];
1199
- for (const name of validToolNames) {
1454
+ for (const name of this._toolRegistry.keys()) {
1200
1455
  const snippet = this._toolPromptSnippets.get(name);
1201
- if (snippet) {
1456
+ if (snippet)
1202
1457
  toolSnippets[name] = snippet;
1203
- }
1204
- const toolGuidelines = this._toolPromptGuidelines.get(name);
1205
- if (toolGuidelines) {
1206
- promptGuidelines.push(...toolGuidelines);
1207
- }
1208
1458
  }
1209
1459
  const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
1210
1460
  const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
1211
- const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined;
1461
+ const appendSystemPrompt = loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : "";
1212
1462
  const loadedSkills = this._resourceLoader.getSkills().skills;
1213
1463
  const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
1214
- this._baseSystemPromptOptions = {
1464
+ this._baseSystemPromptOptions = normalizeBuildSystemPromptOptions({
1215
1465
  cwd: this._cwd,
1216
1466
  skills: loadedSkills,
1217
1467
  contextFiles: loadedContextFiles,
@@ -1219,23 +1469,95 @@ export class AgentSession {
1219
1469
  appendSystemPrompt,
1220
1470
  selectedTools: validToolNames,
1221
1471
  toolSnippets,
1222
- promptGuidelines,
1472
+ toolGuidelines: Object.fromEntries(this._toolPromptGuidelines),
1473
+ });
1474
+ }
1475
+ /**
1476
+ * Apply a prompt and tool loadout for the next request. Sets the executable tools and
1477
+ * returns a system message patching the prompt sections the model currently has (replayed
1478
+ * from `messages`), or undefined when the prompt is unchanged. Tool changes are declared by
1479
+ * the agent loop before the request.
1480
+ *
1481
+ * A forced prompt does not affect the transcript: the structured sections are still diffed
1482
+ * and persisted, and the forced text is projected onto the request by
1483
+ * {@link _installAgentForcedPromptProjection}.
1484
+ */
1485
+ _preparePromptAndToolLoadout(options, messages = this.agent.state.messages) {
1486
+ options.selectedTools = [...new Set(options.selectedTools)].filter((name) => this._toolRegistry.has(name));
1487
+ this.agent.state.tools = options.selectedTools.flatMap((name) => {
1488
+ const tool = this._toolRegistry.get(name);
1489
+ return tool ? [tool] : [];
1490
+ });
1491
+ const sections = diffSystemPromptSections(getCurrentSystemMessage(messages)?.sections ?? {}, buildSystemPromptSections(options));
1492
+ return sections ? { role: "system", content: "", sections, timestamp: Date.now() } : undefined;
1493
+ }
1494
+ /**
1495
+ * Send a forced prompt as the provider's leading system prompt without recording it.
1496
+ *
1497
+ * A `before_agent_start` handler that returns `systemPrompt` needs that exact text at the
1498
+ * head of the request; a mid-conversation system message would leave the original prompt
1499
+ * in place. The forced text is a rendering of the current prompt, so the transcript keeps
1500
+ * its structured sections and the request is projected instead: the system messages
1501
+ * collapse into one head holding the forced text and the current tools. Runs after the
1502
+ * `context` extension handlers.
1503
+ */
1504
+ _installAgentForcedPromptProjection() {
1505
+ const previousTransformContext = this.agent.transformContext;
1506
+ this.agent.transformContext = async (messages, signal) => {
1507
+ const transformed = previousTransformContext ? await previousTransformContext(messages, signal) : messages;
1508
+ const forced = this._runSystemPromptOptions?.forceSystemPrompt;
1509
+ if (forced === undefined)
1510
+ return transformed;
1511
+ const current = getCurrentSystemMessage(transformed);
1512
+ const head = {
1513
+ role: "system",
1514
+ content: forced,
1515
+ ...(current?.toolsAdded ? { toolsAdded: current.toolsAdded } : {}),
1516
+ timestamp: current?.timestamp ?? Date.now(),
1517
+ };
1518
+ return [head, ...transformed.filter((message) => message.role !== "system")];
1223
1519
  };
1224
- return buildSystemPrompt(this._baseSystemPromptOptions);
1520
+ }
1521
+ /** Restore the active tool loadout declared by the session transcript, if it declares one. */
1522
+ _restoreToolsFromTranscript() {
1523
+ const current = getCurrentSystemMessage(this.sessionManager.buildSessionContext().messages);
1524
+ if (!current)
1525
+ return;
1526
+ const toolNames = (current.toolsAdded ?? [])
1527
+ .map((tool) => tool.name)
1528
+ .filter((name) => this._toolRegistry.has(name));
1529
+ this.agent.state.tools = toolNames.flatMap((name) => {
1530
+ const registered = this._toolRegistry.get(name);
1531
+ return registered ? [registered] : [];
1532
+ });
1533
+ this._rebuildSystemPrompt(toolNames);
1225
1534
  }
1226
1535
  // =========================================================================
1227
1536
  // Prompting
1228
1537
  // =========================================================================
1229
1538
  async _runAgentPrompt(messages) {
1539
+ this._agentRunAbortRequested = false;
1230
1540
  this._isAgentRunActive = true;
1231
1541
  try {
1232
1542
  await this.agent.prompt(messages);
1233
- while (await this._handlePostAgentRun()) {
1543
+ while (!this._agentRunAbortRequested) {
1544
+ if (await this._handlePostAgentRun()) {
1545
+ if (this._agentRunAbortRequested)
1546
+ break;
1547
+ await this.agent.continue();
1548
+ continue;
1549
+ }
1550
+ if (this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary()))
1551
+ break;
1552
+ if (this._agentRunAbortRequested)
1553
+ break;
1234
1554
  await this.agent.continue();
1235
1555
  }
1236
1556
  }
1237
1557
  finally {
1238
- this._systemPromptOverride = undefined;
1558
+ if (this._agentRunAbortRequested)
1559
+ this._finishCancelledRetry();
1560
+ this._runSystemPromptOptions = undefined;
1239
1561
  this._flushPendingBashMessages();
1240
1562
  this._flushPendingCustomMessages();
1241
1563
  await this._emitAgentSettled();
@@ -1246,29 +1568,96 @@ export class AgentSession {
1246
1568
  this._midRunCompactionNeeded = false;
1247
1569
  return await this._runAutoCompaction("threshold", true);
1248
1570
  }
1249
- const msg = this._lastAssistantMessage;
1571
+ const message = this._lastAssistantMessage;
1572
+ const toolResults = this._lastAssistantToolResults;
1250
1573
  this._lastAssistantMessage = undefined;
1251
- if (!msg) {
1574
+ this._lastAssistantToolResults = [];
1575
+ if (this._agentRunAbortRequested) {
1576
+ this._finishCancelledRetry();
1252
1577
  return false;
1253
1578
  }
1254
- if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
1255
- return true;
1579
+ if (!message)
1580
+ return this.agent.hasQueuedMessages();
1581
+ if (this._isRetryableError(message) && (await this._prepareRetry(message))) {
1582
+ if (this._agentRunAbortRequested)
1583
+ this._finishCancelledRetry();
1584
+ return !this._agentRunAbortRequested;
1585
+ }
1586
+ if (this._agentRunAbortRequested) {
1587
+ this._finishCancelledRetry();
1588
+ return false;
1256
1589
  }
1257
- if (msg.stopReason === "error" && this._retryAttempt > 0) {
1590
+ if (message.stopReason === "error" && this._retryAttempt > 0) {
1258
1591
  this._emit({
1259
1592
  type: "auto_retry_end",
1260
1593
  success: false,
1261
1594
  attempt: this._retryAttempt,
1262
- finalError: msg.errorMessage,
1595
+ finalError: message.errorMessage,
1263
1596
  });
1264
1597
  this._retryAttempt = 0;
1265
1598
  }
1266
- if (await this._checkCompaction(msg)) {
1267
- return true;
1599
+ if (await this._checkCompaction(message, true, toolResults)) {
1600
+ return !this._agentRunAbortRequested;
1601
+ }
1602
+ // The low-level loop drains both queues before agent_end. Messages queued by
1603
+ // agent_end handlers require a fresh run before pre-settlement handlers fire.
1604
+ return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
1605
+ }
1606
+ async _runBeforeSettleBoundary() {
1607
+ if (!this._extensionRunner.hasHandlers("agent_before_settle"))
1608
+ return this.agent.hasQueuedMessages();
1609
+ this._isBeforeSettle = true;
1610
+ this._abortDuringBeforeSettle = false;
1611
+ try {
1612
+ const result = await this._extensionRunner.emitBoundary({ type: "agent_before_settle", outcome: this._lastActivityOutcome }, (entries) => this._buildBoundaryContext(entries, "agent_before_settle"));
1613
+ this._commitBoundaryDrafts(result.entries);
1614
+ this._flushPendingCustomMessages();
1615
+ const finalContext = this._buildBoundaryContext([], "agent_before_settle");
1616
+ if (this._abortDuringBeforeSettle)
1617
+ return false;
1618
+ const shouldContinue = result.continue || this.agent.hasQueuedMessages();
1619
+ if (shouldContinue && !finalContext.canContinue) {
1620
+ if (result.continue)
1621
+ this._reportInvalidBoundaryContinuation("agent_before_settle");
1622
+ return false;
1623
+ }
1624
+ return shouldContinue;
1625
+ }
1626
+ finally {
1627
+ this._isBeforeSettle = false;
1628
+ }
1629
+ }
1630
+ async _runInputHandlers(text, images, source, streamingBehavior) {
1631
+ if (!this._extensionRunner.hasHandlers("input")) {
1632
+ return { text, images };
1633
+ }
1634
+ const inputResult = await this._extensionRunner.emitInput(text, images, source, streamingBehavior);
1635
+ if (inputResult.action === "handled") {
1636
+ return undefined;
1637
+ }
1638
+ if (inputResult.action === "transform") {
1639
+ return { text: inputResult.text, images: inputResult.images ?? images };
1640
+ }
1641
+ return { text, images };
1642
+ }
1643
+ async _normalizePromptImages(images) {
1644
+ if (!images)
1645
+ return { images: [], hints: [] };
1646
+ const normalizedImages = [];
1647
+ const hints = [];
1648
+ for (const image of images) {
1649
+ const processed = await processImage(Buffer.from(image.data, "base64"), image.mimeType, {
1650
+ autoResizeImages: this.settingsManager.getImageAutoResize(),
1651
+ resizeOptions: this.model?.inputLimits?.images?.resize,
1652
+ });
1653
+ if (!processed.ok) {
1654
+ hints.push(processed.message);
1655
+ continue;
1656
+ }
1657
+ normalizedImages.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
1658
+ hints.push(...processed.hints);
1268
1659
  }
1269
- // The agent loop drains both queues before emitting agent_end. Any messages
1270
- // here were queued by agent_end extension handlers and need a continuation.
1271
- return this.agent.hasQueuedMessages();
1660
+ return { images: normalizedImages, hints };
1272
1661
  }
1273
1662
  /**
1274
1663
  * Send a prompt to the agent.
@@ -1280,6 +1669,10 @@ export class AgentSession {
1280
1669
  * @throws Error if no model selected or no API key available (when not streaming)
1281
1670
  */
1282
1671
  async prompt(text, options) {
1672
+ if (this._isEmittingAgentSettled) {
1673
+ this._deferredSettledActions.push(async () => await this.prompt(text, options));
1674
+ return;
1675
+ }
1283
1676
  if (this._lifecycle === "closed")
1284
1677
  throw new Error("Session is closed.");
1285
1678
  if (this._activePrompt) {
@@ -1325,19 +1718,12 @@ export class AgentSession {
1325
1718
  throw new Error("Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.");
1326
1719
  }
1327
1720
  // Emit input event for extension interception (before skill/template expansion)
1328
- let currentText = text;
1329
- let currentImages = options?.images;
1330
- if (this._extensionRunner.hasHandlers("input")) {
1331
- const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
1332
- if (inputResult.action === "handled") {
1333
- preflightResult?.(true);
1334
- return;
1335
- }
1336
- if (inputResult.action === "transform") {
1337
- currentText = inputResult.text;
1338
- currentImages = inputResult.images ?? currentImages;
1339
- }
1721
+ const processedInput = await this._runInputHandlers(text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined);
1722
+ if (!processedInput) {
1723
+ preflightResult?.(true);
1724
+ return;
1340
1725
  }
1726
+ const { text: currentText, images: currentImages } = processedInput;
1341
1727
  // Expand skill commands (/skill:name args) and prompt templates (/template args)
1342
1728
  let expandedText = currentText;
1343
1729
  if (expandPromptTemplates) {
@@ -1382,13 +1768,23 @@ export class AgentSession {
1382
1768
  if (lastAssistant) {
1383
1769
  await this._checkCompaction(lastAssistant, false);
1384
1770
  }
1385
- // Build messages array (custom message if any, then user message)
1771
+ // Emit before_agent_start before normalizing images so extension-driven model
1772
+ // selection determines the resize profile used for the request and history.
1773
+ const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
1774
+ const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
1775
+ // Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
1776
+ // which updates the live loadout instead. An explicit edit wins; otherwise the live
1777
+ // loadout is authoritative, so a setActiveTools() call is not undone here.
1778
+ const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
1779
+ result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
1780
+ if (!handlerEditedTools)
1781
+ result.systemPromptOptions.selectedTools = this.getActiveToolNames();
1782
+ const normalized = await this._normalizePromptImages(currentImages);
1783
+ const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
1784
+ // Build messages only after hooks and image normalization have completed.
1386
1785
  messages = [];
1387
- // Add user message
1388
- const userContent = [{ type: "text", text: expandedText }];
1389
- if (currentImages) {
1390
- userContent.push(...currentImages);
1391
- }
1786
+ const userContent = [{ type: "text", text: userText }];
1787
+ userContent.push(...normalized.images);
1392
1788
  messages.push({
1393
1789
  role: "user",
1394
1790
  content: userContent,
@@ -1399,32 +1795,21 @@ export class AgentSession {
1399
1795
  messages.push(msg);
1400
1796
  }
1401
1797
  this._pendingNextTurnMessages = [];
1402
- // Emit before_agent_start extension event
1403
- const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPrompt, this._baseSystemPromptOptions);
1404
- // Add all custom messages from extensions
1405
- if (result?.messages) {
1406
- for (const msg of result.messages) {
1407
- messages.push({
1408
- role: "custom",
1409
- customType: msg.customType,
1410
- // Untyped extensions can pass null/missing content; normalize at ingestion.
1411
- content: msg.content ?? [],
1412
- display: msg.display,
1413
- details: msg.details,
1414
- timestamp: Date.now(),
1415
- });
1416
- }
1417
- }
1418
- // Apply extension-modified system prompt, or reset to base
1419
- if (result?.systemPrompt !== undefined) {
1420
- this._systemPromptOverride = result.systemPrompt;
1421
- this.agent.state.systemPrompt = result.systemPrompt;
1422
- }
1423
- else {
1424
- // Ensure we're using the base prompt (in case previous turn had modifications)
1425
- this._systemPromptOverride = undefined;
1426
- this.agent.state.systemPrompt = this._baseSystemPrompt;
1798
+ for (const msg of result.messages) {
1799
+ messages.push({
1800
+ role: "custom",
1801
+ customType: msg.customType,
1802
+ // Untyped extensions can pass null/missing content; normalize at ingestion.
1803
+ content: msg.content ?? [],
1804
+ display: msg.display,
1805
+ details: msg.details,
1806
+ timestamp: Date.now(),
1807
+ });
1427
1808
  }
1809
+ const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);
1810
+ this._runSystemPromptOptions = result.systemPromptOptions;
1811
+ if (updateMessage)
1812
+ messages.unshift(updateMessage);
1428
1813
  }
1429
1814
  catch (error) {
1430
1815
  preflightResult?.(false);
@@ -1504,40 +1889,44 @@ export class AgentSession {
1504
1889
  return text; // Return original on error
1505
1890
  }
1506
1891
  }
1892
+ async _queueUserInput(text, images, behavior, source) {
1893
+ if (text.startsWith("/")) {
1894
+ this._throwIfExtensionCommand(text);
1895
+ }
1896
+ const processedInput = await this._runInputHandlers(text, images, source, this.isStreaming ? behavior : undefined);
1897
+ if (!processedInput)
1898
+ return;
1899
+ let expandedText = this._expandSkillCommand(processedInput.text);
1900
+ expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1901
+ if (behavior === "steer") {
1902
+ await this._queueSteer(expandedText, processedInput.images);
1903
+ }
1904
+ else {
1905
+ await this._queueFollowUp(expandedText, processedInput.images);
1906
+ }
1907
+ }
1507
1908
  /**
1508
1909
  * Queue a steering message while the agent is running.
1509
1910
  * Delivered after the current assistant turn finishes executing its tool calls,
1510
1911
  * before the next LLM call.
1511
1912
  * Expands skill commands and prompt templates. Errors on extension commands.
1512
1913
  * @param images Optional image attachments to include with the message
1914
+ * @param options Input source; defaults to interactive
1513
1915
  * @throws Error if text is an extension command
1514
1916
  */
1515
- async steer(text, images) {
1516
- // Check for extension commands (cannot be queued)
1517
- if (text.startsWith("/")) {
1518
- this._throwIfExtensionCommand(text);
1519
- }
1520
- // Expand skill commands and prompt templates
1521
- let expandedText = this._expandSkillCommand(text);
1522
- expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1523
- await this._queueSteer(expandedText, images);
1917
+ async steer(text, images, options) {
1918
+ await this._queueUserInput(text, images, "steer", options?.source ?? "interactive");
1524
1919
  }
1525
1920
  /**
1526
1921
  * Queue a follow-up message to be processed after the agent finishes.
1527
1922
  * Delivered only when agent has no more tool calls or steering messages.
1528
1923
  * Expands skill commands and prompt templates. Errors on extension commands.
1529
1924
  * @param images Optional image attachments to include with the message
1925
+ * @param options Input source; defaults to interactive
1530
1926
  * @throws Error if text is an extension command
1531
1927
  */
1532
- async followUp(text, images) {
1533
- // Check for extension commands (cannot be queued)
1534
- if (text.startsWith("/")) {
1535
- this._throwIfExtensionCommand(text);
1536
- }
1537
- // Expand skill commands and prompt templates
1538
- let expandedText = this._expandSkillCommand(text);
1539
- expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);
1540
- await this._queueFollowUp(expandedText, images);
1928
+ async followUp(text, images, options) {
1929
+ await this._queueUserInput(text, images, "followUp", options?.source ?? "interactive");
1541
1930
  }
1542
1931
  /**
1543
1932
  * Internal: Queue a steering message (already expanded, no extension command check).
@@ -1565,11 +1954,7 @@ export class AgentSession {
1565
1954
  if (images) {
1566
1955
  content.push(...images);
1567
1956
  }
1568
- this.agent.followUp({
1569
- role: "user",
1570
- content,
1571
- timestamp: Date.now(),
1572
- });
1957
+ this.agent.followUp({ role: "user", content, timestamp: Date.now() });
1573
1958
  }
1574
1959
  /**
1575
1960
  * Throw an error if the text is an extension command.
@@ -1617,6 +2002,10 @@ export class AgentSession {
1617
2002
  }
1618
2003
  }
1619
2004
  else if (options?.triggerTurn) {
2005
+ if (this._isEmittingAgentSettled) {
2006
+ this._deferredSettledActions.push(async () => await this._runAgentPrompt(appMessage));
2007
+ return;
2008
+ }
1620
2009
  await this._runAgentPrompt(appMessage);
1621
2010
  }
1622
2011
  else if (this.isStreaming) {
@@ -1631,8 +2020,8 @@ export class AgentSession {
1631
2020
  }
1632
2021
  }
1633
2022
  _appendCustomMessage(appMessage) {
1634
- this.agent.state.messages.push(appMessage);
1635
2023
  this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
2024
+ this._refreshFinalizedContext();
1636
2025
  this._emit({ type: "message_start", message: appMessage });
1637
2026
  this._emit({ type: "message_end", message: appMessage });
1638
2027
  }
@@ -1721,7 +2110,14 @@ export class AgentSession {
1721
2110
  async abort() {
1722
2111
  if (this._activePrompt && this._lifecycle !== "closed")
1723
2112
  this._lifecycle = "interrupted";
2113
+ if (this._isAgentRunActive) {
2114
+ this._agentRunAbortRequested = true;
2115
+ }
1724
2116
  this.abortRetry();
2117
+ this.abortCompaction();
2118
+ this.abortBranchSummary();
2119
+ if (this._isBeforeSettle)
2120
+ this._abortDuringBeforeSettle = true;
1725
2121
  this.agent.abort();
1726
2122
  await this.waitForIdle();
1727
2123
  }
@@ -2195,6 +2591,10 @@ export class AgentSession {
2195
2591
  this.agent.recordMaintenanceRequest();
2196
2592
  return compact(preparation, requestModel, apiKey, headers, customInstructions, signal, this.thinkingLevel, this.agent.streamFunction, env, this.settingsManager.getRetrySettings(), this._summarizationRetryCallbacks({ source: "compaction", reason }), undefined);
2197
2593
  }
2594
+ _clearManualCompactionState() {
2595
+ this._compactionAbortController = undefined;
2596
+ this._resolveIdleWaitIfIdle();
2597
+ }
2198
2598
  /**
2199
2599
  * Manually compact the session context.
2200
2600
  *
@@ -2215,13 +2615,15 @@ export class AgentSession {
2215
2615
  this._compactionAbortController = new AbortController();
2216
2616
  this._emit({ type: "compaction_start", reason: "manual" });
2217
2617
  let fromExtension = false;
2618
+ let cancelledByExtension = false;
2218
2619
  try {
2219
- if (!this.model) {
2620
+ const model = this.model;
2621
+ if (!model) {
2220
2622
  throw new Error(formatNoModelSelectedMessage());
2221
2623
  }
2222
- const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
2624
+ const settings = this.settingsManager.getCompactionSettings(model);
2625
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, this._compactionAbortController.signal);
2223
2626
  const pathEntries = this.sessionManager.getBranch();
2224
- const settings = this.settingsManager.getCompactionSettings();
2225
2627
  const preparation = prepareCompaction(pathEntries, settings);
2226
2628
  if (!preparation) {
2227
2629
  // Check why we can't compact
@@ -2249,6 +2651,7 @@ export class AgentSession {
2249
2651
  signal: this._compactionAbortController.signal,
2250
2652
  }));
2251
2653
  if (result?.cancel) {
2654
+ cancelledByExtension = true;
2252
2655
  throw new Error("Compaction cancelled");
2253
2656
  }
2254
2657
  if (result?.compaction) {
@@ -2306,9 +2709,8 @@ export class AgentSession {
2306
2709
  }
2307
2710
  }
2308
2711
  const newEntries = this.sessionManager.getEntries();
2309
- const sessionContext = this.sessionManager.buildSessionContext();
2310
- this.agent.state.messages = sessionContext.messages;
2311
- const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
2712
+ this._refreshFinalizedContext();
2713
+ const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2312
2714
  // Get the saved compaction entry for the extension event
2313
2715
  const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2314
2716
  if (this._extensionRunner && savedCompactionEntry) {
@@ -2329,7 +2731,7 @@ export class AgentSession {
2329
2731
  details,
2330
2732
  };
2331
2733
  // compaction_end listeners may submit queued prompts, so expose idle state before notifying them.
2332
- this._compactionAbortController = undefined;
2734
+ this._clearManualCompactionState();
2333
2735
  this._emit({
2334
2736
  type: "compaction_end",
2335
2737
  reason: "manual",
@@ -2341,9 +2743,9 @@ export class AgentSession {
2341
2743
  }
2342
2744
  catch (error) {
2343
2745
  const message = error instanceof Error ? error.message : String(error);
2344
- const aborted = message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError");
2746
+ const aborted = this._compactionAbortController.signal.aborted || cancelledByExtension;
2345
2747
  const errorMessage = aborted ? undefined : `Compaction failed: ${message}`;
2346
- this._compactionAbortController = undefined;
2748
+ this._clearManualCompactionState();
2347
2749
  this._emit({
2348
2750
  type: "compaction_end",
2349
2751
  reason: "manual",
@@ -2362,7 +2764,7 @@ export class AgentSession {
2362
2764
  throw error;
2363
2765
  }
2364
2766
  finally {
2365
- this._compactionAbortController = undefined;
2767
+ this._clearManualCompactionState();
2366
2768
  }
2367
2769
  }
2368
2770
  /**
@@ -2399,8 +2801,8 @@ export class AgentSession {
2399
2801
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
2400
2802
  * @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
2401
2803
  */
2402
- async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
2403
- const settings = this.settingsManager.getCompactionSettings();
2804
+ async _checkCompaction(assistantMessage, skipAbortedCheck = true, toolResults = []) {
2805
+ const settings = this.settingsManager.getCompactionSettings(this.model);
2404
2806
  if (!settings.enabled)
2405
2807
  return false;
2406
2808
  // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
@@ -2423,8 +2825,27 @@ export class AgentSession {
2423
2825
  // Automatic cases 1 and 2: context overflow.
2424
2826
  // A length stop is recoverable when output ended below the model's original desired limit,
2425
2827
  // independent of the configured context size or any context-clamped provider request limit.
2426
- const contextOverflow = sameModel && isContextOverflow(assistantMessage, contextWindow);
2427
- const recoverableLength = sameModel && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
2828
+ const currentProjection = this.sessionManager.buildSessionProjection();
2829
+ const assistantEntryId = this._findPersistedMessageEntryId(assistantMessage);
2830
+ const assistantIsProjected = assistantEntryId === undefined ||
2831
+ currentProjection.entries.some((entry) => entry.sourceEntry.id === assistantEntryId &&
2832
+ entry.messages.some((message) => message.role === "assistant"));
2833
+ const branch = this.sessionManager.getBranch();
2834
+ const assistantIndex = assistantEntryId ? branch.findIndex((entry) => entry.id === assistantEntryId) : -1;
2835
+ const entriesAfterAssistant = assistantIndex >= 0 ? branch.slice(assistantIndex + 1) : [];
2836
+ const hasPostAssistantContextEdit = entriesAfterAssistant.some((entry) => entry.type === "context_edit");
2837
+ const latestAssistantEdit = entriesAfterAssistant
2838
+ .filter((entry) => entry.type === "context_edit" && entry.targetId === assistantEntryId)
2839
+ .at(-1);
2840
+ const assistantRetainedForExplicitRecovery = assistantEntryId === undefined ||
2841
+ (!entriesAfterAssistant.some((entry) => entry.type === "compaction") &&
2842
+ latestAssistantEdit?.replacement !== null);
2843
+ const assistantUsageMatchesProjection = assistantIsProjected && !hasPostAssistantContextEdit;
2844
+ const explicitOverflow = assistantMessage.stopReason === "error" && isContextOverflow(assistantMessage);
2845
+ const contextOverflow = sameModel &&
2846
+ ((explicitOverflow && assistantRetainedForExplicitRecovery) ||
2847
+ (assistantUsageMatchesProjection && isContextOverflow(assistantMessage, contextWindow)));
2848
+ const recoverableLength = sameModel && assistantIsProjected && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
2428
2849
  if (contextOverflow || recoverableLength) {
2429
2850
  const willRetry = assistantMessage.stopReason !== "stop";
2430
2851
  // Case 2: the response completed successfully. Compact, but do not retry because
@@ -2453,13 +2874,9 @@ export class AgentSession {
2453
2874
  });
2454
2875
  return false;
2455
2876
  }
2456
- // Case 1: remove the failed or truncated message from agent state, compact, and
2457
- // retry once. The message remains in session history but is excluded from retry context.
2877
+ // Persistently omit the selected final attempt before post-run recovery compaction.
2458
2878
  this._overflowRecoveryAttempted = true;
2459
- const messages = this.agent.state.messages;
2460
- if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2461
- this.agent.state.messages = messages.slice(0, -1);
2462
- }
2879
+ this._omitRecoveryAttempt(assistantMessage, toolResults);
2463
2880
  return await this._runAutoCompaction("overflow", willRetry);
2464
2881
  }
2465
2882
  // Case 3: threshold compaction without retry.
@@ -2467,8 +2884,13 @@ export class AgentSession {
2467
2884
  // This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
2468
2885
  // responses can still compact and do not reset context accounting.
2469
2886
  let contextTokens;
2887
+ const projection = currentProjection;
2888
+ const hasContextEdits = projection.entries.some((entry) => entry.sourceEntry.type === "context_edit");
2470
2889
  const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
2471
- if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
2890
+ if (hasContextEdits) {
2891
+ contextTokens = estimateProjectedContextTokens(projection, branch).tokens;
2892
+ }
2893
+ else if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
2472
2894
  const messages = this.agent.state.messages;
2473
2895
  const estimate = estimateContextTokens(messages);
2474
2896
  // Without provider usage, estimate.tokens is the pure message-size estimate.
@@ -2505,22 +2927,28 @@ export class AgentSession {
2505
2927
  * @returns Whether the post-run loop should call `agent.continue()`
2506
2928
  */
2507
2929
  async _runAutoCompaction(reason, willRetry) {
2508
- const settings = this.settingsManager.getCompactionSettings();
2930
+ const model = this.model;
2931
+ const settings = this.settingsManager.getCompactionSettings(model);
2932
+ let abortController;
2509
2933
  let started = false;
2510
2934
  let fromExtension = false;
2935
+ let cancelledByExtension = false;
2511
2936
  try {
2512
- if (!this.model) {
2937
+ if (!model) {
2513
2938
  return false;
2514
2939
  }
2515
- const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(this.model);
2516
2940
  const pathEntries = this.sessionManager.getBranch();
2517
2941
  const preparation = prepareCompaction(pathEntries, settings);
2518
2942
  if (!preparation) {
2519
2943
  return false;
2520
2944
  }
2521
- this._emit({ type: "compaction_start", reason });
2522
- this._autoCompactionAbortController = new AbortController();
2945
+ abortController = new AbortController();
2946
+ this._autoCompactionAbortController = abortController;
2523
2947
  started = true;
2948
+ this._emit({ type: "compaction_start", reason });
2949
+ abortController.signal.throwIfAborted();
2950
+ const { model: requestModel, apiKey, headers, env, } = await this._getSummarizationRequestAuth(model, abortController.signal);
2951
+ abortController.signal.throwIfAborted();
2524
2952
  let extensionCompaction;
2525
2953
  if (this._hookRuntime?.hasHandlers("session_before_compact")) {
2526
2954
  await this._hookRuntime.emitObserve("session_before_compact", { type: "session_before_compact", reason });
@@ -2533,29 +2961,18 @@ export class AgentSession {
2533
2961
  customInstructions: undefined,
2534
2962
  reason,
2535
2963
  willRetry,
2536
- signal: this._autoCompactionAbortController.signal,
2964
+ signal: abortController.signal,
2537
2965
  }));
2538
2966
  if (extensionResult?.cancel) {
2539
- this._emit({
2540
- type: "compaction_end",
2541
- reason,
2542
- result: undefined,
2543
- aborted: true,
2544
- willRetry: false,
2545
- });
2546
- await this._emitSessionCompactFailed({
2547
- reason,
2548
- aborted: true,
2549
- willRetry: false,
2550
- fromExtension: false,
2551
- });
2552
- return false;
2967
+ cancelledByExtension = true;
2968
+ throw new Error("Compaction cancelled");
2553
2969
  }
2554
2970
  if (extensionResult?.compaction) {
2555
2971
  extensionCompaction = extensionResult.compaction;
2556
2972
  fromExtension = true;
2557
2973
  }
2558
2974
  }
2975
+ abortController.signal.throwIfAborted();
2559
2976
  let summary;
2560
2977
  let firstKeptEntryId;
2561
2978
  let tokensBefore;
@@ -2577,7 +2994,7 @@ export class AgentSession {
2577
2994
  }
2578
2995
  else {
2579
2996
  // Shared default summary generator, also used by manual compaction.
2580
- const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, this._autoCompactionAbortController.signal, env, reason);
2997
+ const compactResult = await this._runDefaultCompaction(preparation, requestModel, apiKey, headers, undefined, abortController.signal, env, reason);
2581
2998
  summary = compactResult.summary;
2582
2999
  firstKeptEntryId = compactResult.firstKeptEntryId;
2583
3000
  tokensBefore = compactResult.tokensBefore;
@@ -2587,22 +3004,7 @@ export class AgentSession {
2587
3004
  summary += `\n\n${formatWorkspaceProjection(workspaceRecord)}`;
2588
3005
  }
2589
3006
  }
2590
- if (this._autoCompactionAbortController.signal.aborted) {
2591
- this._emit({
2592
- type: "compaction_end",
2593
- reason,
2594
- result: undefined,
2595
- aborted: true,
2596
- willRetry: false,
2597
- });
2598
- await this._emitSessionCompactFailed({
2599
- reason,
2600
- aborted: true,
2601
- willRetry: false,
2602
- fromExtension,
2603
- });
2604
- return false;
2605
- }
3007
+ abortController.signal.throwIfAborted();
2606
3008
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
2607
3009
  if (workspaceRecord) {
2608
3010
  try {
@@ -2619,9 +3021,8 @@ export class AgentSession {
2619
3021
  }
2620
3022
  }
2621
3023
  const newEntries = this.sessionManager.getEntries();
2622
- const sessionContext = this.sessionManager.buildSessionContext();
2623
- this.agent.state.messages = sessionContext.messages;
2624
- const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
3024
+ this._refreshFinalizedContext();
3025
+ const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
2625
3026
  // Get the saved compaction entry for the extension event
2626
3027
  const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
2627
3028
  if (this._extensionRunner && savedCompactionEntry) {
@@ -2642,40 +3043,33 @@ export class AgentSession {
2642
3043
  details,
2643
3044
  };
2644
3045
  this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
2645
- if (willRetry) {
2646
- const messages = this.agent.state.messages;
2647
- const lastMsg = messages[messages.length - 1];
2648
- // The overflow response was persisted on message_end before _checkCompaction() removed it
2649
- // from agent state. Rebuilding state from the new compaction can restore that kept entry,
2650
- // leaving an assistant as the final message. agent.continue() rejects that state, so remove
2651
- // the retriable error or truncated-length response again before continuing the interrupted turn.
2652
- if (lastMsg?.role === "assistant" && (lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
2653
- this.agent.state.messages = messages.slice(0, -1);
2654
- }
3046
+ if (willRetry)
2655
3047
  return true;
2656
- }
2657
3048
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
2658
3049
  // Continue once so queued messages are delivered.
2659
3050
  return this.agent.hasQueuedMessages();
2660
3051
  }
2661
3052
  catch (error) {
2662
- const errorMessage = error instanceof Error ? error.message : "compaction failed";
3053
+ const message = error instanceof Error ? error.message : "compaction failed";
3054
+ const aborted = abortController?.signal.aborted === true || cancelledByExtension;
2663
3055
  if (started) {
2664
- const formattedErrorMessage = reason === "overflow"
2665
- ? `Context overflow recovery failed: ${errorMessage}`
2666
- : `Auto-compaction failed: ${errorMessage}`;
3056
+ const errorMessage = aborted
3057
+ ? undefined
3058
+ : reason === "overflow"
3059
+ ? `Context overflow recovery failed: ${message}`
3060
+ : `Auto-compaction failed: ${message}`;
2667
3061
  this._emit({
2668
3062
  type: "compaction_end",
2669
3063
  reason,
2670
3064
  result: undefined,
2671
- aborted: false,
3065
+ aborted,
2672
3066
  willRetry: false,
2673
- errorMessage: formattedErrorMessage,
3067
+ errorMessage,
2674
3068
  });
2675
3069
  await this._emitSessionCompactFailed({
2676
3070
  reason,
2677
- errorMessage: formattedErrorMessage,
2678
- aborted: false,
3071
+ errorMessage,
3072
+ aborted,
2679
3073
  willRetry: false,
2680
3074
  fromExtension,
2681
3075
  });
@@ -2683,7 +3077,10 @@ export class AgentSession {
2683
3077
  return false;
2684
3078
  }
2685
3079
  finally {
2686
- this._autoCompactionAbortController = undefined;
3080
+ if (this._autoCompactionAbortController === abortController) {
3081
+ this._autoCompactionAbortController = undefined;
3082
+ }
3083
+ this._resolveIdleWaitIfIdle();
2687
3084
  }
2688
3085
  }
2689
3086
  /**
@@ -2733,8 +3130,7 @@ export class AgentSession {
2733
3130
  themePaths: this.buildExtensionResourcePaths(themePaths),
2734
3131
  };
2735
3132
  this._resourceLoader.extendResources(extensionPaths);
2736
- this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
2737
- this.agent.state.systemPrompt = this._baseSystemPrompt;
3133
+ this._rebuildSystemPrompt(this.getActiveToolNames());
2738
3134
  }
2739
3135
  buildExtensionResourcePaths(entries) {
2740
3136
  return entries.map((entry) => {
@@ -3113,6 +3509,18 @@ export class AgentSession {
3113
3509
  },
3114
3510
  };
3115
3511
  }
3512
+ _finishCancelledRetry() {
3513
+ if (this._retryAttempt === 0)
3514
+ return;
3515
+ const attempt = this._retryAttempt;
3516
+ this._retryAttempt = 0;
3517
+ this._emit({
3518
+ type: "auto_retry_end",
3519
+ success: false,
3520
+ attempt,
3521
+ finalError: "Retry cancelled",
3522
+ });
3523
+ }
3116
3524
  /**
3117
3525
  * Prepare a retryable error for continuation with exponential backoff.
3118
3526
  * @returns true if the caller should continue the agent, false otherwise
@@ -3128,7 +3536,7 @@ export class AgentSession {
3128
3536
  this._retryAttempt--;
3129
3537
  return false;
3130
3538
  }
3131
- const delayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
3539
+ const delayMs = retryDelayMs(settings, this._retryAttempt);
3132
3540
  this._emit({
3133
3541
  type: "auto_retry_start",
3134
3542
  attempt: this._retryAttempt,
@@ -3136,11 +3544,8 @@ export class AgentSession {
3136
3544
  delayMs,
3137
3545
  errorMessage: message.errorMessage || "Unknown error",
3138
3546
  });
3139
- // Remove error message from agent state (keep in session for history)
3140
- const messages = this.agent.state.messages;
3141
- if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
3142
- this.agent.state.messages = messages.slice(0, -1);
3143
- }
3547
+ // Keep the failed attempt in raw history while durably omitting it from model projection.
3548
+ this._omitRecoveryAttempt(message);
3144
3549
  // Wait with exponential backoff (abortable)
3145
3550
  this._retryAbortController = new AbortController();
3146
3551
  try {
@@ -3148,14 +3553,7 @@ export class AgentSession {
3148
3553
  }
3149
3554
  catch {
3150
3555
  // Aborted during sleep - emit end event so UI can clean up
3151
- const attempt = this._retryAttempt;
3152
- this._retryAttempt = 0;
3153
- this._emit({
3154
- type: "auto_retry_end",
3155
- success: false,
3156
- attempt,
3157
- finalError: "Retry cancelled",
3158
- });
3556
+ this._finishCancelledRetry();
3159
3557
  return false;
3160
3558
  }
3161
3559
  finally {
@@ -3239,10 +3637,8 @@ export class AgentSession {
3239
3637
  this._pendingBashMessages.push(bashMessage);
3240
3638
  }
3241
3639
  else {
3242
- // Add to agent state immediately
3243
- this.agent.state.messages.push(bashMessage);
3244
- // Save to session
3245
3640
  this.sessionManager.appendMessage(bashMessage);
3641
+ this._refreshFinalizedContext();
3246
3642
  }
3247
3643
  }
3248
3644
  /**
@@ -3269,12 +3665,10 @@ export class AgentSession {
3269
3665
  if (this._pendingBashMessages.length === 0)
3270
3666
  return;
3271
3667
  for (const bashMessage of this._pendingBashMessages) {
3272
- // Add to agent state
3273
- this.agent.state.messages.push(bashMessage);
3274
- // Save to session
3275
3668
  this.sessionManager.appendMessage(bashMessage);
3276
3669
  }
3277
3670
  this._pendingBashMessages = [];
3671
+ this._refreshFinalizedContext();
3278
3672
  }
3279
3673
  // =========================================================================
3280
3674
  // Session Management
@@ -3306,6 +3700,9 @@ export class AgentSession {
3306
3700
  if (this.isStreaming) {
3307
3701
  throw new Error("Wait for the current response to finish before navigating the session tree.");
3308
3702
  }
3703
+ if (this.isCompacting) {
3704
+ throw new Error("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
3705
+ }
3309
3706
  const oldLeafId = this.sessionManager.getLeafId();
3310
3707
  // No-op if already at target
3311
3708
  if (targetId === oldLeafId) {
@@ -3452,9 +3849,9 @@ export class AgentSession {
3452
3849
  if (label && !summaryText) {
3453
3850
  this.sessionManager.appendLabelChange(targetId, label);
3454
3851
  }
3455
- // Update agent state
3456
- const sessionContext = this.sessionManager.buildSessionContext();
3457
- this.agent.state.messages = sessionContext.messages;
3852
+ // Update finalized context from the canonical session projection.
3853
+ this._refreshFinalizedContext();
3854
+ this._restoreToolsFromTranscript();
3458
3855
  // Emit session_tree event
3459
3856
  await this._extensionRunner.emit({
3460
3857
  type: "session_tree",
@@ -3468,6 +3865,7 @@ export class AgentSession {
3468
3865
  }
3469
3866
  finally {
3470
3867
  this._branchSummaryAbortController = undefined;
3868
+ this._resolveIdleWaitIfIdle();
3471
3869
  }
3472
3870
  }
3473
3871
  /**
@@ -3566,7 +3964,10 @@ export class AgentSession {
3566
3964
  let toolCalls = 0;
3567
3965
  const usageTotals = createUsageTotals();
3568
3966
  for (const entry of this.sessionManager.getEntries()) {
3569
- if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
3967
+ if (entry.type === "usage") {
3968
+ addUsageToTotals(usageTotals, entry.usage);
3969
+ }
3970
+ else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
3570
3971
  addUsageToTotals(usageTotals, entry.usage);
3571
3972
  }
3572
3973
  if (entry.type !== "message")
@@ -3620,30 +4021,24 @@ export class AgentSession {
3620
4021
  // After compaction, the last assistant usage reflects pre-compaction context size.
3621
4022
  // We can only trust usage from an assistant that responded after the latest compaction.
3622
4023
  // If no such assistant exists, context token count is unknown until the next LLM response.
3623
- const branchEntries = this.sessionManager.getBranch();
3624
- const latestCompaction = getLatestCompactionEntry(branchEntries);
4024
+ const projection = this.sessionManager.buildSessionProjection();
4025
+ const branch = this.sessionManager.getBranch();
4026
+ const latestCompaction = getLatestCompactionEntry(branch);
3625
4027
  if (latestCompaction) {
3626
- // Check if there's a valid assistant usage after the compaction boundary
3627
- const compactionIndex = branchEntries.lastIndexOf(latestCompaction);
3628
- let hasPostCompactionUsage = false;
3629
- for (let i = branchEntries.length - 1; i > compactionIndex; i--) {
3630
- const entry = branchEntries[i];
3631
- if (entry.type === "message" && entry.message.role === "assistant") {
3632
- const assistant = entry.message;
3633
- if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") {
3634
- const contextTokens = calculateContextTokens(assistant.usage);
3635
- if (contextTokens > 0) {
3636
- hasPostCompactionUsage = true;
3637
- break;
3638
- }
3639
- }
3640
- }
3641
- }
3642
- if (!hasPostCompactionUsage) {
4028
+ const projectedAssistants = new Set(projection.entries.flatMap((entry) => entry.messages.some((message) => message.role === "assistant" &&
4029
+ message.stopReason !== "aborted" &&
4030
+ message.stopReason !== "error" &&
4031
+ calculateContextTokens(message.usage) > 0)
4032
+ ? [entry.sourceEntry.id]
4033
+ : []));
4034
+ const compactionIndex = branch.findIndex((entry) => entry.id === latestCompaction.id);
4035
+ const hasPostCompactionUsage = branch
4036
+ .slice(compactionIndex + 1)
4037
+ .some((entry) => projectedAssistants.has(entry.id));
4038
+ if (!hasPostCompactionUsage)
3643
4039
  return { tokens: null, contextWindow, percent: null };
3644
- }
3645
4040
  }
3646
- const estimate = estimateContextTokens(this.messages);
4041
+ const estimate = estimateProjectedContextTokens(projection, branch);
3647
4042
  const percent = (estimate.tokens / contextWindow) * 100;
3648
4043
  return {
3649
4044
  tokens: estimate.tokens,
@@ -3680,6 +4075,30 @@ export class AgentSession {
3680
4075
  exportToJsonl(outputPath) {
3681
4076
  return exportSessionToJsonl(this.sessionManager, outputPath);
3682
4077
  }
4078
+ /**
4079
+ * Ask the current model to describe what went wrong in this session for a bug report.
4080
+ * Used when the user declines to share the transcript itself.
4081
+ */
4082
+ async summarizeForBugReport(options) {
4083
+ const model = this.model;
4084
+ if (!model) {
4085
+ throw new Error("No model selected");
4086
+ }
4087
+ const { model: requestModel, apiKey, headers, env } = await this._getSummarizationRequestAuth(model);
4088
+ return generateBugReportSummary({
4089
+ messages: this.messages,
4090
+ hint: options.hint,
4091
+ model: requestModel,
4092
+ apiKey,
4093
+ headers,
4094
+ env,
4095
+ signal: options.signal,
4096
+ thinkingLevel: this.thinkingLevel,
4097
+ streamFn: this.agent.streamFunction,
4098
+ retry: this.settingsManager.getRetrySettings(),
4099
+ sessionId: this.sessionId,
4100
+ });
4101
+ }
3683
4102
  // =========================================================================
3684
4103
  // Utilities
3685
4104
  // =========================================================================