@opendash-ai/cli 0.1.0-beta.1

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 (1212) hide show
  1. package/Dockerfile +18 -0
  2. package/README.md +15 -0
  3. package/analysis.py +171 -0
  4. package/bin/opendash +180 -0
  5. package/bunfig.toml +7 -0
  6. package/drizzle.config.ts +10 -0
  7. package/git +0 -0
  8. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  9. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  10. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  11. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  12. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  13. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  14. package/migration/20260225215848_workspace/migration.sql +7 -0
  15. package/migration/20260225215848_workspace/snapshot.json +959 -0
  16. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  17. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  18. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  19. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  20. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  21. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  22. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  23. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  24. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  25. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  26. package/migration/20260323234822_events/migration.sql +13 -0
  27. package/migration/20260323234822_events/snapshot.json +1271 -0
  28. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  29. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  30. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  31. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  32. package/migration/20260422160000_context_inheritance/migration.sql +3 -0
  33. package/migration/20260422170000_task_registry/migration.sql +18 -0
  34. package/migration/20260423145421_remove_session_entry/migration.sql +4 -0
  35. package/migration/20260515000000_actor_rename/migration.sql +7 -0
  36. package/migration/20260515010000_memory_fts/migration.sql +33 -0
  37. package/migration/20260515020000_user_task/migration.sql +29 -0
  38. package/migration/20260519000000_last_checkpoint_message_id/migration.sql +1 -0
  39. package/migration/20260521000000_message_agent_id/migration.sql +2 -0
  40. package/migration/20260521000100_actor_registry_v6/migration.sql +25 -0
  41. package/migration/20260521010000_memory_fts_v6/migration.sql +33 -0
  42. package/migration/20260521020000_memory_fts_triggers/migration.sql +17 -0
  43. package/migration/20260526000000_agent_id_main/migration.sql +14 -0
  44. package/migration/20260527000000_actor_lifecycle/migration.sql +8 -0
  45. package/migration/20260527000100_inbox/migration.sql +12 -0
  46. package/migration/20260529000000_task_todo_redesign/migration.sql +16 -0
  47. package/migration/20260603000000_task_in_progress_owner/migration.sql +1 -0
  48. package/migration/20260603000000_workflow_run/migration.sql +17 -0
  49. package/migration/20260604000000_workflow_script_sha/migration.sql +1 -0
  50. package/migration/20260608000000_claude_import/migration.sql +7 -0
  51. package/migration/20260608010000_claude_import_message_ids/migration.sql +1 -0
  52. package/migration/20260609000000_history_fts/migration.sql +29 -0
  53. package/migration/20260609230000_workflow_agent_timeout/migration.sql +1 -0
  54. package/migration/20260612000000_external_import/migration.sql +16 -0
  55. package/package.json +218 -0
  56. package/parsers-config.ts +290 -0
  57. package/script/build.ts +337 -0
  58. package/script/build_noinstall.ts +337 -0
  59. package/script/check-migrations.ts +16 -0
  60. package/script/dev.ts +38 -0
  61. package/script/fds-upload.ts +170 -0
  62. package/script/fix-node-pty.ts +28 -0
  63. package/script/generate.ts +23 -0
  64. package/script/postinstall.mjs +102 -0
  65. package/script/publish.ts +75 -0
  66. package/script/run-workspace-server +106 -0
  67. package/script/schema.ts +63 -0
  68. package/script/time.ts +6 -0
  69. package/script/trace-imports.ts +153 -0
  70. package/script/upgrade-opentui.ts +64 -0
  71. package/src/account/account.sql.ts +39 -0
  72. package/src/account/account.ts +456 -0
  73. package/src/account/repo.ts +166 -0
  74. package/src/account/schema.ts +99 -0
  75. package/src/account/url.ts +8 -0
  76. package/src/acp/README.md +174 -0
  77. package/src/acp/agent.ts +1787 -0
  78. package/src/acp/session.ts +116 -0
  79. package/src/acp/types.ts +24 -0
  80. package/src/actor/actor.sql.ts +38 -0
  81. package/src/actor/events.ts +67 -0
  82. package/src/actor/index.ts +2 -0
  83. package/src/actor/registry.ts +412 -0
  84. package/src/actor/return-header.ts +24 -0
  85. package/src/actor/schema.ts +47 -0
  86. package/src/actor/spawn-ref.ts +16 -0
  87. package/src/actor/spawn.ts +756 -0
  88. package/src/actor/turn.ts +49 -0
  89. package/src/actor/waiter.ts +166 -0
  90. package/src/agent/agent.ts +574 -0
  91. package/src/agent/config.ts +5 -0
  92. package/src/agent/generate.txt +75 -0
  93. package/src/agent/prompt/checkpoint-writer.txt +167 -0
  94. package/src/agent/prompt/compaction.txt +9 -0
  95. package/src/agent/prompt/distill.txt +199 -0
  96. package/src/agent/prompt/dream.txt +155 -0
  97. package/src/agent/prompt/explore.txt +18 -0
  98. package/src/agent/prompt/summary.txt +11 -0
  99. package/src/agent/prompt/title.txt +44 -0
  100. package/src/audio.d.ts +9 -0
  101. package/src/auth/index.ts +97 -0
  102. package/src/bus/bus-event.ts +33 -0
  103. package/src/bus/global.ts +12 -0
  104. package/src/bus/index.ts +193 -0
  105. package/src/cli/bootstrap.ts +33 -0
  106. package/src/cli/cmd/account.ts +258 -0
  107. package/src/cli/cmd/acp.ts +70 -0
  108. package/src/cli/cmd/agent.ts +248 -0
  109. package/src/cli/cmd/cmd.ts +7 -0
  110. package/src/cli/cmd/db.ts +120 -0
  111. package/src/cli/cmd/debug/agent.ts +192 -0
  112. package/src/cli/cmd/debug/config.ts +17 -0
  113. package/src/cli/cmd/debug/file.ts +100 -0
  114. package/src/cli/cmd/debug/index.ts +48 -0
  115. package/src/cli/cmd/debug/lsp.ts +61 -0
  116. package/src/cli/cmd/debug/ripgrep.ts +105 -0
  117. package/src/cli/cmd/debug/scrap.ts +16 -0
  118. package/src/cli/cmd/debug/skill.ts +23 -0
  119. package/src/cli/cmd/debug/snapshot.ts +53 -0
  120. package/src/cli/cmd/export.ts +306 -0
  121. package/src/cli/cmd/generate.ts +50 -0
  122. package/src/cli/cmd/github.ts +1647 -0
  123. package/src/cli/cmd/import.ts +208 -0
  124. package/src/cli/cmd/mcp.ts +812 -0
  125. package/src/cli/cmd/models.ts +88 -0
  126. package/src/cli/cmd/plug.ts +233 -0
  127. package/src/cli/cmd/pr.ts +138 -0
  128. package/src/cli/cmd/providers.ts +713 -0
  129. package/src/cli/cmd/run-completion.ts +77 -0
  130. package/src/cli/cmd/run.ts +694 -0
  131. package/src/cli/cmd/serve.ts +30 -0
  132. package/src/cli/cmd/session.ts +181 -0
  133. package/src/cli/cmd/stats.ts +413 -0
  134. package/src/cli/cmd/tui/app.tsx +1140 -0
  135. package/src/cli/cmd/tui/asset/TEN_VAD_LICENSE +12 -0
  136. package/src/cli/cmd/tui/asset/charge.wav +0 -0
  137. package/src/cli/cmd/tui/asset/pulse-a.wav +0 -0
  138. package/src/cli/cmd/tui/asset/pulse-b.wav +0 -0
  139. package/src/cli/cmd/tui/asset/pulse-c.wav +0 -0
  140. package/src/cli/cmd/tui/asset/ten_vad.wasm +0 -0
  141. package/src/cli/cmd/tui/asset/ten_vad_loader.js +30 -0
  142. package/src/cli/cmd/tui/attach.ts +84 -0
  143. package/src/cli/cmd/tui/component/background-image.tsx +150 -0
  144. package/src/cli/cmd/tui/component/bg-pulse.tsx +130 -0
  145. package/src/cli/cmd/tui/component/border.tsx +21 -0
  146. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  147. package/src/cli/cmd/tui/component/dialog-agreement.tsx +111 -0
  148. package/src/cli/cmd/tui/component/dialog-command.tsx +219 -0
  149. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  150. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +157 -0
  151. package/src/cli/cmd/tui/component/dialog-image-list.tsx +111 -0
  152. package/src/cli/cmd/tui/component/dialog-logo-design.tsx +37 -0
  153. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  154. package/src/cli/cmd/tui/component/dialog-mimo-login.tsx +165 -0
  155. package/src/cli/cmd/tui/component/dialog-model.tsx +335 -0
  156. package/src/cli/cmd/tui/component/dialog-provider.tsx +449 -0
  157. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +101 -0
  158. package/src/cli/cmd/tui/component/dialog-session-list.tsx +269 -0
  159. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  160. package/src/cli/cmd/tui/component/dialog-skill.tsx +42 -0
  161. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  162. package/src/cli/cmd/tui/component/dialog-status.tsx +170 -0
  163. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  164. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  165. package/src/cli/cmd/tui/component/dialog-token-plan.tsx +77 -0
  166. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  167. package/src/cli/cmd/tui/component/dialog-workflows.tsx +51 -0
  168. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +289 -0
  169. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +81 -0
  170. package/src/cli/cmd/tui/component/dialog-worktree.tsx +90 -0
  171. package/src/cli/cmd/tui/component/error-component.tsx +92 -0
  172. package/src/cli/cmd/tui/component/logo.tsx +961 -0
  173. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  174. package/src/cli/cmd/tui/component/prompt/autocomplete-detect.ts +34 -0
  175. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +668 -0
  176. package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
  177. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  178. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  179. package/src/cli/cmd/tui/component/prompt/index.tsx +1917 -0
  180. package/src/cli/cmd/tui/component/prompt/offset.ts +45 -0
  181. package/src/cli/cmd/tui/component/prompt/part.ts +36 -0
  182. package/src/cli/cmd/tui/component/prompt/shimmer.ts +62 -0
  183. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  184. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  185. package/src/cli/cmd/tui/component/starry-background.tsx +305 -0
  186. package/src/cli/cmd/tui/component/startup-loading.tsx +67 -0
  187. package/src/cli/cmd/tui/component/task-item.tsx +63 -0
  188. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  189. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  190. package/src/cli/cmd/tui/component/workflow-tree.tsx +177 -0
  191. package/src/cli/cmd/tui/config/cwd.ts +5 -0
  192. package/src/cli/cmd/tui/config/tui-migrate.ts +151 -0
  193. package/src/cli/cmd/tui/config/tui-schema.ts +38 -0
  194. package/src/cli/cmd/tui/config/tui.ts +219 -0
  195. package/src/cli/cmd/tui/context/args.tsx +16 -0
  196. package/src/cli/cmd/tui/context/directory.ts +15 -0
  197. package/src/cli/cmd/tui/context/event.ts +45 -0
  198. package/src/cli/cmd/tui/context/exit.tsx +65 -0
  199. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  200. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  201. package/src/cli/cmd/tui/context/kv.tsx +86 -0
  202. package/src/cli/cmd/tui/context/language.tsx +91 -0
  203. package/src/cli/cmd/tui/context/local.tsx +469 -0
  204. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  205. package/src/cli/cmd/tui/context/project.tsx +109 -0
  206. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  207. package/src/cli/cmd/tui/context/route.tsx +68 -0
  208. package/src/cli/cmd/tui/context/sdk.tsx +150 -0
  209. package/src/cli/cmd/tui/context/sync.tsx +884 -0
  210. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  211. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  212. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  213. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
  214. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
  215. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  216. package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
  217. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  218. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  219. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  220. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  221. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  222. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  223. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  224. package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
  225. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  226. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  227. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  228. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  229. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  230. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  231. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  232. package/src/cli/cmd/tui/context/theme/opendash.json +243 -0
  233. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  234. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  235. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  236. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  237. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  238. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  239. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  240. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  241. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  242. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  243. package/src/cli/cmd/tui/context/theme.tsx +1299 -0
  244. package/src/cli/cmd/tui/context/thinking.ts +48 -0
  245. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  246. package/src/cli/cmd/tui/event.ts +56 -0
  247. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
  248. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +194 -0
  249. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +54 -0
  250. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +114 -0
  251. package/src/cli/cmd/tui/feature-plugins/sidebar/cwd.tsx +45 -0
  252. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  253. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  254. package/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx +84 -0
  255. package/src/cli/cmd/tui/feature-plugins/sidebar/instructions.tsx +54 -0
  256. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  257. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +98 -0
  258. package/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx +95 -0
  259. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +51 -0
  260. package/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts +31 -0
  261. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +274 -0
  262. package/src/cli/cmd/tui/i18n/en.ts +470 -0
  263. package/src/cli/cmd/tui/i18n/es.ts +508 -0
  264. package/src/cli/cmd/tui/i18n/fr.ts +515 -0
  265. package/src/cli/cmd/tui/i18n/ja.ts +463 -0
  266. package/src/cli/cmd/tui/i18n/locales.ts +82 -0
  267. package/src/cli/cmd/tui/i18n/ru.ts +527 -0
  268. package/src/cli/cmd/tui/i18n/slash-command.ts +11 -0
  269. package/src/cli/cmd/tui/i18n/zh.ts +459 -0
  270. package/src/cli/cmd/tui/i18n/zht.ts +430 -0
  271. package/src/cli/cmd/tui/layer.ts +6 -0
  272. package/src/cli/cmd/tui/plugin/api.tsx +402 -0
  273. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  274. package/src/cli/cmd/tui/plugin/internal.ts +35 -0
  275. package/src/cli/cmd/tui/plugin/runtime.ts +1057 -0
  276. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  277. package/src/cli/cmd/tui/routes/home.tsx +172 -0
  278. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  279. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +116 -0
  280. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +47 -0
  281. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  282. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  283. package/src/cli/cmd/tui/routes/session/index.tsx +3087 -0
  284. package/src/cli/cmd/tui/routes/session/permission.tsx +697 -0
  285. package/src/cli/cmd/tui/routes/session/question.tsx +488 -0
  286. package/src/cli/cmd/tui/routes/session/sidebar.tsx +109 -0
  287. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +143 -0
  288. package/src/cli/cmd/tui/thread.ts +323 -0
  289. package/src/cli/cmd/tui/ui/dialog-alert.tsx +61 -0
  290. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +95 -0
  291. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +223 -0
  292. package/src/cli/cmd/tui/ui/dialog-help.tsx +42 -0
  293. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +123 -0
  294. package/src/cli/cmd/tui/ui/dialog-select.tsx +467 -0
  295. package/src/cli/cmd/tui/ui/dialog.tsx +207 -0
  296. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  297. package/src/cli/cmd/tui/ui/spinner.ts +386 -0
  298. package/src/cli/cmd/tui/ui/toast.tsx +102 -0
  299. package/src/cli/cmd/tui/util/clipboard.ts +203 -0
  300. package/src/cli/cmd/tui/util/editor.ts +36 -0
  301. package/src/cli/cmd/tui/util/image-protocol.ts +35 -0
  302. package/src/cli/cmd/tui/util/index.ts +6 -0
  303. package/src/cli/cmd/tui/util/model.ts +23 -0
  304. package/src/cli/cmd/tui/util/pinyin.ts +20 -0
  305. package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
  306. package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
  307. package/src/cli/cmd/tui/util/scroll.ts +23 -0
  308. package/src/cli/cmd/tui/util/selection.ts +23 -0
  309. package/src/cli/cmd/tui/util/signal.ts +71 -0
  310. package/src/cli/cmd/tui/util/sound.ts +154 -0
  311. package/src/cli/cmd/tui/util/system-locale.ts +209 -0
  312. package/src/cli/cmd/tui/util/terminal.ts +110 -0
  313. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  314. package/src/cli/cmd/tui/util/vad.ts +229 -0
  315. package/src/cli/cmd/tui/util/voice.ts +595 -0
  316. package/src/cli/cmd/tui/win32.ts +130 -0
  317. package/src/cli/cmd/tui/worker.ts +104 -0
  318. package/src/cli/cmd/uninstall.ts +351 -0
  319. package/src/cli/cmd/upgrade.ts +79 -0
  320. package/src/cli/cmd/web.ts +96 -0
  321. package/src/cli/effect/prompt.ts +25 -0
  322. package/src/cli/error.ts +82 -0
  323. package/src/cli/heap.ts +59 -0
  324. package/src/cli/i18n.ts +15 -0
  325. package/src/cli/logo.ts +52 -0
  326. package/src/cli/network.ts +68 -0
  327. package/src/cli/ui.ts +133 -0
  328. package/src/cli/upgrade.ts +41 -0
  329. package/src/command/index.ts +276 -0
  330. package/src/command/template/initialize.txt +66 -0
  331. package/src/command/template/review.txt +101 -0
  332. package/src/config/agent.ts +197 -0
  333. package/src/config/command.ts +69 -0
  334. package/src/config/compose.ts +26 -0
  335. package/src/config/config.ts +1052 -0
  336. package/src/config/console-state.ts +16 -0
  337. package/src/config/entry-name.ts +16 -0
  338. package/src/config/error.ts +21 -0
  339. package/src/config/formatter.ts +17 -0
  340. package/src/config/history.ts +21 -0
  341. package/src/config/index.ts +17 -0
  342. package/src/config/keybinds.ts +127 -0
  343. package/src/config/layout.ts +10 -0
  344. package/src/config/lsp.ts +45 -0
  345. package/src/config/managed.ts +70 -0
  346. package/src/config/markdown.ts +97 -0
  347. package/src/config/mcp.ts +172 -0
  348. package/src/config/model-id.ts +14 -0
  349. package/src/config/parse.ts +44 -0
  350. package/src/config/paths.ts +73 -0
  351. package/src/config/permission.ts +76 -0
  352. package/src/config/plugin.ts +88 -0
  353. package/src/config/provider.ts +118 -0
  354. package/src/config/server.ts +20 -0
  355. package/src/config/skills.ts +16 -0
  356. package/src/config/variable.ts +90 -0
  357. package/src/control-plane/adaptors/index.ts +52 -0
  358. package/src/control-plane/adaptors/worktree.ts +47 -0
  359. package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
  360. package/src/control-plane/schema.ts +19 -0
  361. package/src/control-plane/sse.ts +66 -0
  362. package/src/control-plane/types.ts +34 -0
  363. package/src/control-plane/util.ts +37 -0
  364. package/src/control-plane/workspace-context.ts +26 -0
  365. package/src/control-plane/workspace.sql.ts +17 -0
  366. package/src/control-plane/workspace.ts +615 -0
  367. package/src/effect/app-runtime.ts +146 -0
  368. package/src/effect/bootstrap-runtime.ts +33 -0
  369. package/src/effect/bridge.ts +48 -0
  370. package/src/effect/cross-spawn-spawner.ts +514 -0
  371. package/src/effect/index.ts +5 -0
  372. package/src/effect/instance-ref.ts +11 -0
  373. package/src/effect/instance-registry.ts +12 -0
  374. package/src/effect/instance-state.ts +81 -0
  375. package/src/effect/logger.ts +73 -0
  376. package/src/effect/memo-map.ts +3 -0
  377. package/src/effect/observability.ts +107 -0
  378. package/src/effect/run-service.ts +52 -0
  379. package/src/effect/runner.ts +210 -0
  380. package/src/effect/runtime.ts +19 -0
  381. package/src/env/index.ts +37 -0
  382. package/src/file/ignore.ts +81 -0
  383. package/src/file/index.ts +664 -0
  384. package/src/file/protected.ts +59 -0
  385. package/src/file/ripgrep.ts +485 -0
  386. package/src/file/watcher.ts +163 -0
  387. package/src/flag/flag.ts +212 -0
  388. package/src/format/formatter.ts +403 -0
  389. package/src/format/index.ts +203 -0
  390. package/src/git/index.ts +260 -0
  391. package/src/global/index.ts +54 -0
  392. package/src/history/backfill.ts +162 -0
  393. package/src/history/extract.ts +67 -0
  394. package/src/history/fts-query.ts +15 -0
  395. package/src/history/fts.sql.ts +20 -0
  396. package/src/history/index.ts +10 -0
  397. package/src/history/resolve.ts +65 -0
  398. package/src/history/service.ts +258 -0
  399. package/src/history/writer.ts +112 -0
  400. package/src/id/id.ts +87 -0
  401. package/src/ide/index.ts +73 -0
  402. package/src/inbox/inbox-ref.ts +38 -0
  403. package/src/inbox/inbox.sql.ts +26 -0
  404. package/src/inbox/inbox.ts +223 -0
  405. package/src/inbox/index.ts +3 -0
  406. package/src/inbox/render.ts +40 -0
  407. package/src/index.ts +261 -0
  408. package/src/installation/index.ts +362 -0
  409. package/src/installation/version.ts +32 -0
  410. package/src/intelligence/AUDIT.md +292 -0
  411. package/src/intelligence/ENTERPRISE-CAPABILITIES.md +317 -0
  412. package/src/intelligence/ENTERPRISE-REVIEW.md +284 -0
  413. package/src/intelligence/IMPLEMENTATION.md +214 -0
  414. package/src/intelligence/PHASE1.md +178 -0
  415. package/src/intelligence/PHASE2.5.md +162 -0
  416. package/src/intelligence/PHASE2.md +150 -0
  417. package/src/intelligence/PHASE3-REFINED.md +257 -0
  418. package/src/intelligence/PHASE3.md +188 -0
  419. package/src/intelligence/README.md +260 -0
  420. package/src/intelligence/analytics/SKILL.md +369 -0
  421. package/src/intelligence/analytics/adapters/index.ts +77 -0
  422. package/src/intelligence/analytics/benchmarks/observable-runner.ts +575 -0
  423. package/src/intelligence/analytics/benchmarks/runner.ts +758 -0
  424. package/src/intelligence/analytics/engine/index.ts +152 -0
  425. package/src/intelligence/analytics/hook.ts +154 -0
  426. package/src/intelligence/analytics/index.ts +194 -0
  427. package/src/intelligence/analytics/loader/index.ts +502 -0
  428. package/src/intelligence/analytics/memory/index.ts +455 -0
  429. package/src/intelligence/analytics/orchestrator/index.ts +329 -0
  430. package/src/intelligence/analytics/planner/index.ts +188 -0
  431. package/src/intelligence/analytics/prompt/index.ts +449 -0
  432. package/src/intelligence/analytics/reasoning/enterprise-capabilities.ts +534 -0
  433. package/src/intelligence/analytics/reasoning/enterprise-prompt.ts +321 -0
  434. package/src/intelligence/analytics/reasoning/index.ts +267 -0
  435. package/src/intelligence/analytics/reasoning/test.ts +166 -0
  436. package/src/intelligence/analytics/reasoning/visible-reasoning.ts +609 -0
  437. package/src/intelligence/analytics/registry/index.ts +176 -0
  438. package/src/intelligence/analytics/router/index.ts +352 -0
  439. package/src/intelligence/analytics/test.ts +127 -0
  440. package/src/intelligence/analytics/workflow/index.ts +375 -0
  441. package/src/intelligence/index.ts +50 -0
  442. package/src/lsp/client.ts +249 -0
  443. package/src/lsp/diagnostic.ts +29 -0
  444. package/src/lsp/index.ts +3 -0
  445. package/src/lsp/language.ts +120 -0
  446. package/src/lsp/launch.ts +21 -0
  447. package/src/lsp/lsp.ts +519 -0
  448. package/src/lsp/server.ts +1956 -0
  449. package/src/mcp/auth.ts +144 -0
  450. package/src/mcp/index.ts +939 -0
  451. package/src/mcp/oauth-callback.ts +236 -0
  452. package/src/mcp/oauth-provider.ts +214 -0
  453. package/src/memory/fts-query.ts +37 -0
  454. package/src/memory/fts.sql.ts +19 -0
  455. package/src/memory/index.ts +1 -0
  456. package/src/memory/paths.ts +116 -0
  457. package/src/memory/reconcile.ts +144 -0
  458. package/src/memory/service.ts +144 -0
  459. package/src/metrics/client.ts +40 -0
  460. package/src/metrics/event.ts +43 -0
  461. package/src/metrics/index.ts +5 -0
  462. package/src/metrics/installation.ts +18 -0
  463. package/src/metrics/subscriber.ts +58 -0
  464. package/src/metrics/util.ts +9 -0
  465. package/src/node.ts +6 -0
  466. package/src/npm/config.ts +0 -0
  467. package/src/npm/index.ts +293 -0
  468. package/src/npmcli-config.d.ts +43 -0
  469. package/src/patch/index.ts +680 -0
  470. package/src/permission/arity.ts +163 -0
  471. package/src/permission/evaluate.ts +15 -0
  472. package/src/permission/index.ts +382 -0
  473. package/src/permission/schema.ts +17 -0
  474. package/src/plugin/checkpoint-splitover.ts +60 -0
  475. package/src/plugin/cloudflare.ts +76 -0
  476. package/src/plugin/codex.ts +611 -0
  477. package/src/plugin/github-copilot/copilot.ts +368 -0
  478. package/src/plugin/github-copilot/models.ts +153 -0
  479. package/src/plugin/index.ts +637 -0
  480. package/src/plugin/install.ts +439 -0
  481. package/src/plugin/loader.ts +216 -0
  482. package/src/plugin/matcher.ts +33 -0
  483. package/src/plugin/meta.ts +188 -0
  484. package/src/plugin/mimo.ts +296 -0
  485. package/src/plugin/shared.ts +323 -0
  486. package/src/plugin/subagent-progress-checker.ts +147 -0
  487. package/src/project/bootstrap.ts +59 -0
  488. package/src/project/index.ts +2 -0
  489. package/src/project/instance.ts +215 -0
  490. package/src/project/project-id.ts +48 -0
  491. package/src/project/project.sql.ts +16 -0
  492. package/src/project/project.ts +522 -0
  493. package/src/project/schema.ts +15 -0
  494. package/src/project/vcs.ts +227 -0
  495. package/src/project/workspace-trust.ts +67 -0
  496. package/src/provider/auth.ts +234 -0
  497. package/src/provider/error.ts +216 -0
  498. package/src/provider/index.ts +5 -0
  499. package/src/provider/models.ts +180 -0
  500. package/src/provider/provider.ts +1788 -0
  501. package/src/provider/schema.ts +36 -0
  502. package/src/provider/sdk/copilot/README.md +5 -0
  503. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  504. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  505. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  506. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  507. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
  508. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  509. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  510. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  511. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  512. package/src/provider/sdk/copilot/index.ts +2 -0
  513. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  514. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  515. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  516. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  517. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  518. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  519. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
  520. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  521. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  522. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  523. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  524. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  525. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  526. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  527. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  528. package/src/provider/transform.ts +1376 -0
  529. package/src/pty/index.ts +364 -0
  530. package/src/pty/pty.bun.ts +26 -0
  531. package/src/pty/pty.node.ts +27 -0
  532. package/src/pty/pty.ts +25 -0
  533. package/src/pty/schema.ts +17 -0
  534. package/src/question/index.ts +252 -0
  535. package/src/question/schema.ts +17 -0
  536. package/src/server/adapter.bun.ts +40 -0
  537. package/src/server/adapter.node.ts +66 -0
  538. package/src/server/adapter.ts +21 -0
  539. package/src/server/auth.ts +16 -0
  540. package/src/server/error.ts +53 -0
  541. package/src/server/event.ts +7 -0
  542. package/src/server/fence.ts +81 -0
  543. package/src/server/mdns.ts +60 -0
  544. package/src/server/middleware.ts +94 -0
  545. package/src/server/projectors.ts +28 -0
  546. package/src/server/proxy.ts +171 -0
  547. package/src/server/pty-ticket.ts +42 -0
  548. package/src/server/rate-limit.ts +38 -0
  549. package/src/server/routes/control/index.ts +160 -0
  550. package/src/server/routes/control/workspace.ts +203 -0
  551. package/src/server/routes/global.ts +367 -0
  552. package/src/server/routes/instance/bash-interactive.ts +82 -0
  553. package/src/server/routes/instance/config.ts +89 -0
  554. package/src/server/routes/instance/event.ts +108 -0
  555. package/src/server/routes/instance/experimental.ts +408 -0
  556. package/src/server/routes/instance/file.ts +190 -0
  557. package/src/server/routes/instance/httpapi/config.ts +51 -0
  558. package/src/server/routes/instance/httpapi/permission.ts +72 -0
  559. package/src/server/routes/instance/httpapi/project.ts +62 -0
  560. package/src/server/routes/instance/httpapi/provider.ts +142 -0
  561. package/src/server/routes/instance/httpapi/question.ts +121 -0
  562. package/src/server/routes/instance/httpapi/server.ts +153 -0
  563. package/src/server/routes/instance/index.ts +301 -0
  564. package/src/server/routes/instance/mcp.ts +260 -0
  565. package/src/server/routes/instance/middleware.ts +44 -0
  566. package/src/server/routes/instance/permission.ts +73 -0
  567. package/src/server/routes/instance/project.ts +122 -0
  568. package/src/server/routes/instance/provider.ts +158 -0
  569. package/src/server/routes/instance/pty.ts +302 -0
  570. package/src/server/routes/instance/question.ts +162 -0
  571. package/src/server/routes/instance/session.ts +1328 -0
  572. package/src/server/routes/instance/sync.ts +143 -0
  573. package/src/server/routes/instance/trace.ts +59 -0
  574. package/src/server/routes/instance/tui.ts +384 -0
  575. package/src/server/routes/instance/workflows.ts +142 -0
  576. package/src/server/routes/ui.ts +37 -0
  577. package/src/server/server.ts +146 -0
  578. package/src/server/workspace.ts +122 -0
  579. package/src/session/auto-dream.ts +123 -0
  580. package/src/session/boundary.ts +77 -0
  581. package/src/session/budgeted-read.ts +118 -0
  582. package/src/session/checkpoint-align.ts +29 -0
  583. package/src/session/checkpoint-context.ts +36 -0
  584. package/src/session/checkpoint-paths.ts +86 -0
  585. package/src/session/checkpoint-progress-reconcile.ts +111 -0
  586. package/src/session/checkpoint-retry.ts +192 -0
  587. package/src/session/checkpoint-templates.ts +114 -0
  588. package/src/session/checkpoint-validator.ts +259 -0
  589. package/src/session/checkpoint.ts +1560 -0
  590. package/src/session/classify.ts +117 -0
  591. package/src/session/claude-import.ts +381 -0
  592. package/src/session/codex-import.ts +416 -0
  593. package/src/session/compaction.ts +545 -0
  594. package/src/session/external-import.sql.ts +18 -0
  595. package/src/session/external-import.ts +136 -0
  596. package/src/session/goal.ts +232 -0
  597. package/src/session/index.ts +1 -0
  598. package/src/session/instruction.ts +276 -0
  599. package/src/session/last-message-info.ts +32 -0
  600. package/src/session/llm-request-prefix.ts +82 -0
  601. package/src/session/llm.ts +734 -0
  602. package/src/session/max-mode.ts +410 -0
  603. package/src/session/message-v2.ts +1138 -0
  604. package/src/session/message.ts +191 -0
  605. package/src/session/opendash-import.ts +281 -0
  606. package/src/session/overflow.ts +53 -0
  607. package/src/session/prefix-capture-ref.ts +48 -0
  608. package/src/session/processor.ts +983 -0
  609. package/src/session/projectors.ts +137 -0
  610. package/src/session/prompt/anthropic.txt +177 -0
  611. package/src/session/prompt/beast.txt +186 -0
  612. package/src/session/prompt/build-switch.txt +5 -0
  613. package/src/session/prompt/codex.txt +108 -0
  614. package/src/session/prompt/compose.txt +119 -0
  615. package/src/session/prompt/copilot-gpt-5.txt +173 -0
  616. package/src/session/prompt/deepseek.txt +156 -0
  617. package/src/session/prompt/default.old.txt +151 -0
  618. package/src/session/prompt/default.txt +209 -0
  619. package/src/session/prompt/gemini.txt +186 -0
  620. package/src/session/prompt/glm.txt +73 -0
  621. package/src/session/prompt/gpt.txt +134 -0
  622. package/src/session/prompt/kimi.txt +117 -0
  623. package/src/session/prompt/max-steps.txt +16 -0
  624. package/src/session/prompt/minimax.txt +165 -0
  625. package/src/session/prompt/text-loop-recovery.ts +40 -0
  626. package/src/session/prompt/text-ngram-detection.ts +116 -0
  627. package/src/session/prompt/trinity.txt +128 -0
  628. package/src/session/prompt.ts +3878 -0
  629. package/src/session/prune.ts +481 -0
  630. package/src/session/retry.ts +178 -0
  631. package/src/session/revert.ts +161 -0
  632. package/src/session/run-state.ts +135 -0
  633. package/src/session/schema.ts +36 -0
  634. package/src/session/session.sql.ts +110 -0
  635. package/src/session/session.ts +908 -0
  636. package/src/session/status.ts +89 -0
  637. package/src/session/summary.ts +163 -0
  638. package/src/session/system.ts +97 -0
  639. package/src/session/todo.ts +77 -0
  640. package/src/session/trajectory.ts +98 -0
  641. package/src/share/index.ts +2 -0
  642. package/src/share/session.ts +57 -0
  643. package/src/share/share-next.ts +381 -0
  644. package/src/share/share.sql.ts +13 -0
  645. package/src/shell/shell.ts +124 -0
  646. package/src/skill/builtin/.bundle/self-extend/SKILL.md +131 -0
  647. package/src/skill/builtin/.bundle/self-extend/reference/hook-api.md +242 -0
  648. package/src/skill/builtin/.bundle/self-extend/reference/skill-api.md +114 -0
  649. package/src/skill/builtin/.bundle/self-extend/reference/tool-api.md +115 -0
  650. package/src/skill/builtin/.bundle/self-extend/reference/tui-api.md +258 -0
  651. package/src/skill/builtin/bundle.macro.ts +30 -0
  652. package/src/skill/builtin/extract.ts +41 -0
  653. package/src/skill/compose/.bundle/ask/SKILL.md +58 -0
  654. package/src/skill/compose/.bundle/brainstorm/SKILL.md +200 -0
  655. package/src/skill/compose/.bundle/brainstorm/scripts/frame-template.html +214 -0
  656. package/src/skill/compose/.bundle/brainstorm/scripts/helper.js +88 -0
  657. package/src/skill/compose/.bundle/brainstorm/scripts/server.cjs +354 -0
  658. package/src/skill/compose/.bundle/brainstorm/scripts/start-server.sh +148 -0
  659. package/src/skill/compose/.bundle/brainstorm/scripts/stop-server.sh +56 -0
  660. package/src/skill/compose/.bundle/brainstorm/spec-document-reviewer-prompt.md +50 -0
  661. package/src/skill/compose/.bundle/brainstorm/visual-companion.md +258 -0
  662. package/src/skill/compose/.bundle/debug/CREATION-LOG.md +119 -0
  663. package/src/skill/compose/.bundle/debug/SKILL.md +297 -0
  664. package/src/skill/compose/.bundle/debug/condition-based-waiting-example.ts +158 -0
  665. package/src/skill/compose/.bundle/debug/condition-based-waiting.md +106 -0
  666. package/src/skill/compose/.bundle/debug/defense-in-depth.md +122 -0
  667. package/src/skill/compose/.bundle/debug/find-polluter.sh +63 -0
  668. package/src/skill/compose/.bundle/debug/root-cause-tracing.md +144 -0
  669. package/src/skill/compose/.bundle/debug/test-academic.md +14 -0
  670. package/src/skill/compose/.bundle/debug/test-pressure-1.md +58 -0
  671. package/src/skill/compose/.bundle/debug/test-pressure-2.md +68 -0
  672. package/src/skill/compose/.bundle/debug/test-pressure-3.md +69 -0
  673. package/src/skill/compose/.bundle/execute/SKILL.md +71 -0
  674. package/src/skill/compose/.bundle/feedback/SKILL.md +214 -0
  675. package/src/skill/compose/.bundle/merge/SKILL.md +252 -0
  676. package/src/skill/compose/.bundle/new-skill/SKILL.md +211 -0
  677. package/src/skill/compose/.bundle/parallel/SKILL.md +168 -0
  678. package/src/skill/compose/.bundle/plan/SKILL.md +183 -0
  679. package/src/skill/compose/.bundle/plan/plan-document-reviewer-prompt.md +50 -0
  680. package/src/skill/compose/.bundle/report/SKILL.md +180 -0
  681. package/src/skill/compose/.bundle/review/SKILL.md +104 -0
  682. package/src/skill/compose/.bundle/review/code-reviewer.md +178 -0
  683. package/src/skill/compose/.bundle/subagent/SKILL.md +325 -0
  684. package/src/skill/compose/.bundle/subagent/code-quality-reviewer-prompt.md +26 -0
  685. package/src/skill/compose/.bundle/subagent/implementer-prompt.md +128 -0
  686. package/src/skill/compose/.bundle/subagent/spec-reviewer-prompt.md +118 -0
  687. package/src/skill/compose/.bundle/tdd/SKILL.md +360 -0
  688. package/src/skill/compose/.bundle/tdd/testing-anti-patterns.md +299 -0
  689. package/src/skill/compose/.bundle/verify/SKILL.md +140 -0
  690. package/src/skill/compose/.bundle/worktree/SKILL.md +234 -0
  691. package/src/skill/compose/LICENSE-karpathy +28 -0
  692. package/src/skill/compose/LICENSE-superpowers +26 -0
  693. package/src/skill/compose/bundle.macro.ts +30 -0
  694. package/src/skill/compose/extract.ts +85 -0
  695. package/src/skill/discovery.ts +116 -0
  696. package/src/skill/index.ts +328 -0
  697. package/src/snapshot/index.ts +777 -0
  698. package/src/sql.d.ts +4 -0
  699. package/src/storage/db.bun.ts +8 -0
  700. package/src/storage/db.node.ts +8 -0
  701. package/src/storage/db.ts +172 -0
  702. package/src/storage/index.ts +26 -0
  703. package/src/storage/json-migration.ts +426 -0
  704. package/src/storage/read-sqlite.bun.ts +11 -0
  705. package/src/storage/read-sqlite.node.ts +13 -0
  706. package/src/storage/read-sqlite.ts +10 -0
  707. package/src/storage/schema.sql.ts +10 -0
  708. package/src/storage/schema.ts +7 -0
  709. package/src/storage/storage.ts +331 -0
  710. package/src/sync/README.md +179 -0
  711. package/src/sync/event.sql.ts +16 -0
  712. package/src/sync/index.ts +278 -0
  713. package/src/sync/schema.ts +14 -0
  714. package/src/task/events.ts +28 -0
  715. package/src/task/gate-state.ts +54 -0
  716. package/src/task/gate.ts +116 -0
  717. package/src/task/index.ts +1 -0
  718. package/src/task/registry.ts +394 -0
  719. package/src/task/schema.ts +43 -0
  720. package/src/task/task.sql.ts +50 -0
  721. package/src/team/events.ts +22 -0
  722. package/src/team/index.ts +113 -0
  723. package/src/team/schema.ts +31 -0
  724. package/src/temporary.ts +33 -0
  725. package/src/tool/actor.shell.txt +72 -0
  726. package/src/tool/actor.ts +804 -0
  727. package/src/tool/actor.txt +103 -0
  728. package/src/tool/apply_patch.ts +308 -0
  729. package/src/tool/apply_patch.txt +33 -0
  730. package/src/tool/bash-interactive.ts +183 -0
  731. package/src/tool/bash.ts +722 -0
  732. package/src/tool/bash.txt +123 -0
  733. package/src/tool/bash_token_efficient_pipeline.ts +189 -0
  734. package/src/tool/change-directory.ts +91 -0
  735. package/src/tool/codesearch.ts +63 -0
  736. package/src/tool/codesearch.txt +12 -0
  737. package/src/tool/edit.ts +752 -0
  738. package/src/tool/edit.txt +10 -0
  739. package/src/tool/external-directory.ts +132 -0
  740. package/src/tool/glob.ts +100 -0
  741. package/src/tool/glob.txt +6 -0
  742. package/src/tool/grep.ts +145 -0
  743. package/src/tool/grep.txt +8 -0
  744. package/src/tool/history.ts +146 -0
  745. package/src/tool/history.txt +17 -0
  746. package/src/tool/index.ts +4 -0
  747. package/src/tool/invalid.ts +20 -0
  748. package/src/tool/invocation-style.ts +17 -0
  749. package/src/tool/lsp.ts +91 -0
  750. package/src/tool/lsp.txt +19 -0
  751. package/src/tool/mcp-exa.ts +78 -0
  752. package/src/tool/memory-path-guard.ts +162 -0
  753. package/src/tool/memory.ts +81 -0
  754. package/src/tool/memory.txt +69 -0
  755. package/src/tool/multiedit.ts +54 -0
  756. package/src/tool/multiedit.txt +41 -0
  757. package/src/tool/notebook-edit.ts +225 -0
  758. package/src/tool/notebook-edit.txt +10 -0
  759. package/src/tool/plan-enter.txt +16 -0
  760. package/src/tool/plan-exit.txt +14 -0
  761. package/src/tool/plan.ts +179 -0
  762. package/src/tool/question.ts +67 -0
  763. package/src/tool/question.txt +10 -0
  764. package/src/tool/read-state.ts +44 -0
  765. package/src/tool/read.ts +327 -0
  766. package/src/tool/read.txt +14 -0
  767. package/src/tool/recoverable.ts +35 -0
  768. package/src/tool/registry.ts +429 -0
  769. package/src/tool/schema.ts +17 -0
  770. package/src/tool/session-cwd.ts +35 -0
  771. package/src/tool/shell-tokenize.ts +374 -0
  772. package/src/tool/shell-wrap.ts +235 -0
  773. package/src/tool/skill.ts +76 -0
  774. package/src/tool/skill.txt +5 -0
  775. package/src/tool/task.shell.txt +57 -0
  776. package/src/tool/task.ts +456 -0
  777. package/src/tool/task.txt +56 -0
  778. package/src/tool/tool.ts +166 -0
  779. package/src/tool/truncate.ts +201 -0
  780. package/src/tool/truncation-dir.ts +4 -0
  781. package/src/tool/webfetch.ts +208 -0
  782. package/src/tool/webfetch.txt +13 -0
  783. package/src/tool/websearch/index.ts +104 -0
  784. package/src/tool/websearch/mimo.ts +118 -0
  785. package/src/tool/websearch/websearch.txt +14 -0
  786. package/src/tool/workflow.ts +357 -0
  787. package/src/tool/workflow.txt +25 -0
  788. package/src/tool/write.ts +88 -0
  789. package/src/tool/write.txt +10 -0
  790. package/src/util/abort.ts +35 -0
  791. package/src/util/archive.ts +15 -0
  792. package/src/util/color.ts +17 -0
  793. package/src/util/data-url.ts +9 -0
  794. package/src/util/defer.ts +10 -0
  795. package/src/util/effect-http-client.ts +11 -0
  796. package/src/util/effect-zod.ts +367 -0
  797. package/src/util/env-info.ts +62 -0
  798. package/src/util/error.ts +78 -0
  799. package/src/util/filesystem.ts +243 -0
  800. package/src/util/fn.ts +21 -0
  801. package/src/util/format.ts +20 -0
  802. package/src/util/iife.ts +3 -0
  803. package/src/util/index.ts +14 -0
  804. package/src/util/keybind.ts +101 -0
  805. package/src/util/lazy.ts +18 -0
  806. package/src/util/local-context.ts +23 -0
  807. package/src/util/locale.ts +79 -0
  808. package/src/util/lock.ts +96 -0
  809. package/src/util/log.ts +234 -0
  810. package/src/util/media.ts +26 -0
  811. package/src/util/network.ts +9 -0
  812. package/src/util/opendash-process.ts +24 -0
  813. package/src/util/process.ts +174 -0
  814. package/src/util/provider-priority.ts +48 -0
  815. package/src/util/queue.ts +60 -0
  816. package/src/util/record.ts +3 -0
  817. package/src/util/rpc.ts +64 -0
  818. package/src/util/schema.ts +53 -0
  819. package/src/util/scrap.ts +10 -0
  820. package/src/util/signal.ts +12 -0
  821. package/src/util/ssrf.ts +116 -0
  822. package/src/util/timeout.ts +14 -0
  823. package/src/util/token.ts +5 -0
  824. package/src/util/tool-compat.ts +144 -0
  825. package/src/util/update-schema.ts +13 -0
  826. package/src/util/which.ts +14 -0
  827. package/src/util/wildcard.ts +57 -0
  828. package/src/workflow/builtin/compose.js +749 -0
  829. package/src/workflow/builtin/deep-research.js +398 -0
  830. package/src/workflow/builtin.ts +59 -0
  831. package/src/workflow/events.ts +72 -0
  832. package/src/workflow/meta.ts +335 -0
  833. package/src/workflow/persistence.ts +312 -0
  834. package/src/workflow/resolve.ts +45 -0
  835. package/src/workflow/runtime-ref.ts +18 -0
  836. package/src/workflow/runtime.ts +1447 -0
  837. package/src/workflow/sandbox.ts +286 -0
  838. package/src/workflow/workflow.sql.ts +31 -0
  839. package/src/workflow/workspace.ts +69 -0
  840. package/src/worktree/index.ts +629 -0
  841. package/sst-env.d.ts +10 -0
  842. package/test/AGENTS.md +133 -0
  843. package/test/account/repo.test.ts +352 -0
  844. package/test/account/service.test.ts +456 -0
  845. package/test/acp/agent-interface.test.ts +51 -0
  846. package/test/acp/event-subscription.test.ts +725 -0
  847. package/test/actor/cancel-cascade.test.ts +432 -0
  848. package/test/actor/no-completion-listener.test.ts +41 -0
  849. package/test/actor/poststop-progress-write-permission.repro.test.ts +414 -0
  850. package/test/actor/registry-render.test.ts +113 -0
  851. package/test/actor/registry-status.test.ts +111 -0
  852. package/test/actor/registry.test.ts +619 -0
  853. package/test/actor/return-header.test.ts +40 -0
  854. package/test/actor/spawn-lifecycle.test.ts +346 -0
  855. package/test/actor/spawn-no-deadlock.test.ts +340 -0
  856. package/test/actor/spawn-notification.test.ts +393 -0
  857. package/test/actor/spawn-task-autostart.test.ts +530 -0
  858. package/test/actor/spawn.test.ts +1106 -0
  859. package/test/actor/status-event-payload.test.ts +132 -0
  860. package/test/actor/terminology.test.ts +39 -0
  861. package/test/actor/turn.test.ts +125 -0
  862. package/test/actor/waiter.test.ts +246 -0
  863. package/test/agent/agent.test.ts +935 -0
  864. package/test/agent/allowlist.test.ts +45 -0
  865. package/test/auth/auth.test.ts +86 -0
  866. package/test/bus/bus-effect.test.ts +162 -0
  867. package/test/bus/bus-integration.test.ts +87 -0
  868. package/test/bus/bus.test.ts +219 -0
  869. package/test/cli/account.test.ts +26 -0
  870. package/test/cli/cmd/tui/autocomplete-detect.test.ts +55 -0
  871. package/test/cli/cmd/tui/extmark-cjk.test.ts +96 -0
  872. package/test/cli/cmd/tui/offset.test.ts +101 -0
  873. package/test/cli/cmd/tui/prompt-part.test.ts +97 -0
  874. package/test/cli/error.test.ts +18 -0
  875. package/test/cli/github-action.test.ts +198 -0
  876. package/test/cli/github-remote.test.ts +80 -0
  877. package/test/cli/import.test.ts +54 -0
  878. package/test/cli/plugin-auth-picker.test.ts +120 -0
  879. package/test/cli/run-completion.test.ts +131 -0
  880. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  881. package/test/cli/tui/plugin-add.test.ts +111 -0
  882. package/test/cli/tui/plugin-install.test.ts +87 -0
  883. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  884. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  885. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  886. package/test/cli/tui/plugin-loader.test.ts +816 -0
  887. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  888. package/test/cli/tui/revert-diff.test.ts +35 -0
  889. package/test/cli/tui/route-agent-id.test.ts +26 -0
  890. package/test/cli/tui/sidebar-tps.test.ts +63 -0
  891. package/test/cli/tui/slot-replace.test.tsx +47 -0
  892. package/test/cli/tui/sync-bucket.test.ts +29 -0
  893. package/test/cli/tui/theme-store.test.ts +51 -0
  894. package/test/cli/tui/thread.test.ts +125 -0
  895. package/test/cli/tui/transcript.test.ts +426 -0
  896. package/test/cli/tui/use-event.test.tsx +175 -0
  897. package/test/cli/tui/voice.test.ts +443 -0
  898. package/test/command/deep-research-command.test.ts +16 -0
  899. package/test/config/agent-color.test.ts +77 -0
  900. package/test/config/checkpoint-fork.test.ts +21 -0
  901. package/test/config/config.test.ts +2577 -0
  902. package/test/config/fixtures/empty-frontmatter.md +4 -0
  903. package/test/config/fixtures/frontmatter.md +28 -0
  904. package/test/config/fixtures/markdown-header.md +11 -0
  905. package/test/config/fixtures/no-frontmatter.md +1 -0
  906. package/test/config/fixtures/weird-model-id.md +13 -0
  907. package/test/config/lsp.test.ts +87 -0
  908. package/test/config/markdown.test.ts +228 -0
  909. package/test/config/plugin.test.ts +0 -0
  910. package/test/config/tui.test.ts +627 -0
  911. package/test/control-plane/adaptors.test.ts +71 -0
  912. package/test/control-plane/sse.test.ts +56 -0
  913. package/test/effect/app-runtime-logger.test.ts +92 -0
  914. package/test/effect/cross-spawn-spawner.test.ts +411 -0
  915. package/test/effect/instance-state.test.ts +482 -0
  916. package/test/effect/observability.test.ts +46 -0
  917. package/test/effect/run-service.test.ts +46 -0
  918. package/test/effect/runner-warn-log.test.ts +111 -0
  919. package/test/effect/runner.test.ts +494 -0
  920. package/test/fake/provider.ts +90 -0
  921. package/test/file/fsmonitor.test.ts +68 -0
  922. package/test/file/ignore.test.ts +10 -0
  923. package/test/file/index.test.ts +963 -0
  924. package/test/file/path-traversal.test.ts +243 -0
  925. package/test/file/ripgrep.test.ts +218 -0
  926. package/test/file/watcher.test.ts +249 -0
  927. package/test/filesystem/filesystem.test.ts +319 -0
  928. package/test/fixture/db.ts +11 -0
  929. package/test/fixture/fixture.test.ts +58 -0
  930. package/test/fixture/fixture.ts +223 -0
  931. package/test/fixture/flock-worker.ts +72 -0
  932. package/test/fixture/lsp/fake-lsp-server.js +75 -0
  933. package/test/fixture/plug-worker.ts +93 -0
  934. package/test/fixture/plugin-meta-worker.ts +19 -0
  935. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  936. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  937. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  938. package/test/fixture/skills/index.json +6 -0
  939. package/test/fixture/tui-plugin.ts +328 -0
  940. package/test/fixture/tui-runtime.ts +31 -0
  941. package/test/format/format.test.ts +244 -0
  942. package/test/git/git.test.ts +128 -0
  943. package/test/global/fixture/global-paths-worker.ts +17 -0
  944. package/test/global/mimocode-home.test.ts +143 -0
  945. package/test/history/backfill.test.ts +160 -0
  946. package/test/history/extract.test.ts +106 -0
  947. package/test/history/fts-query.test.ts +30 -0
  948. package/test/history/resolve.test.ts +130 -0
  949. package/test/history/service.test.ts +210 -0
  950. package/test/history/writer.test.ts +163 -0
  951. package/test/ide/ide.test.ts +82 -0
  952. package/test/inbox/drain-in-loop.test.ts +230 -0
  953. package/test/inbox/fork-agent-compat.test.ts +387 -0
  954. package/test/inbox/gc-on-init.test.ts +167 -0
  955. package/test/inbox/send-no-block.test.ts +120 -0
  956. package/test/inbox/sender-cancel-independence.test.ts +160 -0
  957. package/test/inbox/wake-matrix.test.ts +141 -0
  958. package/test/installation/installation.test.ts +255 -0
  959. package/test/keybind.test.ts +421 -0
  960. package/test/lib/effect.ts +53 -0
  961. package/test/lib/filesystem.ts +10 -0
  962. package/test/lib/llm-server.ts +770 -0
  963. package/test/lib/mock-llm.ts +159 -0
  964. package/test/lib/scripted-llm-server.ts +245 -0
  965. package/test/lsp/client.test.ts +98 -0
  966. package/test/lsp/index.test.ts +109 -0
  967. package/test/lsp/launch.test.ts +22 -0
  968. package/test/lsp/lifecycle.test.ts +184 -0
  969. package/test/mcp/headers.test.ts +178 -0
  970. package/test/mcp/lifecycle.test.ts +786 -0
  971. package/test/mcp/oauth-auto-connect.test.ts +281 -0
  972. package/test/mcp/oauth-browser.test.ts +268 -0
  973. package/test/mcp/oauth-callback.test.ts +34 -0
  974. package/test/memory/abort-leak-webfetch.ts +49 -0
  975. package/test/memory/abort-leak.test.ts +127 -0
  976. package/test/memory/cc-frontmatter.test.ts +85 -0
  977. package/test/memory/cc-paths.test.ts +60 -0
  978. package/test/memory/cc-reconcile.test.ts +239 -0
  979. package/test/memory/cc-search.test.ts +151 -0
  980. package/test/memory/fts-query.test.ts +48 -0
  981. package/test/memory/fts-rowid-stability.test.ts +271 -0
  982. package/test/memory/paths.test.ts +210 -0
  983. package/test/memory/reconcile.test.ts +115 -0
  984. package/test/memory/service.test.ts +169 -0
  985. package/test/npm.test.ts +18 -0
  986. package/test/patch/patch.test.ts +348 -0
  987. package/test/permission/abort.test.ts +116 -0
  988. package/test/permission/arity.test.ts +33 -0
  989. package/test/permission/disabled.test.ts +68 -0
  990. package/test/permission/next.test.ts +1116 -0
  991. package/test/permission/non-interactive.test.ts +55 -0
  992. package/test/permission-task.test.ts +326 -0
  993. package/test/plugin/actor-hooks.test.ts +1471 -0
  994. package/test/plugin/auth-override.test.ts +79 -0
  995. package/test/plugin/checkpoint-splitover.test.ts +434 -0
  996. package/test/plugin/cloudflare.test.ts +68 -0
  997. package/test/plugin/codex.test.ts +123 -0
  998. package/test/plugin/github-copilot-models.test.ts +163 -0
  999. package/test/plugin/install-concurrency.test.ts +140 -0
  1000. package/test/plugin/install.test.ts +570 -0
  1001. package/test/plugin/loader-shared.test.ts +1169 -0
  1002. package/test/plugin/matcher.test.ts +97 -0
  1003. package/test/plugin/meta.test.ts +137 -0
  1004. package/test/plugin/mimo.test.ts +259 -0
  1005. package/test/plugin/session-loop-hooks.test.ts +159 -0
  1006. package/test/plugin/shared.test.ts +88 -0
  1007. package/test/plugin/subagent-progress-checker.test.ts +227 -0
  1008. package/test/plugin/trigger.test.ts +116 -0
  1009. package/test/plugin/workspace-adaptor.test.ts +109 -0
  1010. package/test/preload.ts +102 -0
  1011. package/test/project/migrate-global.test.ts +152 -0
  1012. package/test/project/project-id.test.ts +64 -0
  1013. package/test/project/project.test.ts +504 -0
  1014. package/test/project/vcs.test.ts +286 -0
  1015. package/test/project/worktree-remove.test.ts +126 -0
  1016. package/test/project/worktree.test.ts +222 -0
  1017. package/test/provider/amazon-bedrock.test.ts +462 -0
  1018. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  1019. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  1020. package/test/provider/error.test.ts +160 -0
  1021. package/test/provider/gitlab-duo.test.ts +413 -0
  1022. package/test/provider/model-groups.test.ts +389 -0
  1023. package/test/provider/provider-chunk-timeout.test.ts +23 -0
  1024. package/test/provider/provider.test.ts +2610 -0
  1025. package/test/provider/transform.test.ts +3523 -0
  1026. package/test/pty/pty-output-isolation.test.ts +146 -0
  1027. package/test/pty/pty-session.test.ts +102 -0
  1028. package/test/pty/pty-shell.test.ts +69 -0
  1029. package/test/question/question.test.ts +464 -0
  1030. package/test/server/global-session-list.test.ts +105 -0
  1031. package/test/server/project-init-git.test.ts +137 -0
  1032. package/test/server/rate-limit.test.ts +66 -0
  1033. package/test/server/session-actions.test.ts +49 -0
  1034. package/test/server/session-list.test.ts +110 -0
  1035. package/test/server/session-messages.test.ts +220 -0
  1036. package/test/server/session-prompt-busy.test.ts +146 -0
  1037. package/test/server/session-prompt-heartbeat.test.ts +131 -0
  1038. package/test/server/session-select.test.ts +100 -0
  1039. package/test/server/session-task-route.test.ts +165 -0
  1040. package/test/server/summarize-route-main-slice.test.ts +99 -0
  1041. package/test/server/trace-attributes.test.ts +76 -0
  1042. package/test/server/workflows-route.test.ts +363 -0
  1043. package/test/session/bootstrap-skip-system.test.ts +121 -0
  1044. package/test/session/boundary.test.ts +33 -0
  1045. package/test/session/budgeted-read.test.ts +74 -0
  1046. package/test/session/checkpoint-align.test.ts +58 -0
  1047. package/test/session/checkpoint-boundary.test.ts +186 -0
  1048. package/test/session/checkpoint-child-session.test.ts +509 -0
  1049. package/test/session/checkpoint-context.test.ts +141 -0
  1050. package/test/session/checkpoint-drain.test.ts +189 -0
  1051. package/test/session/checkpoint-extract-titles.test.ts +58 -0
  1052. package/test/session/checkpoint-fork-mode.test.ts +577 -0
  1053. package/test/session/checkpoint-main-slice.test.ts +261 -0
  1054. package/test/session/checkpoint-paths.test.ts +87 -0
  1055. package/test/session/checkpoint-permission.test.ts +136 -0
  1056. package/test/session/checkpoint-progress-reconcile.test.ts +219 -0
  1057. package/test/session/checkpoint-rebuild-unify.test.ts +158 -0
  1058. package/test/session/checkpoint-rebuild-v3.test.ts +259 -0
  1059. package/test/session/checkpoint-render-verify.test.ts +512 -0
  1060. package/test/session/checkpoint-retry.test.ts +150 -0
  1061. package/test/session/checkpoint-splitover-integration.test.ts +533 -0
  1062. package/test/session/checkpoint-templates.test.ts +51 -0
  1063. package/test/session/checkpoint-thresholds.test.ts +120 -0
  1064. package/test/session/checkpoint-validator.test.ts +189 -0
  1065. package/test/session/classify-integration.test.ts +476 -0
  1066. package/test/session/classify.test.ts +390 -0
  1067. package/test/session/codex-import.test.ts +331 -0
  1068. package/test/session/compaction-agent-scope.test.ts +209 -0
  1069. package/test/session/context-inheritance.test.ts +46 -0
  1070. package/test/session/external-import.test.ts +17 -0
  1071. package/test/session/fork-prefix-invariant.test.ts +116 -0
  1072. package/test/session/goal.test.ts +106 -0
  1073. package/test/session/instruction.test.ts +388 -0
  1074. package/test/session/invalid-output-continuation.test.ts +150 -0
  1075. package/test/session/last-message-info.test.ts +47 -0
  1076. package/test/session/length-tool-safety.test.ts +125 -0
  1077. package/test/session/llm-request-prefix.test.ts +197 -0
  1078. package/test/session/llm-retry.test.ts +59 -0
  1079. package/test/session/llm-system-prompt.test.ts +479 -0
  1080. package/test/session/llm.test.ts +1268 -0
  1081. package/test/session/main-lifecycle.test.ts +51 -0
  1082. package/test/session/main-runloop-history-invariant.test.ts +182 -0
  1083. package/test/session/max-mode-econnreset.test.ts +232 -0
  1084. package/test/session/max-mode.test.ts +54 -0
  1085. package/test/session/message-v2-filter.test.ts +197 -0
  1086. package/test/session/message-v2.test.ts +1119 -0
  1087. package/test/session/messages-default-main.test.ts +105 -0
  1088. package/test/session/messages-pagination.test.ts +888 -0
  1089. package/test/session/overflow.test.ts +576 -0
  1090. package/test/session/processor-effect.test.ts +853 -0
  1091. package/test/session/prompt-effect.test.ts +1534 -0
  1092. package/test/session/prompt-rebuild-loop.test.ts +108 -0
  1093. package/test/session/prompt-rebuild-reset.test.ts +67 -0
  1094. package/test/session/prompt-sweep.test.ts +145 -0
  1095. package/test/session/prompt-task-gate.test.ts +127 -0
  1096. package/test/session/prompt.test.ts +827 -0
  1097. package/test/session/prune-main-slice.test.ts +272 -0
  1098. package/test/session/prune-skip-system.test.ts +346 -0
  1099. package/test/session/prune.test.ts +419 -0
  1100. package/test/session/rebuild-microcompact.test.ts +318 -0
  1101. package/test/session/recall-reminder.test.ts +37 -0
  1102. package/test/session/recent-user-msg.test.ts +219 -0
  1103. package/test/session/retry.test.ts +410 -0
  1104. package/test/session/revert-compact.test.ts +639 -0
  1105. package/test/session/run-state-tuple-key.test.ts +152 -0
  1106. package/test/session/session-create-registers-main.test.ts +70 -0
  1107. package/test/session/session.test.ts +181 -0
  1108. package/test/session/snapshot-tool-race.test.ts +301 -0
  1109. package/test/session/structured-output-integration.test.ts +264 -0
  1110. package/test/session/structured-output-retry.test.ts +127 -0
  1111. package/test/session/structured-output.test.ts +397 -0
  1112. package/test/session/summary-main-slice.test.ts +170 -0
  1113. package/test/session/system.test.ts +72 -0
  1114. package/test/session/text-loop-detection.test.ts +185 -0
  1115. package/test/session/text-loop-integration.test.ts +448 -0
  1116. package/test/session/text-ngram-detection.test.ts +169 -0
  1117. package/test/session/trajectory.test.ts +236 -0
  1118. package/test/share/share-next.test.ts +332 -0
  1119. package/test/shell/shell.test.ts +73 -0
  1120. package/test/skill/compose-review.test.ts +174 -0
  1121. package/test/skill/discovery.test.ts +116 -0
  1122. package/test/skill/skill.test.ts +466 -0
  1123. package/test/snapshot/snapshot.test.ts +1531 -0
  1124. package/test/storage/db.test.ts +16 -0
  1125. package/test/storage/json-migration.test.ts +831 -0
  1126. package/test/storage/storage.test.ts +293 -0
  1127. package/test/sync/index.test.ts +237 -0
  1128. package/test/task/gate-state.test.ts +66 -0
  1129. package/test/task/gate.test.ts +167 -0
  1130. package/test/task/registry.test.ts +171 -0
  1131. package/test/task/state-machine.test.ts +292 -0
  1132. package/test/team/migrate-to-inbox.test.ts +124 -0
  1133. package/test/team/team.test.ts +75 -0
  1134. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  1135. package/test/tool/actor-cancel.test.ts +211 -0
  1136. package/test/tool/actor-recover.test.ts +50 -0
  1137. package/test/tool/actor-send.test.ts +200 -0
  1138. package/test/tool/actor-status.test.ts +296 -0
  1139. package/test/tool/actor-wait.test.ts +193 -0
  1140. package/test/tool/actor.shell.test.ts +266 -0
  1141. package/test/tool/actor.test.ts +753 -0
  1142. package/test/tool/apply_patch.test.ts +626 -0
  1143. package/test/tool/bash.test.ts +1232 -0
  1144. package/test/tool/bash_token_efficient_pipeline.test.ts +329 -0
  1145. package/test/tool/describe-workflow.test.ts +12 -0
  1146. package/test/tool/edit.test.ts +730 -0
  1147. package/test/tool/external-directory.test.ts +207 -0
  1148. package/test/tool/fixtures/large-image.png +0 -0
  1149. package/test/tool/fixtures/models-api.json +65179 -0
  1150. package/test/tool/glob.test.ts +81 -0
  1151. package/test/tool/grep.test.ts +114 -0
  1152. package/test/tool/history.test.ts +144 -0
  1153. package/test/tool/invocation-style.test.ts +30 -0
  1154. package/test/tool/memory-edit-ask-skip.test.ts +62 -0
  1155. package/test/tool/memory-path-guard.test.ts +594 -0
  1156. package/test/tool/memory.test.ts +71 -0
  1157. package/test/tool/question.test.ts +167 -0
  1158. package/test/tool/read.test.ts +483 -0
  1159. package/test/tool/recoverable.test.ts +36 -0
  1160. package/test/tool/registry-invocation-style.test.ts +121 -0
  1161. package/test/tool/registry.test.ts +164 -0
  1162. package/test/tool/shell-tokenize.test.ts +309 -0
  1163. package/test/tool/shell-wrap-missing-script.test.ts +128 -0
  1164. package/test/tool/shell-wrap.test.ts +345 -0
  1165. package/test/tool/skill.test.ts +99 -0
  1166. package/test/tool/task-recover.test.ts +36 -0
  1167. package/test/tool/task.shell.test.ts +234 -0
  1168. package/test/tool/task.test.ts +296 -0
  1169. package/test/tool/tool-def-shell-shape.test.ts +23 -0
  1170. package/test/tool/tool-define.test.ts +59 -0
  1171. package/test/tool/tool-validation-error.test.ts +25 -0
  1172. package/test/tool/truncation.test.ts +253 -0
  1173. package/test/tool/webfetch.test.ts +103 -0
  1174. package/test/tool/whitelist.test.ts +377 -0
  1175. package/test/tool/write.test.ts +244 -0
  1176. package/test/util/data-url.test.ts +14 -0
  1177. package/test/util/effect-zod.test.ts +869 -0
  1178. package/test/util/env-info.test.ts +26 -0
  1179. package/test/util/error.test.ts +38 -0
  1180. package/test/util/filesystem.test.ts +656 -0
  1181. package/test/util/format.test.ts +59 -0
  1182. package/test/util/glob.test.ts +164 -0
  1183. package/test/util/iife.test.ts +36 -0
  1184. package/test/util/lazy.test.ts +50 -0
  1185. package/test/util/lock.test.ts +72 -0
  1186. package/test/util/log.test.ts +69 -0
  1187. package/test/util/module.test.ts +59 -0
  1188. package/test/util/process.test.ts +128 -0
  1189. package/test/util/queue.test.ts +64 -0
  1190. package/test/util/ssrf.test.ts +115 -0
  1191. package/test/util/timeout.test.ts +21 -0
  1192. package/test/util/tool-compat.test.ts +189 -0
  1193. package/test/util/which.test.ts +100 -0
  1194. package/test/util/wildcard.test.ts +90 -0
  1195. package/test/workflow/builtin.test.ts +36 -0
  1196. package/test/workflow/compose.test.ts +668 -0
  1197. package/test/workflow/deep-research-cluster.test.ts +47 -0
  1198. package/test/workflow/lib.ts +243 -0
  1199. package/test/workflow/meta.test.ts +142 -0
  1200. package/test/workflow/model-routing.test.ts +68 -0
  1201. package/test/workflow/persistence.test.ts +229 -0
  1202. package/test/workflow/resolve.test.ts +37 -0
  1203. package/test/workflow/runtime-nested.test.ts +419 -0
  1204. package/test/workflow/runtime-worktree.test.ts +261 -0
  1205. package/test/workflow/runtime.test.ts +1142 -0
  1206. package/test/workflow/sandbox.test.ts +259 -0
  1207. package/test/workflow/tool.test.ts +487 -0
  1208. package/test/workflow/verify-wow.test.ts +144 -0
  1209. package/test/workflow/workspace.test.ts +88 -0
  1210. package/test/workspace/workspace-restore.test.ts +281 -0
  1211. package/test/worktree/index.test.ts +30 -0
  1212. package/tsconfig.json +24 -0
@@ -0,0 +1,3878 @@
1
+ import path from "path"
2
+ import os from "os"
3
+ import z from "zod"
4
+ import { SessionID, MessageID, PartID } from "./schema"
5
+ import { MessageV2 } from "./message-v2"
6
+ import { classifyAssistantStep } from "./classify"
7
+ import { Log } from "../util"
8
+ import { SessionRevert } from "./revert"
9
+ import * as Session from "./session"
10
+ import { Agent } from "../agent/agent"
11
+ import { SYSTEM_SPAWNED_AGENT_TYPES } from "@/agent/config"
12
+ import { Provider } from "../provider"
13
+ import { ModelID, ProviderID } from "../provider/schema"
14
+ import { type Tool as AITool, type ModelMessage, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
15
+ import type { JSONSchema7 } from "@ai-sdk/provider"
16
+ import { SessionPrune } from "./prune"
17
+ import { SessionCheckpoint } from "./checkpoint"
18
+ import { SessionCompaction } from "./compaction"
19
+ import { computeLastMessageInfo } from "./last-message-info"
20
+ import { pressureLevel, isOverflow as overflowCheck } from "./overflow"
21
+ import { Config } from "@/config"
22
+ import { Global } from "@/global"
23
+ import { Bus } from "../bus"
24
+ import { ProviderTransform } from "../provider"
25
+ import { SystemPrompt } from "./system"
26
+ import { Instruction } from "./instruction"
27
+ import { TuiEvent } from "@/cli/cmd/tui/event"
28
+ import { Plugin } from "../plugin"
29
+ import BUILD_SWITCH from "../session/prompt/build-switch.txt"
30
+ import MAX_STEPS from "../session/prompt/max-steps.txt"
31
+ import PROMPT_COMPOSE from "../session/prompt/compose.txt"
32
+ import {
33
+ RECOVERY_PROMPT_MILD,
34
+ RECOVERY_PROMPT_STRONG,
35
+ TEXT_LOOP_BUFFER_SIZE,
36
+ TEXT_LOOP_TRIGGER_COUNT,
37
+ TEXT_LOOP_MAX_RECOVERY,
38
+ normalizeForLoopDetection,
39
+ detectTextLoop,
40
+ } from "../session/prompt/text-loop-recovery"
41
+ import {
42
+ TEXT_NGRAM_MAX_RECOVERY,
43
+ TEXT_NGRAM_RECOVERY_REMIND,
44
+ TEXT_NGRAM_RECOVERY_REPLAN,
45
+ } from "../session/prompt/text-ngram-detection"
46
+ import { composeSkillsBlock } from "@/skill/compose/extract"
47
+ import { ToolRegistry } from "../tool"
48
+ import { MCP } from "../mcp"
49
+ import { LSP } from "../lsp"
50
+ import { Flag } from "../flag/flag"
51
+ import { ulid } from "ulid"
52
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
53
+ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
54
+ import * as Stream from "effect/Stream"
55
+ import { Command } from "../command"
56
+ import { pathToFileURL, fileURLToPath } from "url"
57
+ import { ConfigMarkdown, ConfigCompose } from "../config"
58
+ import { SessionSummary } from "./summary"
59
+ import { NamedError } from "@opendash-ai/shared/util/error"
60
+ import { SessionProcessor } from "./processor"
61
+ import { buildLLMRequestPrefix } from "./llm-request-prefix"
62
+ import {
63
+ serializeTrajectoryMessages,
64
+ withAssistantParts,
65
+ userQueryText,
66
+ assistantFinalText,
67
+ sessionErrorText,
68
+ } from "./trajectory"
69
+ import { prefixCaptureRef } from "./prefix-capture-ref"
70
+ import { spawnRef } from "@/actor/spawn-ref"
71
+ import { Inbox } from "@/inbox"
72
+ import { sessionPromptRef } from "@/inbox/inbox-ref"
73
+ import { Tool } from "@/tool"
74
+ import { Permission } from "@/permission"
75
+ import { SessionStatus } from "./status"
76
+ import { LLM } from "./llm"
77
+ import { MaxMode } from "./max-mode"
78
+ import { Shell } from "@/shell/shell"
79
+ import { AppFileSystem } from "@opendash-ai/shared/filesystem"
80
+ import { Truncate } from "@/tool"
81
+ import { decodeDataUrl } from "@/util/data-url"
82
+ import { Process } from "@/util"
83
+ import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
84
+ import { EffectLogger } from "@/effect"
85
+ import { InstanceState } from "@/effect"
86
+ import { ActorTool, type ActorPromptOps } from "@/tool/actor"
87
+ import { SessionRunState } from "./run-state"
88
+ import { Goal } from "./goal"
89
+ import { TaskGate, MAX_TASK_GATE_MAIN_REACT } from "@/task/gate"
90
+ import { TaskGateState } from "@/task/gate-state"
91
+ import { TaskRegistry } from "@/task/registry"
92
+ import { EffectBridge } from "@/effect"
93
+ import { Team } from "@/team"
94
+ import { ActorRegistry } from "@/actor/registry"
95
+ import { Metrics } from "@/metrics"
96
+ import { resolveInvocationStyle, type ToolStyleConfig } from "../tool/invocation-style"
97
+ import { shouldAutoDream, shouldAutoDistill, DREAM_TASK, DISTILL_TASK, AUTO_DREAM_TITLE, AUTO_DISTILL_TITLE } from "./auto-dream"
98
+
99
+ // @ts-ignore
100
+ globalThis.AI_SDK_LOG_WARNINGS = false
101
+
102
+ // Recall-reminder hints, rendered in each tool's configured invocation style so
103
+ // shell-mode sessions never see a JSON-shaped example (which primes models to
104
+ // emit JSON and crash the shell parser). `memory` has no shell form, so it is
105
+ // always JSON. Exported for unit testing.
106
+ export function recallHintLines(toolCfg: ToolStyleConfig | undefined): string[] {
107
+ const taskHint =
108
+ resolveInvocationStyle(toolCfg, "task") === "shell" ? "- task list" : `- task({ operation: "list" })`
109
+ const actorHint =
110
+ resolveInvocationStyle(toolCfg, "actor") === "shell"
111
+ ? "- actor status <actor_id>"
112
+ : `- actor({ operation: "status", actor_id: "<id>" })`
113
+ // memory has no shell form (no shell.parse) → always JSON.
114
+ return [`- memory({ operation: "search", query: "<keyword>" })`, taskHint, actorHint]
115
+ }
116
+
117
+ /**
118
+ * Cap on goal-driven main-loop re-entries per turn — the safety valve against
119
+ * a never-satisfiable condition burning tokens forever. Higher than spawned
120
+ * actors' MAX_PRE_REACT (=3) because main-session goals are usually larger.
121
+ * TODO: lift to opendash.json config (e.g. session.maxGoalReact).
122
+ */
123
+ const MAX_GOAL_REACT = 12
124
+
125
+ /**
126
+ * Number of consecutive finished assistant steps with an identical action
127
+ * signature that trips the repeated-step nudge. Three in a row is a strong
128
+ * signal the model is stuck repeating itself rather than making progress.
129
+ */
130
+ const REPEATED_STEP_THRESHOLD = 3
131
+
132
+ /**
133
+ * Deterministic JSON serialization with sorted object keys, so that two
134
+ * semantically-identical tool inputs produce the same string regardless of the
135
+ * order the model happened to emit the keys in. `JSON.stringify` preserves
136
+ * insertion order, and models routinely re-emit the same arguments with keys in
137
+ * a different order (e.g. {url,format} vs {format,url}) — without this the
138
+ * signatures would differ and the repeated-step check would miss real loops.
139
+ */
140
+ function stableStringify(value: unknown): string {
141
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null"
142
+ if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"
143
+ const keys = Object.keys(value as Record<string, unknown>).sort()
144
+ return (
145
+ "{" +
146
+ keys.map((k) => JSON.stringify(k) + ":" + stableStringify((value as Record<string, unknown>)[k])).join(",") +
147
+ "}"
148
+ )
149
+ }
150
+
151
+ /**
152
+ * Stable signature for an assistant step's *action* — the tool calls it made
153
+ * (name + key-order-independent input). Text and reasoning are excluded on
154
+ * purpose: in a ReAct loop the model narrates each step in slightly different
155
+ * words while taking the exact same action, and some models emit their
156
+ * reasoning as plain text parts — counting either would mask the repeated
157
+ * action we want to catch. Returns undefined when a step makes no tool calls
158
+ * (e.g. a pure-text turn), since there is no repeated *action* to compare.
159
+ */
160
+ function stepSignature(parts: MessageV2.Part[]): string | undefined {
161
+ const segments: string[] = []
162
+ for (const part of parts) {
163
+ if (part.type === "tool") {
164
+ segments.push("tool:" + part.tool + ":" + stableStringify(part.state.input ?? {}))
165
+ }
166
+ }
167
+ if (segments.length === 0) return undefined
168
+ return segments.join("\n")
169
+ }
170
+
171
+ const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
172
+
173
+ IMPORTANT:
174
+ - You MUST call this tool exactly once at the end of your response
175
+ - The input must be valid JSON matching the required schema
176
+ - Complete all necessary research and tool calls BEFORE calling this tool
177
+ - This tool provides your final answer - no further actions are taken after calling it`
178
+
179
+ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
180
+
181
+ const PREDICT_SYSTEM = `You predict the single most likely next message a user will send to a coding assistant, based on the conversation so far. Output only that next message as one short, natural first-person request (what the user would type). No preamble, no quotes, no explanation, no markdown. Keep it under 100 characters.`
182
+
183
+ const PREDICT_NUDGE = `Based on the conversation above, write the user's most likely next message:`
184
+
185
+ const OUTPUT_LENGTH_CONTINUATION_LIMIT = Flag.OPENDASH_OUTPUT_LENGTH_CONTINUATION_LIMIT
186
+ const INVALID_OUTPUT_CONTINUATION_LIMIT = Flag.OPENDASH_INVALID_OUTPUT_CONTINUATION_LIMIT
187
+ const TEXT_TOOL_CALL_RETRY_LIMIT = Flag.OPENDASH_TEXT_TOOL_CALL_RETRY_LIMIT
188
+
189
+ const log = Log.create({ service: "session.prompt" })
190
+
191
+ function isExtensionPath(filePath: string): boolean {
192
+ return /\/\.opendash\/(tools?|skills?|hooks?)\//.test(filePath)
193
+ }
194
+ const elog = EffectLogger.create({ service: "session.prompt" })
195
+
196
+ export interface Interface {
197
+ readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
198
+ readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
199
+ readonly loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts>
200
+ readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
201
+ readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts>
202
+ readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
203
+ readonly sweepOrphanAssistants: (sessionID: SessionID) => Effect.Effect<void>
204
+ readonly predict: (input: { sessionID: SessionID }) => Effect.Effect<string>
205
+ }
206
+
207
+ export class Service extends Context.Service<Service, Interface>()("@opendash/SessionPrompt") {}
208
+
209
+ export const layer = Layer.effect(
210
+ Service,
211
+ Effect.gen(function* () {
212
+ const bus = yield* Bus.Service
213
+ const status = yield* SessionStatus.Service
214
+ const sessions = yield* Session.Service
215
+ const agents = yield* Agent.Service
216
+ const provider = yield* Provider.Service
217
+ const processor = yield* SessionProcessor.Service
218
+ const prune = yield* SessionPrune.Service
219
+ const checkpoint = yield* SessionCheckpoint.Service
220
+ const compaction = yield* SessionCompaction.Service
221
+ const config = yield* Config.Service
222
+ const plugin = yield* Plugin.Service
223
+ const commands = yield* Command.Service
224
+ const permission = yield* Permission.Service
225
+ const fsys = yield* AppFileSystem.Service
226
+ const mcp = yield* MCP.Service
227
+ const lsp = yield* LSP.Service
228
+ const registry = yield* ToolRegistry.Service
229
+ const truncate = yield* Truncate.Service
230
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
231
+ const scope = yield* Scope.Scope
232
+ const instruction = yield* Instruction.Service
233
+ const state = yield* SessionRunState.Service
234
+ const goal = yield* Goal.Service
235
+ const taskGateState = yield* TaskGateState.Service
236
+ const taskRegistry = yield* TaskRegistry.Service
237
+ const revert = yield* SessionRevert.Service
238
+ const summary = yield* SessionSummary.Service
239
+ const sys = yield* SystemPrompt.Service
240
+ const llm = yield* LLM.Service
241
+ const actorRegistry = yield* ActorRegistry.Service
242
+ const inbox = yield* Inbox.Service
243
+
244
+ // Track sessions that have already shown the "loaded instructions" toast so we
245
+ // surface it once per primary session rather than on every run-loop turn.
246
+ const instructionsNotified = new Set<SessionID>()
247
+
248
+ // Late-bind prefix-capture helper so SessionCheckpoint.tryStartCheckpointWriter
249
+ // can call buildLLMRequestPrefix without forming a layer cycle
250
+ // (ToolRegistry → SessionCheckpoint → ToolRegistry). See prefix-capture-ref.ts.
251
+ // The closure resolves Agent.Info and Provider.Model internally so checkpoint.ts
252
+ // only needs to pass string IDs.
253
+ const capture: typeof prefixCaptureRef.current = (input) =>
254
+ Effect.gen(function* () {
255
+ const empty = { system: [] as string[], tools: {} as Record<string, AITool>, inheritedMessages: [] as ModelMessage[], parentPermission: [] as Permission.Ruleset }
256
+ const ag = yield* agents.get(input.agentName).pipe(Effect.catch(() => Effect.succeed(undefined)))
257
+ if (!ag) return empty
258
+ const model = yield* provider
259
+ .getModel(input.providerID as ProviderID, input.modelID as ModelID)
260
+ .pipe(Effect.catch(() => Effect.succeed(undefined)))
261
+ if (!model) return empty
262
+ // Anchor the env date to the session's creation time so the captured prefix is
263
+ // byte-identical to the runLoop's (which uses session.time.created), preserving
264
+ // Anthropic cache parity. If the session can't be loaded we can't guarantee that
265
+ // parity, so fall through to empty rather than emit a divergent date.
266
+ const captureSession = yield* sessions.get(input.sessionID).pipe(Effect.catch(() => Effect.succeed(undefined)))
267
+ if (!captureSession) return empty
268
+ const [skills, env, instructions] = yield* Effect.all([
269
+ sys.skills(ag),
270
+ Effect.sync(() => sys.environment(model, captureSession.time.created)),
271
+ instruction.system().pipe(Effect.orDie),
272
+ ])
273
+ // (checkpoint-writer never requests json_schema output, so STRUCTURED_OUTPUT_SYSTEM_PROMPT
274
+ // is not included; parent's runLoop adds it conditionally based on user.format)
275
+ const additions = [...env, ...(skills ? [skills] : []), ...instructions.content]
276
+ const prefix = yield* buildLLMRequestPrefix({
277
+ sessionID: input.sessionID,
278
+ agent: ag,
279
+ model,
280
+ msgs: input.msgs as Parameters<typeof buildLLMRequestPrefix>[0]["msgs"],
281
+ additions,
282
+ }).pipe(
283
+ Effect.provideService(LLM.Service, llm),
284
+ Effect.provideService(ToolRegistry.Service, registry),
285
+ Effect.catch(() => Effect.succeed(empty)),
286
+ )
287
+ return { ...prefix, parentPermission: ag.permission }
288
+ })
289
+ prefixCaptureRef.current = capture
290
+ yield* Effect.addFinalizer(() =>
291
+ Effect.sync(() => {
292
+ if (prefixCaptureRef.current === capture) prefixCaptureRef.current = undefined
293
+ }),
294
+ )
295
+
296
+ const runner = Effect.fn("SessionPrompt.runner")(function* () {
297
+ return yield* EffectBridge.make()
298
+ })
299
+ const ops = Effect.fn("SessionPrompt.ops")(function* () {
300
+ const run = yield* runner()
301
+ return {
302
+ cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)),
303
+ resolvePromptParts: (template: string) => resolvePromptParts(template),
304
+ prompt: (input: PromptInput) => prompt(input),
305
+ } satisfies ActorPromptOps
306
+ })
307
+
308
+ const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
309
+ yield* elog.info("cancel", { sessionID })
310
+ yield* state.cancel(sessionID)
311
+ })
312
+
313
+ const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
314
+ const ctx = yield* InstanceState.context
315
+ const parts: PromptInput["parts"] = [{ type: "text", text: template }]
316
+ const files = ConfigMarkdown.files(template)
317
+ const seen = new Set<string>()
318
+ yield* Effect.forEach(
319
+ files,
320
+ Effect.fnUntraced(function* (match) {
321
+ const name = match[1]
322
+ if (seen.has(name)) return
323
+ seen.add(name)
324
+ const filepath = name.startsWith("~/")
325
+ ? path.join(os.homedir(), name.slice(2))
326
+ : path.resolve(ctx.worktree, name)
327
+
328
+ const info = yield* fsys.stat(filepath).pipe(Effect.option)
329
+ if (Option.isNone(info)) {
330
+ const found = yield* agents.get(name)
331
+ if (found) parts.push({ type: "agent", name: found.name })
332
+ return
333
+ }
334
+ const stat = info.value
335
+ parts.push({
336
+ type: "file",
337
+ url: pathToFileURL(filepath).href,
338
+ filename: name,
339
+ mime: stat.type === "Directory" ? "application/x-directory" : "text/plain",
340
+ })
341
+ }),
342
+ { concurrency: "unbounded", discard: true },
343
+ )
344
+ return parts
345
+ })
346
+
347
+ const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
348
+ session: Session.Info
349
+ history: MessageV2.WithParts[]
350
+ providerID: ProviderID
351
+ modelID: ModelID
352
+ }) {
353
+ if (input.session.parentID) return
354
+ if (!Session.isDefaultTitle(input.session.title)) return
355
+
356
+ const real = (m: MessageV2.WithParts) =>
357
+ m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
358
+ const idx = input.history.findIndex(real)
359
+ if (idx === -1) return
360
+ if (input.history.filter(real).length !== 1) return
361
+
362
+ const context = input.history.slice(0, idx + 1)
363
+ const firstUser = context[idx]
364
+ if (!firstUser || firstUser.info.role !== "user") return
365
+ const firstInfo = firstUser.info
366
+
367
+ const subtasks = firstUser.parts.filter((p): p is MessageV2.SubtaskPart => p.type === "subtask")
368
+ const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
369
+
370
+ const ag = yield* agents.get("title")
371
+ if (!ag) return
372
+ const mdl = ag.modelRef
373
+ ? yield* provider.resolveModelRef(ag.modelRef, input.providerID)
374
+ : ag.model
375
+ ? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
376
+ : ((yield* provider.getSmallModel(input.providerID)) ??
377
+ (yield* provider.getModel(input.providerID, input.modelID)))
378
+ const msgs = onlySubtasks
379
+ ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
380
+ : yield* MessageV2.toModelMessagesEffect(context, mdl)
381
+ const text = yield* llm
382
+ .stream({
383
+ agent: ag,
384
+ user: firstInfo,
385
+ system: [],
386
+ small: true,
387
+ tools: {},
388
+ model: mdl,
389
+ sessionID: input.session.id,
390
+ retries: 2,
391
+ messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
392
+ })
393
+ .pipe(
394
+ Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
395
+ Stream.map((e) => e.text),
396
+ Stream.mkString,
397
+ Effect.orDie,
398
+ )
399
+ const cleaned = text
400
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
401
+ .split("\n")
402
+ .map((line) => line.trim())
403
+ .find((line) => line.length > 0)
404
+ if (!cleaned) return
405
+ const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
406
+ yield* sessions
407
+ .setTitle({ sessionID: input.session.id, title: t })
408
+ .pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
409
+ })
410
+
411
+ const predict = Effect.fn("SessionPrompt.predict")(function* (input: { sessionID: SessionID }) {
412
+ const cfg = yield* config.get()
413
+ if (cfg.experimental?.predict_next_prompt === false) return ""
414
+
415
+ const history = yield* sessions.messages({ sessionID: input.sessionID, agentID: "main" })
416
+ const real = (m: MessageV2.WithParts) =>
417
+ m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
418
+ const userIdx = history.findLastIndex(real)
419
+ if (userIdx === -1) return ""
420
+ const lastUser = history[userIdx]
421
+ if (lastUser.info.role !== "user") return ""
422
+
423
+ // Only the assistant turn that actually answered this user message counts.
424
+ // Bail if that turn is still running (an incomplete assistant after it),
425
+ // so we never pair the newest prompt with a stale/older result.
426
+ const assistants = history
427
+ .slice(userIdx + 1)
428
+ .filter((m): m is MessageV2.WithParts & { info: MessageV2.Assistant } => m.info.role === "assistant")
429
+ if (assistants.length === 0) return ""
430
+ if (assistants.some((m) => m.info.time.completed === undefined)) return ""
431
+ const lastAssistant = assistants[assistants.length - 1]
432
+
433
+ const base = yield* agents.get("title")
434
+ if (!base) return ""
435
+ // Reuse the lightweight title agent's settings but swap its prompt for the
436
+ // prediction prompt — its default ("output ONLY a thread title") would
437
+ // otherwise be prepended ahead of PREDICT_SYSTEM and win.
438
+ const ag = { ...base, prompt: PREDICT_SYSTEM }
439
+ const mdl = ag.modelRef
440
+ ? yield* provider.resolveModelRef(ag.modelRef, lastAssistant.info.providerID)
441
+ : ag.model
442
+ ? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
443
+ : ((yield* provider.getSmallModel(lastAssistant.info.providerID)) ??
444
+ (yield* provider.getModel(lastAssistant.info.providerID, lastAssistant.info.modelID)))
445
+
446
+ const msgs = yield* MessageV2.toModelMessagesEffect([lastUser, lastAssistant], mdl, { stripMedia: true })
447
+ const text = yield* llm
448
+ .stream({
449
+ agent: ag,
450
+ user: lastUser.info,
451
+ system: [],
452
+ small: true,
453
+ tools: {},
454
+ model: mdl,
455
+ sessionID: input.sessionID,
456
+ retries: 1,
457
+ messages: [...msgs, { role: "user", content: PREDICT_NUDGE }],
458
+ })
459
+ .pipe(
460
+ Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
461
+ Stream.map((e) => e.text),
462
+ Stream.mkString,
463
+ Effect.orElseSucceed(() => ""),
464
+ )
465
+ const cleaned = text
466
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
467
+ .split("\n")
468
+ .map((line) => line.trim())
469
+ .find((line) => line.length > 0)
470
+ if (!cleaned) return ""
471
+ const stripped = cleaned.replace(quoteTrimRegex, "")
472
+ return stripped.length > 120 ? stripped.substring(0, 117) + "..." : stripped
473
+ })
474
+
475
+ const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
476
+ messages: MessageV2.WithParts[]
477
+ agent: Agent.Info
478
+ session: Session.Info
479
+ }) {
480
+ const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
481
+ if (!userMessage) return input.messages
482
+
483
+ const composeModeMsg = input.messages.find(
484
+ (msg) => msg.info.role === "user" && msg.info.agent === "compose",
485
+ )
486
+ if (composeModeMsg) {
487
+ const composeModeBlock = composeSkillsBlock()
488
+ const ctx = yield* InstanceState.context
489
+ const composeCfg = (yield* config.get()).compose
490
+ const docsDir = ConfigCompose.resolveDocsDir(ctx.worktree, composeCfg)
491
+ const composeDocsBlock = [
492
+ "<compose_docs_dir>",
493
+ `Save compose skill outputs: specs in \`${path.join(docsDir, "specs")}\`, plans in \`${path.join(docsDir, "plans")}\`, reports in \`${path.join(docsDir, "reports")}\`.`,
494
+ "</compose_docs_dir>",
495
+ ].join("\n")
496
+ composeModeMsg.parts.unshift({
497
+ id: PartID.ascending(),
498
+ messageID: composeModeMsg.info.id,
499
+ sessionID: composeModeMsg.info.sessionID,
500
+ type: "text",
501
+ text:
502
+ PROMPT_COMPOSE +
503
+ (composeModeBlock ? "\n\n" + composeModeBlock : "") +
504
+ "\n\n" +
505
+ composeDocsBlock,
506
+ synthetic: true,
507
+ })
508
+ }
509
+
510
+ const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
511
+ if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
512
+ const plan = Session.plan(input.session)
513
+ if (!(yield* fsys.existsSafe(plan))) return input.messages
514
+ const part = yield* sessions.updatePart({
515
+ id: PartID.ascending(),
516
+ messageID: userMessage.info.id,
517
+ sessionID: userMessage.info.sessionID,
518
+ type: "text",
519
+ text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`,
520
+ synthetic: true,
521
+ })
522
+ userMessage.parts.push(part)
523
+ return input.messages
524
+ }
525
+
526
+ if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
527
+
528
+ const plan = Session.plan(input.session)
529
+ const exists = yield* fsys.existsSafe(plan)
530
+ if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
531
+ const part = yield* sessions.updatePart({
532
+ id: PartID.ascending(),
533
+ messageID: userMessage.info.id,
534
+ sessionID: userMessage.info.sessionID,
535
+ type: "text",
536
+ text: `<system-reminder>
537
+ Plan mode is active. The user wants you to research and design, NOT to execute yet. This supersedes any other instructions you have received.
538
+
539
+ ## What you SHOULD do (recommended)
540
+ - Prefer the dedicated read-only tools for everything they cover — \`read\` (view files), \`grep\` (search contents), \`glob\` (find files), and the \`lsp\` tools (definitions, references, diagnostics). These are the right way to explore the code.
541
+ - Spawn \`explore\`/\`general\` subagents for parallel research.
542
+ - Only when those tools genuinely can't get what you need, you MAY use \`bash\` for the gap — but ONLY for commands you are certain are a pure read with NO side effects (e.g. \`git status\`/\`log\`/\`diff\`, listing dependencies). Do NOT reach for \`bash\` to do what \`read\`/\`grep\`/\`glob\` already do.
543
+
544
+ ## What you MUST NOT do
545
+ - Do NOT edit or create any file other than the plan file below. Writes to non-plan files are blocked outright and will fail — do not attempt them and do not ask the user to approve them.
546
+ - Do NOT run \`test\`, \`lint\`, \`typecheck\`, \`build\`, or similar project commands. These are NOT safe by default: \`lint\` is often configured with \`--fix\`, \`test\` may write snapshots or touch a database, \`build\` writes artifacts, and scripts behind them can do anything. The ONLY exception is if you have explicitly verified — by reading the exact command/config — that this specific invocation has no side effects (no \`--fix\`/\`--write\`, no file/state/db mutation). If you cannot verify that, treat it as forbidden and note it in the plan instead.
547
+ - Do NOT run any other side-effecting \`bash\`: no commits, no \`git push\`, no installing/removing packages, no writing/moving/deleting files, no changing configs, no \`change_directory\`, no \`workflow\`.
548
+ - If you find yourself wanting to mutate something to make progress, that's a signal to write it into the plan instead and continue researching read-only.
549
+
550
+ Use good judgment: take the read-only action yourself rather than pushing avoidable confirmation prompts onto the user. Only the plan file is writable.
551
+
552
+ ## Plan File Info:
553
+ ${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
554
+ You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
555
+
556
+ ## Plan Workflow
557
+
558
+ ### Phase 1: Initial Understanding
559
+ Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
560
+
561
+ 1. Focus on understanding the user's request and the code associated with their request
562
+
563
+ 2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
564
+ - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
565
+ - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
566
+ - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
567
+ - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
568
+
569
+ 3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
570
+
571
+ ### Phase 2: Design
572
+ Goal: Design an implementation approach.
573
+
574
+ Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
575
+
576
+ You can launch up to 1 agent(s) in parallel.
577
+
578
+ **Guidelines:**
579
+ - **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
580
+ - **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
581
+
582
+ Examples of when to use multiple agents:
583
+ - The task touches multiple parts of the codebase
584
+ - It's a large refactor or architectural change
585
+ - There are many edge cases to consider
586
+ - You'd benefit from exploring different approaches
587
+
588
+ Example perspectives by task type:
589
+ - New feature: simplicity vs performance vs maintainability
590
+ - Bug fix: root cause vs workaround vs prevention
591
+ - Refactoring: minimal change vs clean architecture
592
+
593
+ In the agent prompt:
594
+ - Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
595
+ - Describe requirements and constraints
596
+ - Request a detailed implementation plan
597
+
598
+ ### Phase 3: Review
599
+ Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
600
+ 1. Read the critical files identified by agents to deepen your understanding
601
+ 2. Ensure that the plans align with the user's original request
602
+ 3. Use question tool to clarify any remaining questions with the user
603
+
604
+ ### Phase 4: Final Plan
605
+ Goal: Write your final plan to the plan file (the only file you can edit).
606
+ - Include only your recommended approach, not all alternatives
607
+ - Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
608
+ - Include the paths of critical files to be modified
609
+ - Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
610
+
611
+ ### Phase 5: Call plan_exit tool
612
+ At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
613
+ This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
614
+
615
+ **Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
616
+
617
+ NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
618
+ </system-reminder>`,
619
+ synthetic: true,
620
+ })
621
+ userMessage.parts.push(part)
622
+ return input.messages
623
+ })
624
+
625
+ const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
626
+ agent: Agent.Info
627
+ model: Provider.Model
628
+ session: Session.Info
629
+ tools?: Record<string, boolean>
630
+ processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
631
+ bypassAgentCheck: boolean
632
+ messages: MessageV2.WithParts[]
633
+ agentID?: string
634
+ task_id?: string
635
+ }) {
636
+ using _ = log.time("resolveTools")
637
+ const tools: Record<string, AITool> = {}
638
+ const run = yield* runner()
639
+ const promptOps = yield* ops()
640
+
641
+ // Per-tool runtime whitelist: when the LLM call is being made on behalf
642
+ // of a registered actor (subagent or peer), look up the actor row and,
643
+ // if `actor.tools` is an array, reject calls to tools not in the
644
+ // whitelist. `INHERIT` and a missing actor row both mean full access.
645
+ const whitelistFor = Effect.fn("SessionPrompt.whitelistFor")(function* () {
646
+ if (!input.agentID) return undefined
647
+ const actor = yield* actorRegistry.get(input.session.id, input.agentID)
648
+ if (!actor || !Array.isArray(actor.tools)) return undefined
649
+ return new Set(actor.tools)
650
+ })
651
+ const whitelist = yield* whitelistFor()
652
+ // Whether a permission ask must be non-interactive (fail clean, never hang):
653
+ // true for system-spawned actors (checkpoint-writer/dream/distill) AND any
654
+ // background actor such as compose workflow subagents (spawned as "general"
655
+ // + background:true). Scoped to THIS permission decision on purpose — not
656
+ // folded into the shared isSystemSpawned, which also gates memory
657
+ // instructions and checkpoint self-triggering for user background actors.
658
+ // Fall back to the agent-name check if the actor row is missing (race /
659
+ // unregistered) so a system actor can't slip through as interactive.
660
+ const askActor = input.agentID
661
+ ? yield* actorRegistry.get(input.session.id, input.agentID)
662
+ : undefined
663
+ const askNonInteractive = askActor
664
+ ? SYSTEM_SPAWNED_AGENT_TYPES.has(askActor.agent) || askActor.background
665
+ : SYSTEM_SPAWNED_AGENT_TYPES.has(input.agent.name)
666
+ const rejectionFor = (toolID: string) => ({
667
+ title: "Tool not permitted",
668
+ output: `The "${toolID}" tool is not in this actor's whitelist. Allowed tools: ${
669
+ whitelist ? [...whitelist].join(", ") : "(none)"
670
+ }.`,
671
+ metadata: { rejected: true, reason: "tool-whitelist" as const },
672
+ })
673
+
674
+ const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({
675
+ sessionID: input.session.id,
676
+ abort: options.abortSignal!,
677
+ messageID: input.processor.message.id,
678
+ callID: options.toolCallId,
679
+ extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
680
+ agent: input.agent.name,
681
+ actorID: input.agentID,
682
+ taskId: input.task_id,
683
+ messages: input.messages,
684
+ metadata: (val) =>
685
+ input.processor.updateToolCall(options.toolCallId, (match) => {
686
+ if (!["running", "pending"].includes(match.state.status)) return match
687
+ return {
688
+ ...match,
689
+ state: {
690
+ title: val.title,
691
+ metadata: val.metadata,
692
+ status: "running",
693
+ input: args,
694
+ time: { start: Date.now() },
695
+ },
696
+ }
697
+ }),
698
+ ask: (req) =>
699
+ permission
700
+ .ask(
701
+ {
702
+ ...req,
703
+ sessionID: input.session.id,
704
+ tool: { messageID: input.processor.message.id, callID: options.toolCallId },
705
+ ruleset: Agent.runtimePermission(input.agent, input.session.permission),
706
+ // System-spawned background agents (checkpoint-writer, dream, distill)
707
+ // AND any background actor (e.g. compose workflow subagents) have no
708
+ // human to answer a permission prompt — fail clean, don't hang.
709
+ interactive: !askNonInteractive,
710
+ },
711
+ options.abortSignal,
712
+ )
713
+ .pipe(Effect.orDie),
714
+ })
715
+
716
+ for (const item of yield* registry.tools({
717
+ modelID: ModelID.make(input.model.api.id),
718
+ providerID: input.model.providerID,
719
+ agent: input.agent,
720
+ })) {
721
+ const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
722
+ tools[item.id] = tool({
723
+ description: item.description,
724
+ inputSchema: jsonSchema(schema),
725
+ execute(args, options) {
726
+ return run.promise(
727
+ Effect.gen(function* () {
728
+ const startTs = Date.now()
729
+ const callID = options?.toolCallId ?? "?"
730
+ log.debug("tool execute start", {
731
+ tool: item.id,
732
+ callID,
733
+ sessionID: input.session.id,
734
+ })
735
+ const ctx = context(args, options)
736
+ if (whitelist && !whitelist.has(item.id)) {
737
+ const output = rejectionFor(item.id)
738
+ log.debug("tool execute rejected", {
739
+ tool: item.id,
740
+ callID,
741
+ durationMs: Date.now() - startTs,
742
+ })
743
+ yield* input.processor.completeToolCall(options.toolCallId, output)
744
+ return output
745
+ }
746
+ const beforeOutput: { args: any; cancel?: boolean; cancelReason?: string } = { args }
747
+ yield* plugin.trigger(
748
+ "tool.execute.before",
749
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
750
+ beforeOutput,
751
+ )
752
+ if (beforeOutput.cancel) {
753
+ const cancelOutput = {
754
+ title: "Cancelled",
755
+ output: beforeOutput.cancelReason || "Tool call cancelled by hook",
756
+ metadata: { cancelled: true },
757
+ }
758
+ yield* bus
759
+ .publish(Metrics.ToolCall, {
760
+ sessionID: ctx.sessionID,
761
+ tool_name: item.id,
762
+ input_bytes: Metrics.jsonByteLength(beforeOutput.args),
763
+ output_bytes: 0,
764
+ tool_call_id: options.toolCallId,
765
+ tool_call_status: "cancelled",
766
+ })
767
+ .pipe(Effect.ignore)
768
+ yield* input.processor.completeToolCall(options.toolCallId, cancelOutput)
769
+ return cancelOutput
770
+ }
771
+ const result = yield* item.execute(beforeOutput.args, ctx)
772
+ log.debug("tool execute done", {
773
+ tool: item.id,
774
+ callID,
775
+ durationMs: Date.now() - startTs,
776
+ ok: true,
777
+ })
778
+ const output = {
779
+ ...result,
780
+ attachments: result.attachments?.map((attachment) => ({
781
+ ...attachment,
782
+ id: PartID.ascending(),
783
+ sessionID: ctx.sessionID,
784
+ messageID: input.processor.message.id,
785
+ })),
786
+ }
787
+ yield* plugin.trigger(
788
+ "tool.execute.after",
789
+ { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args: beforeOutput.args },
790
+ output,
791
+ )
792
+ if (
793
+ (item.id === "write" || item.id === "edit") &&
794
+ beforeOutput.args?.file_path &&
795
+ isExtensionPath(beforeOutput.args.file_path)
796
+ ) {
797
+ yield* registry.reload().pipe(Effect.tapError((err) => Effect.sync(() => log.warn("extension reload failed", { error: err }))), Effect.ignore)
798
+ }
799
+ yield* bus
800
+ .publish(Metrics.ToolCall, {
801
+ sessionID: ctx.sessionID,
802
+ tool_name: item.id,
803
+ input_bytes: Metrics.jsonByteLength(beforeOutput.args),
804
+ output_bytes: Buffer.byteLength(output.output ?? "", "utf8"),
805
+ tool_call_id: options.toolCallId,
806
+ tool_call_status: "success",
807
+ })
808
+ .pipe(Effect.ignore)
809
+ if (options.abortSignal?.aborted) {
810
+ yield* input.processor.completeToolCall(options.toolCallId, output)
811
+ }
812
+ return output
813
+ }),
814
+ )
815
+ },
816
+ })
817
+ }
818
+
819
+ for (const [key, item] of Object.entries(yield* mcp.tools())) {
820
+ const execute = item.execute
821
+ if (!execute) continue
822
+
823
+ const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema))
824
+ const transformed = ProviderTransform.schema(input.model, schema)
825
+ item.inputSchema = jsonSchema(transformed)
826
+ item.execute = (args, opts) =>
827
+ run.promise(
828
+ Effect.gen(function* () {
829
+ const startTs = Date.now()
830
+ const callID = opts?.toolCallId ?? "?"
831
+ log.debug("tool execute start (mcp)", {
832
+ tool: key,
833
+ callID,
834
+ sessionID: input.session.id,
835
+ })
836
+ const ctx = context(args, opts)
837
+ if (whitelist && !whitelist.has(key)) {
838
+ const rejection = rejectionFor(key)
839
+ const output = {
840
+ title: rejection.title,
841
+ metadata: rejection.metadata,
842
+ output: rejection.output,
843
+ attachments: [],
844
+ content: [{ type: "text" as const, text: rejection.output }],
845
+ }
846
+ log.debug("tool execute rejected (mcp)", {
847
+ tool: key,
848
+ callID,
849
+ durationMs: Date.now() - startTs,
850
+ })
851
+ yield* input.processor.completeToolCall(opts.toolCallId, output)
852
+ return output
853
+ }
854
+ const mcpBeforeOutput: { args: any; cancel?: boolean; cancelReason?: string } = { args }
855
+ yield* plugin.trigger(
856
+ "tool.execute.before",
857
+ { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
858
+ mcpBeforeOutput,
859
+ )
860
+ if (mcpBeforeOutput.cancel) {
861
+ const cancelResult = {
862
+ content: [{ type: "text" as const, text: mcpBeforeOutput.cancelReason || "Tool call cancelled by hook" }],
863
+ }
864
+ yield* bus
865
+ .publish(Metrics.ToolCall, {
866
+ sessionID: ctx.sessionID,
867
+ tool_name: key,
868
+ input_bytes: Metrics.jsonByteLength(mcpBeforeOutput.args),
869
+ output_bytes: 0,
870
+ tool_call_id: opts.toolCallId,
871
+ tool_call_status: "cancelled",
872
+ })
873
+ .pipe(Effect.ignore)
874
+ return cancelResult
875
+ }
876
+ yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
877
+ const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
878
+ execute(mcpBeforeOutput.args, opts),
879
+ )
880
+ log.debug("tool execute done (mcp)", {
881
+ tool: key,
882
+ callID,
883
+ durationMs: Date.now() - startTs,
884
+ ok: true,
885
+ })
886
+ yield* plugin.trigger(
887
+ "tool.execute.after",
888
+ { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
889
+ result,
890
+ )
891
+
892
+ const textParts: string[] = []
893
+ yield* bus
894
+ .publish(Metrics.ToolCall, {
895
+ sessionID: ctx.sessionID,
896
+ tool_name: key,
897
+ input_bytes: Metrics.jsonByteLength(args),
898
+ output_bytes: Metrics.jsonByteLength(result.content ?? ""),
899
+ tool_call_id: opts.toolCallId,
900
+ tool_call_status: "success",
901
+ })
902
+ .pipe(Effect.ignore)
903
+ const attachments: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[] = []
904
+ for (const contentItem of result.content) {
905
+ if (contentItem.type === "text") textParts.push(contentItem.text)
906
+ else if (contentItem.type === "image") {
907
+ attachments.push({
908
+ type: "file",
909
+ mime: contentItem.mimeType,
910
+ url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
911
+ })
912
+ } else if (contentItem.type === "resource") {
913
+ const { resource } = contentItem
914
+ if (resource.text) textParts.push(resource.text)
915
+ if (resource.blob) {
916
+ attachments.push({
917
+ type: "file",
918
+ mime: resource.mimeType ?? "application/octet-stream",
919
+ url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
920
+ filename: resource.uri,
921
+ })
922
+ }
923
+ }
924
+ }
925
+
926
+ const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent)
927
+ const metadata = {
928
+ ...result.metadata,
929
+ truncated: truncated.truncated,
930
+ ...(truncated.truncated && { outputPath: truncated.outputPath }),
931
+ }
932
+
933
+ const output = {
934
+ title: "",
935
+ metadata,
936
+ output: truncated.content,
937
+ attachments: attachments.map((attachment) => ({
938
+ ...attachment,
939
+ id: PartID.ascending(),
940
+ sessionID: ctx.sessionID,
941
+ messageID: input.processor.message.id,
942
+ })),
943
+ content: result.content,
944
+ }
945
+ if (opts.abortSignal?.aborted) {
946
+ yield* input.processor.completeToolCall(opts.toolCallId, output)
947
+ }
948
+ return output
949
+ }),
950
+ )
951
+ tools[key] = item
952
+ }
953
+
954
+ return tools
955
+ })
956
+
957
+ const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
958
+ task: MessageV2.SubtaskPart
959
+ model: Provider.Model
960
+ lastUser: MessageV2.User
961
+ sessionID: SessionID
962
+ session: Session.Info
963
+ msgs: MessageV2.WithParts[]
964
+ }) {
965
+ const { task, model, lastUser, sessionID, session, msgs } = input
966
+ const ctx = yield* InstanceState.context
967
+ const promptOps = yield* ops()
968
+ const { actor: actorTool } = yield* registry.named()
969
+ const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
970
+ const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({
971
+ id: MessageID.ascending(),
972
+ role: "assistant",
973
+ parentID: lastUser.id,
974
+ sessionID,
975
+ agentID: lastUser.agentID,
976
+ mode: task.agent,
977
+ agent: task.agent,
978
+ variant: lastUser.model.variant,
979
+ path: { cwd: ctx.directory, root: ctx.worktree },
980
+ cost: 0,
981
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
982
+ modelID: taskModel.id,
983
+ providerID: taskModel.providerID,
984
+ time: { created: Date.now() },
985
+ })
986
+ const taskArgs = {
987
+ operation: {
988
+ action: "run" as const,
989
+ prompt: task.prompt,
990
+ description: task.description,
991
+ subagent_type: task.agent,
992
+ command: task.command,
993
+ },
994
+ }
995
+ let part: MessageV2.ToolPart = yield* sessions.updatePart({
996
+ id: PartID.ascending(),
997
+ messageID: assistantMessage.id,
998
+ sessionID: assistantMessage.sessionID,
999
+ type: "tool",
1000
+ callID: ulid(),
1001
+ tool: ActorTool.id,
1002
+ state: {
1003
+ status: "running",
1004
+ input: taskArgs,
1005
+ time: { start: Date.now() },
1006
+ },
1007
+ })
1008
+ yield* plugin.trigger(
1009
+ "tool.execute.before",
1010
+ { tool: ActorTool.id, sessionID, callID: part.id },
1011
+ { args: taskArgs },
1012
+ )
1013
+
1014
+ const taskAgent = yield* agents.get(task.agent)
1015
+ if (!taskAgent) {
1016
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
1017
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1018
+ const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` })
1019
+ yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
1020
+ throw error
1021
+ }
1022
+
1023
+ let error: Error | undefined
1024
+ const taskAbort = new AbortController()
1025
+ const result = yield* actorTool
1026
+ .execute(taskArgs, {
1027
+ agent: task.agent,
1028
+ messageID: assistantMessage.id,
1029
+ sessionID,
1030
+ abort: taskAbort.signal,
1031
+ callID: part.callID,
1032
+ extra: { bypassAgentCheck: true, promptOps },
1033
+ messages: msgs,
1034
+ metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
1035
+ Effect.gen(function* () {
1036
+ part = yield* sessions.updatePart({
1037
+ ...part,
1038
+ type: "tool",
1039
+ state: { ...part.state, ...val },
1040
+ } satisfies MessageV2.ToolPart)
1041
+ }),
1042
+ ask: (req: any) =>
1043
+ permission
1044
+ .ask({
1045
+ ...req,
1046
+ sessionID,
1047
+ ruleset: Agent.runtimePermission(taskAgent, session.permission),
1048
+ })
1049
+ .pipe(Effect.orDie),
1050
+ })
1051
+ .pipe(
1052
+ Effect.catchCause((cause) => {
1053
+ const defect = Cause.squash(cause)
1054
+ error = defect instanceof Error ? defect : new Error(String(defect))
1055
+ log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
1056
+ return Effect.void
1057
+ }),
1058
+ Effect.onInterrupt(() =>
1059
+ Effect.gen(function* () {
1060
+ taskAbort.abort()
1061
+ assistantMessage.finish = "tool-calls"
1062
+ assistantMessage.time.completed = Date.now()
1063
+ yield* sessions.updateMessage(assistantMessage)
1064
+ if (part.state.status === "running") {
1065
+ yield* sessions.updatePart({
1066
+ ...part,
1067
+ state: {
1068
+ status: "error",
1069
+ error: "Cancelled",
1070
+ time: { start: part.state.time.start, end: Date.now() },
1071
+ metadata: part.state.metadata,
1072
+ input: part.state.input,
1073
+ },
1074
+ } satisfies MessageV2.ToolPart)
1075
+ }
1076
+ }),
1077
+ ),
1078
+ )
1079
+
1080
+ const attachments = result?.attachments?.map((attachment) => ({
1081
+ ...attachment,
1082
+ id: PartID.ascending(),
1083
+ sessionID,
1084
+ messageID: assistantMessage.id,
1085
+ }))
1086
+
1087
+ yield* plugin.trigger(
1088
+ "tool.execute.after",
1089
+ { tool: ActorTool.id, sessionID, callID: part.id, args: taskArgs },
1090
+ result,
1091
+ )
1092
+
1093
+ assistantMessage.finish = "tool-calls"
1094
+ assistantMessage.time.completed = Date.now()
1095
+ yield* sessions.updateMessage(assistantMessage)
1096
+
1097
+ if (result && part.state.status === "running") {
1098
+ yield* sessions.updatePart({
1099
+ ...part,
1100
+ state: {
1101
+ status: "completed",
1102
+ input: part.state.input,
1103
+ title: result.title,
1104
+ metadata: result.metadata,
1105
+ output: result.output,
1106
+ attachments,
1107
+ time: { ...part.state.time, end: Date.now() },
1108
+ },
1109
+ } satisfies MessageV2.ToolPart)
1110
+ }
1111
+
1112
+ if (!result) {
1113
+ yield* sessions.updatePart({
1114
+ ...part,
1115
+ state: {
1116
+ status: "error",
1117
+ error: error ? `Tool execution failed: ${error.message}` : "Tool execution failed",
1118
+ time: {
1119
+ start: part.state.status === "running" ? part.state.time.start : Date.now(),
1120
+ end: Date.now(),
1121
+ },
1122
+ metadata: part.state.status === "pending" ? undefined : part.state.metadata,
1123
+ input: part.state.input,
1124
+ },
1125
+ } satisfies MessageV2.ToolPart)
1126
+ }
1127
+
1128
+ if (!task.command) return
1129
+
1130
+ const summaryUserMsg: MessageV2.User = {
1131
+ id: MessageID.ascending(),
1132
+ sessionID,
1133
+ role: "user",
1134
+ agentID: lastUser.agentID,
1135
+ time: { created: Date.now() },
1136
+ agent: lastUser.agent,
1137
+ model: lastUser.model,
1138
+ }
1139
+ yield* sessions.updateMessage(summaryUserMsg)
1140
+ yield* sessions.updatePart({
1141
+ id: PartID.ascending(),
1142
+ messageID: summaryUserMsg.id,
1143
+ sessionID,
1144
+ type: "text",
1145
+ text: "Summarize the actor tool output above and continue with your task.",
1146
+ synthetic: true,
1147
+ } satisfies MessageV2.TextPart)
1148
+ })
1149
+
1150
+ const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput) {
1151
+ const ctx = yield* InstanceState.context
1152
+ const run = yield* runner()
1153
+ const session = yield* sessions.get(input.sessionID)
1154
+ if (session.revert) {
1155
+ yield* revert.cleanup(session)
1156
+ }
1157
+ const agent = yield* agents.get(input.agent)
1158
+ if (!agent) {
1159
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
1160
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1161
+ const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` })
1162
+ yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
1163
+ throw error
1164
+ }
1165
+ const inputModel = input.modelRef
1166
+ ? yield* provider
1167
+ .resolveModelRef(input.modelRef)
1168
+ .pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
1169
+ : input.model
1170
+ const agentModel = agent.modelRef
1171
+ ? yield* provider
1172
+ .resolveModelRef(agent.modelRef)
1173
+ .pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
1174
+ : agent.model
1175
+ const model = inputModel ?? agentModel ?? (yield* lastModel(input.sessionID))
1176
+ const userMsg: MessageV2.User = {
1177
+ id: input.messageID ?? MessageID.ascending(),
1178
+ sessionID: input.sessionID,
1179
+ time: { created: Date.now() },
1180
+ role: "user",
1181
+ agent: input.agent,
1182
+ model: { providerID: model.providerID, modelID: model.modelID },
1183
+ }
1184
+ yield* sessions.updateMessage(userMsg)
1185
+ const userPart: MessageV2.Part = {
1186
+ type: "text",
1187
+ id: PartID.ascending(),
1188
+ messageID: userMsg.id,
1189
+ sessionID: input.sessionID,
1190
+ text: "The following tool was executed by the user",
1191
+ synthetic: true,
1192
+ }
1193
+ yield* sessions.updatePart(userPart)
1194
+
1195
+ const msg: MessageV2.Assistant = {
1196
+ id: MessageID.ascending(),
1197
+ sessionID: input.sessionID,
1198
+ parentID: userMsg.id,
1199
+ agentID: userMsg.agentID,
1200
+ mode: input.agent,
1201
+ agent: input.agent,
1202
+ cost: 0,
1203
+ path: { cwd: ctx.directory, root: ctx.worktree },
1204
+ time: { created: Date.now() },
1205
+ role: "assistant",
1206
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1207
+ modelID: model.modelID,
1208
+ providerID: model.providerID,
1209
+ }
1210
+ yield* sessions.updateMessage(msg)
1211
+ const part: MessageV2.ToolPart = {
1212
+ type: "tool",
1213
+ id: PartID.ascending(),
1214
+ messageID: msg.id,
1215
+ sessionID: input.sessionID,
1216
+ tool: "bash",
1217
+ callID: ulid(),
1218
+ state: {
1219
+ status: "running",
1220
+ time: { start: Date.now() },
1221
+ input: { command: input.command },
1222
+ },
1223
+ }
1224
+ yield* sessions.updatePart(part)
1225
+
1226
+ const sh = Shell.preferred()
1227
+ const shellName = (
1228
+ process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh)
1229
+ ).toLowerCase()
1230
+ const invocations: Record<string, { args: string[] }> = {
1231
+ nu: { args: ["-c", input.command] },
1232
+ fish: { args: ["-c", input.command] },
1233
+ zsh: {
1234
+ args: [
1235
+ "-l",
1236
+ "-c",
1237
+ `
1238
+ __oc_cwd=$PWD
1239
+ [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
1240
+ [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
1241
+ cd "$__oc_cwd"
1242
+ eval ${JSON.stringify(input.command)}
1243
+ `,
1244
+ ],
1245
+ },
1246
+ bash: {
1247
+ args: [
1248
+ "-l",
1249
+ "-c",
1250
+ `
1251
+ __oc_cwd=$PWD
1252
+ shopt -s expand_aliases
1253
+ [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
1254
+ cd "$__oc_cwd"
1255
+ eval ${JSON.stringify(input.command)}
1256
+ `,
1257
+ ],
1258
+ },
1259
+ cmd: { args: ["/c", `${Shell.CMD_UTF8_PREFIX}${input.command}`] },
1260
+ powershell: {
1261
+ args: ["-NoProfile", "-Command", `${Shell.POWERSHELL_UTF8_PREFIX}${input.command}`],
1262
+ },
1263
+ pwsh: {
1264
+ args: ["-NoProfile", "-Command", `${Shell.POWERSHELL_UTF8_PREFIX}${input.command}`],
1265
+ },
1266
+ "": { args: ["-c", input.command] },
1267
+ }
1268
+
1269
+ const args = (invocations[shellName] ?? invocations[""]).args
1270
+ const cwd = ctx.directory
1271
+ const shellEnv = yield* plugin.trigger(
1272
+ "shell.env",
1273
+ { cwd, sessionID: input.sessionID, callID: part.callID },
1274
+ { env: {} },
1275
+ )
1276
+
1277
+ const cmd = ChildProcess.make(sh, args, {
1278
+ cwd,
1279
+ extendEnv: true,
1280
+ env: {
1281
+ ...shellEnv.env,
1282
+ ...(process.platform === "win32" ? { PYTHONIOENCODING: "utf-8" } : {}),
1283
+ TERM: "dumb",
1284
+ },
1285
+ stdin: "ignore",
1286
+ forceKillAfter: "3 seconds",
1287
+ })
1288
+
1289
+ let output = ""
1290
+ let aborted = false
1291
+
1292
+ const finish = Effect.uninterruptible(
1293
+ Effect.gen(function* () {
1294
+ if (aborted) {
1295
+ output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
1296
+ }
1297
+ if (!msg.time.completed) {
1298
+ msg.time.completed = Date.now()
1299
+ yield* sessions.updateMessage(msg)
1300
+ }
1301
+ if (part.state.status === "running") {
1302
+ part.state = {
1303
+ status: "completed",
1304
+ time: { ...part.state.time, end: Date.now() },
1305
+ input: part.state.input,
1306
+ title: "",
1307
+ metadata: { output, description: "" },
1308
+ output,
1309
+ }
1310
+ yield* sessions.updatePart(part)
1311
+ }
1312
+ }),
1313
+ )
1314
+
1315
+ const exit = yield* Effect.gen(function* () {
1316
+ const handle = yield* spawner.spawn(cmd)
1317
+ yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
1318
+ Effect.sync(() => {
1319
+ output += chunk
1320
+ if (part.state.status === "running") {
1321
+ part.state.metadata = { output, description: "" }
1322
+ void run.fork(sessions.updatePart(part))
1323
+ }
1324
+ }),
1325
+ )
1326
+ yield* handle.exitCode
1327
+ }).pipe(
1328
+ Effect.scoped,
1329
+ Effect.onInterrupt(() =>
1330
+ Effect.sync(() => {
1331
+ aborted = true
1332
+ }),
1333
+ ),
1334
+ Effect.orDie,
1335
+ Effect.ensuring(finish),
1336
+ Effect.exit,
1337
+ )
1338
+
1339
+ if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
1340
+ return yield* Effect.failCause(exit.cause)
1341
+ }
1342
+
1343
+ return { info: msg, parts: [part] }
1344
+ })
1345
+
1346
+ const getModel = Effect.fn("SessionPrompt.getModel")(function* (
1347
+ providerID: ProviderID,
1348
+ modelID: ModelID,
1349
+ sessionID: SessionID,
1350
+ ) {
1351
+ const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
1352
+ if (Exit.isSuccess(exit)) return exit.value
1353
+ const err = Cause.squash(exit.cause)
1354
+ if (Provider.ModelNotFoundError.isInstance(err)) {
1355
+ const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : ""
1356
+ yield* bus.publish(Session.Event.Error, {
1357
+ sessionID,
1358
+ error: new NamedError.Unknown({
1359
+ message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`,
1360
+ }).toObject(),
1361
+ })
1362
+ }
1363
+ return yield* Effect.failCause(exit.cause)
1364
+ })
1365
+
1366
+ const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
1367
+ const match = yield* sessions.findMessage(
1368
+ sessionID,
1369
+ (m) => m.info.role === "user" && !!m.info.model,
1370
+ { agentID: "*" },
1371
+ )
1372
+ if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
1373
+ return yield* provider.defaultModel()
1374
+ })
1375
+
1376
+ const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
1377
+ const agentName = input.agent || (yield* agents.defaultAgent())
1378
+ const ag = yield* agents.get(agentName)
1379
+ if (!ag) {
1380
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
1381
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1382
+ const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
1383
+ yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
1384
+ throw error
1385
+ }
1386
+
1387
+ const inputModel = input.modelRef
1388
+ ? yield* provider
1389
+ .resolveModelRef(input.modelRef)
1390
+ .pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
1391
+ : input.model
1392
+ const agentModel = ag.modelRef
1393
+ ? yield* provider
1394
+ .resolveModelRef(ag.modelRef)
1395
+ .pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
1396
+ : ag.model
1397
+ const model = inputModel ?? agentModel ?? (yield* lastModel(input.sessionID))
1398
+ const same = agentModel && model.providerID === agentModel.providerID && model.modelID === agentModel.modelID
1399
+ const full =
1400
+ !input.variant && ag.variant && same
1401
+ ? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
1402
+ : undefined
1403
+ const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
1404
+
1405
+ const info: MessageV2.User = {
1406
+ id: input.messageID ?? MessageID.ascending(),
1407
+ role: "user",
1408
+ sessionID: input.sessionID,
1409
+ agentID: input.agentID,
1410
+ time: { created: Date.now() },
1411
+ tools: input.tools,
1412
+ agent: ag.name,
1413
+ model: {
1414
+ providerID: model.providerID,
1415
+ modelID: model.modelID,
1416
+ variant,
1417
+ },
1418
+ system: input.system,
1419
+ format: input.format,
1420
+ provenance: input.provenance,
1421
+ }
1422
+
1423
+ yield* Effect.addFinalizer(() => instruction.clear(info.id))
1424
+
1425
+ type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
1426
+ const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
1427
+ ...part,
1428
+ id: part.id ? PartID.make(part.id) : PartID.ascending(),
1429
+ })
1430
+
1431
+ const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<MessageV2.Part>[]> = Effect.fn(
1432
+ "SessionPrompt.resolveUserPart",
1433
+ )(function* (part) {
1434
+ if (part.type === "file") {
1435
+ if (part.source?.type === "resource") {
1436
+ const { clientName, uri } = part.source
1437
+ log.info("mcp resource", { clientName, uri, mime: part.mime })
1438
+ const pieces: Draft<MessageV2.Part>[] = [
1439
+ {
1440
+ messageID: info.id,
1441
+ sessionID: input.sessionID,
1442
+ type: "text",
1443
+ synthetic: true,
1444
+ text: `Reading MCP resource: ${part.filename} (${uri})`,
1445
+ },
1446
+ ]
1447
+ const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit)
1448
+ if (Exit.isSuccess(exit)) {
1449
+ const content = exit.value
1450
+ if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`)
1451
+ const items = Array.isArray(content.contents) ? content.contents : [content.contents]
1452
+ for (const c of items) {
1453
+ if ("text" in c && c.text) {
1454
+ pieces.push({
1455
+ messageID: info.id,
1456
+ sessionID: input.sessionID,
1457
+ type: "text",
1458
+ synthetic: true,
1459
+ text: c.text,
1460
+ })
1461
+ } else if ("blob" in c && c.blob) {
1462
+ const mime = "mimeType" in c ? c.mimeType : part.mime
1463
+ pieces.push({
1464
+ messageID: info.id,
1465
+ sessionID: input.sessionID,
1466
+ type: "text",
1467
+ synthetic: true,
1468
+ text: `[Binary content: ${mime}]`,
1469
+ })
1470
+ }
1471
+ }
1472
+ pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
1473
+ } else {
1474
+ const error = Cause.squash(exit.cause)
1475
+ log.error("failed to read MCP resource", { error, clientName, uri })
1476
+ const message = error instanceof Error ? error.message : String(error)
1477
+ pieces.push({
1478
+ messageID: info.id,
1479
+ sessionID: input.sessionID,
1480
+ type: "text",
1481
+ synthetic: true,
1482
+ text: `Failed to read MCP resource ${part.filename}: ${message}`,
1483
+ })
1484
+ }
1485
+ return pieces
1486
+ }
1487
+ const url = new URL(part.url)
1488
+ switch (url.protocol) {
1489
+ case "data:":
1490
+ if (part.mime === "text/plain") {
1491
+ return [
1492
+ {
1493
+ messageID: info.id,
1494
+ sessionID: input.sessionID,
1495
+ type: "text",
1496
+ synthetic: true,
1497
+ text: `Called the Read tool with the following input: ${JSON.stringify({ file_path: part.filename })}`,
1498
+ },
1499
+ {
1500
+ messageID: info.id,
1501
+ sessionID: input.sessionID,
1502
+ type: "text",
1503
+ synthetic: true,
1504
+ text: decodeDataUrl(part.url),
1505
+ },
1506
+ { ...part, messageID: info.id, sessionID: input.sessionID },
1507
+ ]
1508
+ }
1509
+ break
1510
+ case "file:": {
1511
+ log.info("file", { mime: part.mime })
1512
+ const filepath = fileURLToPath(part.url)
1513
+ if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory"
1514
+
1515
+ const { read } = yield* registry.named()
1516
+ const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
1517
+ const controller = new AbortController()
1518
+ return read
1519
+ .execute(args, {
1520
+ sessionID: input.sessionID,
1521
+ abort: controller.signal,
1522
+ agent: input.agent!,
1523
+ messageID: info.id,
1524
+ extra: { bypassCwdCheck: true, ...extra },
1525
+ messages: [],
1526
+ metadata: () => Effect.void,
1527
+ ask: () => Effect.void,
1528
+ })
1529
+ .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
1530
+ }
1531
+
1532
+ if (part.mime === "text/plain") {
1533
+ let offset: number | undefined
1534
+ let limit: number | undefined
1535
+ const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") }
1536
+ if (range.start != null) {
1537
+ const filePathURI = part.url.split("?")[0]
1538
+ let start = parseInt(range.start)
1539
+ let end = range.end ? parseInt(range.end) : undefined
1540
+ if (start === end) {
1541
+ const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([])))
1542
+ for (const symbol of symbols) {
1543
+ let r: LSP.Range | undefined
1544
+ if ("range" in symbol) r = symbol.range
1545
+ else if ("location" in symbol) r = symbol.location.range
1546
+ if (r?.start?.line && r?.start?.line === start) {
1547
+ start = r.start.line
1548
+ end = r?.end?.line ?? start
1549
+ break
1550
+ }
1551
+ }
1552
+ }
1553
+ offset = Math.max(start, 1)
1554
+ if (end) limit = end - (offset - 1)
1555
+ }
1556
+ const args = { file_path: filepath, offset, limit }
1557
+ const pieces: Draft<MessageV2.Part>[] = [
1558
+ {
1559
+ messageID: info.id,
1560
+ sessionID: input.sessionID,
1561
+ type: "text",
1562
+ synthetic: true,
1563
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
1564
+ },
1565
+ ]
1566
+ const exit = yield* provider.getModel(info.model.providerID, info.model.modelID).pipe(
1567
+ Effect.flatMap((mdl) => execRead(args, { model: mdl })),
1568
+ Effect.exit,
1569
+ )
1570
+ if (Exit.isSuccess(exit)) {
1571
+ const result = exit.value
1572
+ pieces.push({
1573
+ messageID: info.id,
1574
+ sessionID: input.sessionID,
1575
+ type: "text",
1576
+ synthetic: true,
1577
+ text: result.output,
1578
+ })
1579
+ if (result.attachments?.length) {
1580
+ pieces.push(
1581
+ ...result.attachments.map((a) => ({
1582
+ ...a,
1583
+ synthetic: true,
1584
+ filename: a.filename ?? part.filename,
1585
+ messageID: info.id,
1586
+ sessionID: input.sessionID,
1587
+ })),
1588
+ )
1589
+ } else {
1590
+ pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
1591
+ }
1592
+ } else {
1593
+ const error = Cause.squash(exit.cause)
1594
+ log.error("failed to read file", { error })
1595
+ const message = error instanceof Error ? error.message : String(error)
1596
+ yield* bus.publish(Session.Event.Error, {
1597
+ sessionID: input.sessionID,
1598
+ error: new NamedError.Unknown({ message }).toObject(),
1599
+ })
1600
+ pieces.push({
1601
+ messageID: info.id,
1602
+ sessionID: input.sessionID,
1603
+ type: "text",
1604
+ synthetic: true,
1605
+ text: `Read tool failed to read ${filepath} with the following error: ${message}`,
1606
+ })
1607
+ }
1608
+ return pieces
1609
+ }
1610
+
1611
+ if (part.mime === "application/x-directory") {
1612
+ const args = { file_path: filepath }
1613
+ const exit = yield* execRead(args).pipe(Effect.exit)
1614
+ if (Exit.isFailure(exit)) {
1615
+ const error = Cause.squash(exit.cause)
1616
+ log.error("failed to read directory", { error })
1617
+ const message = error instanceof Error ? error.message : String(error)
1618
+ yield* bus.publish(Session.Event.Error, {
1619
+ sessionID: input.sessionID,
1620
+ error: new NamedError.Unknown({ message }).toObject(),
1621
+ })
1622
+ return [
1623
+ {
1624
+ messageID: info.id,
1625
+ sessionID: input.sessionID,
1626
+ type: "text",
1627
+ synthetic: true,
1628
+ text: `Read tool failed to read ${filepath} with the following error: ${message}`,
1629
+ },
1630
+ ]
1631
+ }
1632
+ return [
1633
+ {
1634
+ messageID: info.id,
1635
+ sessionID: input.sessionID,
1636
+ type: "text",
1637
+ synthetic: true,
1638
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
1639
+ },
1640
+ {
1641
+ messageID: info.id,
1642
+ sessionID: input.sessionID,
1643
+ type: "text",
1644
+ synthetic: true,
1645
+ text: exit.value.output,
1646
+ },
1647
+ { ...part, messageID: info.id, sessionID: input.sessionID },
1648
+ ]
1649
+ }
1650
+
1651
+ return [
1652
+ {
1653
+ messageID: info.id,
1654
+ sessionID: input.sessionID,
1655
+ type: "text",
1656
+ synthetic: true,
1657
+ text: `Called the Read tool with the following input: {"file_path":"${filepath}"}`,
1658
+ },
1659
+ {
1660
+ id: part.id,
1661
+ messageID: info.id,
1662
+ sessionID: input.sessionID,
1663
+ type: "file",
1664
+ url:
1665
+ `data:${part.mime};base64,` +
1666
+ Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
1667
+ mime: part.mime,
1668
+ filename: part.filename!,
1669
+ source: part.source,
1670
+ },
1671
+ ]
1672
+ }
1673
+ }
1674
+ }
1675
+
1676
+ if (part.type === "agent") {
1677
+ const perm = Permission.evaluate("task", part.name, ag.permission)
1678
+ const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
1679
+ return [
1680
+ { ...part, messageID: info.id, sessionID: input.sessionID },
1681
+ {
1682
+ messageID: info.id,
1683
+ sessionID: input.sessionID,
1684
+ type: "text",
1685
+ synthetic: true,
1686
+ text:
1687
+ " Use the above message and context to generate a prompt and call the actor tool with subagent: " +
1688
+ part.name +
1689
+ hint,
1690
+ },
1691
+ ]
1692
+ }
1693
+
1694
+ return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
1695
+ })
1696
+
1697
+ const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
1698
+ Effect.map((x) => x.flat().map(assign)),
1699
+ )
1700
+
1701
+ yield* plugin.trigger(
1702
+ "chat.message",
1703
+ {
1704
+ sessionID: input.sessionID,
1705
+ agent: input.agent,
1706
+ model: input.model,
1707
+ messageID: input.messageID,
1708
+ variant: input.variant,
1709
+ },
1710
+ { message: info, parts },
1711
+ )
1712
+
1713
+ const parsed = MessageV2.Info.safeParse(info)
1714
+ if (!parsed.success) {
1715
+ log.error("invalid user message before save", {
1716
+ sessionID: input.sessionID,
1717
+ messageID: info.id,
1718
+ agent: info.agent,
1719
+ model: info.model,
1720
+ issues: parsed.error.issues,
1721
+ })
1722
+ }
1723
+ parts.forEach((part, index) => {
1724
+ const p = MessageV2.Part.safeParse(part)
1725
+ if (p.success) return
1726
+ log.error("invalid user part before save", {
1727
+ sessionID: input.sessionID,
1728
+ messageID: info.id,
1729
+ partID: part.id,
1730
+ partType: part.type,
1731
+ index,
1732
+ issues: p.error.issues,
1733
+ part,
1734
+ })
1735
+ })
1736
+
1737
+ yield* sessions.updateMessage(info)
1738
+ for (const part of parts) yield* sessions.updatePart(part)
1739
+
1740
+ return { info, parts }
1741
+ }, Effect.scoped)
1742
+
1743
+ const sweepOrphanAssistants = Effect.fn("SessionPrompt.sweepOrphanAssistants")(function* (sessionID: SessionID) {
1744
+ const msgs = yield* sessions.messages({ sessionID, agentID: "*" })
1745
+ const now = Date.now()
1746
+ // 1 hour — must exceed Task 1's chunkMs (300s) plus Task 2's
1747
+ // PERSISTENT_RETRY worst-case backoff (10 attempts × 5 min cap =
1748
+ // 50 min) so a still-active in-flight request is never falsely
1749
+ // swept while its retry chain is making progress.
1750
+ const ORPHAN_AGE_MS = 3_600_000
1751
+ for (const m of msgs) {
1752
+ if (m.info.role !== "assistant") continue
1753
+ if (m.info.time?.completed) continue
1754
+ const created = m.info.time?.created ?? 0
1755
+ if (now - created < ORPHAN_AGE_MS) continue
1756
+ m.info.time = { ...m.info.time, completed: now }
1757
+ m.info.error =
1758
+ m.info.error ??
1759
+ new MessageV2.AbortedError({
1760
+ message: "Abandoned: previous request interrupted before completion",
1761
+ }).toObject()
1762
+ yield* sessions.updateMessage(m.info).pipe(
1763
+ Effect.catchCause((cause) =>
1764
+ elog.warn("orphan-update-failed", {
1765
+ sessionID,
1766
+ messageID: m.info.id,
1767
+ cause,
1768
+ }),
1769
+ ),
1770
+ )
1771
+ yield* elog.info("orphan-assistant-cleared", {
1772
+ sessionID,
1773
+ messageID: m.info.id,
1774
+ })
1775
+ }
1776
+ })
1777
+
1778
+ const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.prompt")(
1779
+ function* (input: PromptInput) {
1780
+ const session = yield* sessions.get(input.sessionID)
1781
+ if (input.source !== "spawn" && input.source !== "hook") {
1782
+ yield* revert.cleanup(session)
1783
+ yield* sweepOrphanAssistants(input.sessionID)
1784
+ }
1785
+ const message = yield* createUserMessage(input)
1786
+ yield* sessions.touch(input.sessionID)
1787
+
1788
+ const permissions: Permission.Ruleset = []
1789
+ for (const [t, enabled] of Object.entries(input.tools ?? {})) {
1790
+ permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
1791
+ }
1792
+ if (permissions.length > 0) {
1793
+ session.permission = permissions
1794
+ yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
1795
+ }
1796
+
1797
+ if (input.noReply === true) return message
1798
+ return yield* loop({ sessionID: input.sessionID, agentID: input.agentID ?? "main", task_id: input.task_id })
1799
+ },
1800
+ )
1801
+
1802
+ const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID, agentID?: string) {
1803
+ if (agentID !== undefined) {
1804
+ // Agent-scoped: return THIS agent's newest message (assistant preferred).
1805
+ // Critical for concurrent same-session subagents — a session-wide lookup
1806
+ // collapses concurrent actors' return values onto whichever finished last.
1807
+ // messages() yields oldest-first/newest-last, so findLast picks the newest
1808
+ // assistant and the last element is the newest message overall.
1809
+ const own = yield* sessions.messages({ sessionID, agentID })
1810
+ const lastAsst = own.findLast((m) => m.info.role === "assistant")
1811
+ if (lastAsst) return lastAsst
1812
+ if (own.length > 0) return own[own.length - 1]
1813
+ // fall through to session-wide if this agent has no messages yet
1814
+ }
1815
+ const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user", { agentID: "*" })
1816
+ if (Option.isSome(match)) return match.value
1817
+ const msgs = yield* sessions.messages({ sessionID, limit: 1, agentID: "*" })
1818
+ if (msgs.length > 0) return msgs[0]
1819
+ throw new Error("Impossible")
1820
+ })
1821
+
1822
+ const runLoop: (sessionID: SessionID, agentID?: string, task_id?: string) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
1823
+ "SessionPrompt.run",
1824
+ )(
1825
+ function* (sessionID: SessionID, agentID?: string, task_id?: string) {
1826
+ const ctx = yield* InstanceState.context
1827
+ const slog = elog.with({ sessionID })
1828
+ let structured: unknown | undefined
1829
+ let step = 0
1830
+ const session = yield* sessions.get(sessionID)
1831
+ let lastFinishedForPrune: MessageV2.Assistant | undefined
1832
+ let lastModelForPrune: Provider.Model | undefined
1833
+ let outputLengthContinuations = 0
1834
+ // Shared local counter for "model finished but produced nothing usable"
1835
+ // (think-only / empty). T04's generic-invalid retries reuse this same
1836
+ // counter — do not add a second one. Local to runLoop so a fresh user
1837
+ // turn resets it (no cross-message pollution), same as outputLengthContinuations.
1838
+ let invalidContinuations = 0
1839
+ // structured-output 专用 retry:上限来自 lastUser.format.retryCount(默认 2),
1840
+ // 与 invalidContinuations(generic invalid)分离,互不污染。局部于 runLoop,
1841
+ // 新一轮用户 turn 自动归零。
1842
+ let structuredRetries = 0
1843
+ // Bounded retries for text-form tool calls (model wrote a tool call as
1844
+ // prose text instead of a structured tool_use). Local to runLoop so each
1845
+ // fresh user turn starts clean.
1846
+ let textToolCallRetries = 0
1847
+ const resolvedAgentID = agentID ?? "main"
1848
+ // Tracks plugin-driven cancellation (session.pre OR any session.userQuery.pre)
1849
+ // so session.post reports outcome="cancelled" instead of "error".
1850
+ let cancelled = false
1851
+ let cancelReason: string | undefined
1852
+
1853
+ // Fires session.post exactly once via Effect.onExit on the body below.
1854
+ // Without this wrapper any yielded failure inside the while loop (provider
1855
+ // error, network error, thrown defect) would skip the hook entirely.
1856
+ //
1857
+ // Trajectory parity: uses MessageV2.filterCompactedEffect with the session's
1858
+ // contextFrom / contextWatermark so compaction boundaries trim history to
1859
+ // what the agent actually saw, and child-session parent prefixes are
1860
+ // included — matching session.userQuery.post semantics.
1861
+ const firePostSession = (exit: Exit.Exit<MessageV2.WithParts, unknown>) =>
1862
+ Effect.gen(function* () {
1863
+ const sliceMsgs = yield* MessageV2.filterCompactedEffect(sessionID, {
1864
+ contextFrom: session.contextFrom,
1865
+ contextWatermark: session.contextWatermark,
1866
+ agentID: resolvedAgentID,
1867
+ }).pipe(Effect.catch(() => Effect.succeed([] as MessageV2.WithParts[])))
1868
+ const lastSlice = sliceMsgs.findLast((m) => m.info.role === "assistant")
1869
+ const finalAsst =
1870
+ lastSlice && lastSlice.info.role === "assistant" ? lastSlice.info : undefined
1871
+ const finalParts = lastSlice?.parts ?? []
1872
+ const failed = Exit.isFailure(exit)
1873
+ const finalIsError = !!finalAsst?.error
1874
+ const outcome: "completed" | "error" | "cancelled" = cancelled
1875
+ ? "cancelled"
1876
+ : failed || finalIsError
1877
+ ? "error"
1878
+ : "completed"
1879
+ const error = cancelled
1880
+ ? cancelReason
1881
+ : failed
1882
+ ? Cause.pretty(exit.cause)
1883
+ : finalAsst
1884
+ ? sessionErrorText(finalAsst.error)
1885
+ : undefined
1886
+ yield* plugin.trigger(
1887
+ "session.post",
1888
+ {
1889
+ sessionID,
1890
+ agentID: resolvedAgentID,
1891
+ task_id,
1892
+ outcome,
1893
+ error,
1894
+ finalText: finalAsst ? assistantFinalText(finalAsst, finalParts) : undefined,
1895
+ assistantMessageID: finalAsst?.id,
1896
+ trajectory: serializeTrajectoryMessages(sliceMsgs),
1897
+ },
1898
+ {},
1899
+ )
1900
+ }).pipe(Effect.ignore)
1901
+
1902
+ return yield* Effect.gen(function* () {
1903
+ const preSession = { cancel: undefined as boolean | undefined, cancelReason: undefined as string | undefined }
1904
+ yield* plugin.trigger(
1905
+ "session.pre",
1906
+ { sessionID, agentID: resolvedAgentID, task_id },
1907
+ preSession,
1908
+ )
1909
+ if (preSession.cancel) {
1910
+ cancelled = true
1911
+ cancelReason = preSession.cancelReason
1912
+ return yield* Effect.fail(
1913
+ new NamedError.Unknown({
1914
+ message: preSession.cancelReason ?? "Session cancelled by plugin",
1915
+ }),
1916
+ )
1917
+ }
1918
+ const agentMetrics = { tokens_in: 0, tokens_out: 0, files_changed: 0 }
1919
+ const trajectoryForStep = (currentMsgs: MessageV2.WithParts[], assistant: MessageV2.Assistant) =>
1920
+ serializeTrajectoryMessages(
1921
+ withAssistantParts(currentMsgs, assistant, MessageV2.parts(assistant.id)),
1922
+ )
1923
+
1924
+ const publishAgentRequest = (phase: string, taskType: string) =>
1925
+ bus
1926
+ .publish(Metrics.AgentRequest, {
1927
+ sessionID,
1928
+ phase,
1929
+ task_type: taskType,
1930
+ surface: Flag.OPENDASH_CLIENT,
1931
+ total_tokens_in: agentMetrics.tokens_in,
1932
+ total_tokens_out: agentMetrics.tokens_out,
1933
+ files_changed: agentMetrics.files_changed,
1934
+ validation_status: "skipped",
1935
+ })
1936
+ .pipe(Effect.ignore)
1937
+ // Trim freed space but `lastFinished.tokens` still reflects pre-trim state.
1938
+ // Skip one overflow check so the model can respond on the trimmed context;
1939
+ // its new assistant message will carry accurate tokens for the next check.
1940
+ let skipOverflowCheck = false
1941
+
1942
+ const textLoopBuffer: string[] = []
1943
+ let textLoopRecoveryAttempts = 0
1944
+ let textNgramRecoveryAttempts = 0
1945
+
1946
+ // Contract (T05): on finish="length", inject a continuation nudge ONLY for
1947
+ // plain text. If any non-providerExecuted client tool part exists we bail
1948
+ // (return false) and let classify route the normal tool-observation re-loop.
1949
+ // This guarantees "no output-length continuation when a tool is involved" —
1950
+ // it does NOT guarantee a stream-time-truncated tool never executed, since
1951
+ // the AI SDK runs tools mid-stream before the finish reason is known.
1952
+ const autoContinueOutputLength = Effect.fn("SessionPrompt.autoContinueOutputLength")(function* (input: {
1953
+ lastUser: MessageV2.User
1954
+ assistant: MessageV2.Assistant
1955
+ }) {
1956
+ if (input.assistant.finish !== "length" || input.assistant.error || input.assistant.summary) return false
1957
+ if (
1958
+ MessageV2.parts(input.assistant.id).some((part) => part.type === "tool" && !part.metadata?.providerExecuted)
1959
+ ) {
1960
+ return false
1961
+ }
1962
+ if (outputLengthContinuations >= OUTPUT_LENGTH_CONTINUATION_LIMIT) {
1963
+ input.assistant.error = new MessageV2.OutputLengthError({}).toObject()
1964
+ yield* sessions.updateMessage(input.assistant)
1965
+ yield* bus.publish(Session.Event.Error, {
1966
+ sessionID: input.assistant.sessionID,
1967
+ error: input.assistant.error,
1968
+ })
1969
+ return false
1970
+ }
1971
+
1972
+ outputLengthContinuations++
1973
+ yield* slog.info("auto-continuing output length", { attempt: outputLengthContinuations })
1974
+ const msg = yield* sessions.updateMessage({
1975
+ id: MessageID.ascending(),
1976
+ role: "user" as const,
1977
+ sessionID: input.lastUser.sessionID,
1978
+ agentID: input.lastUser.agentID,
1979
+ agent: input.lastUser.agent,
1980
+ model: input.lastUser.model,
1981
+ tools: input.lastUser.tools,
1982
+ format: input.lastUser.format,
1983
+ time: { created: Date.now() },
1984
+ })
1985
+ yield* sessions.updatePart({
1986
+ id: PartID.ascending(),
1987
+ messageID: msg.id,
1988
+ sessionID: msg.sessionID,
1989
+ type: "text",
1990
+ synthetic: true,
1991
+ text: [
1992
+ "<system-reminder>",
1993
+ "The previous assistant response hit the model output token limit before completing.",
1994
+ "Continue the same task from the exact point where it stopped.",
1995
+ "Do not restart, recap, or repeat prior reasoning. Keep reasoning concise, prefer concrete tool calls or final output, and only stop when the user's task is complete or genuinely blocked.",
1996
+ "</system-reminder>",
1997
+ ].join("\n"),
1998
+ } satisfies MessageV2.TextPart)
1999
+ return true
2000
+ })
2001
+
2002
+ // Task stop-condition gate (main agent only). Before honoring a stop,
2003
+ // list non-terminal tasks in the session: if any remain, inject a
2004
+ // nudge as a synthetic user turn and re-enter (return true) so the
2005
+ // model closes them with `task done` / `task abandon`. ReAct cap +
2006
+ // counter mirror the goal gate; cap-exceeded allows stop with a
2007
+ // warn log (no reportedStatus on main). owner=undefined picks up
2008
+ // tasks orphaned by subagent gates that hit their own cap. Runs
2009
+ // BEFORE goalGate because task state is cheaper to settle and a
2010
+ // pending-task board pollutes any goal verdict.
2011
+ const taskGate = Effect.fn("SessionPrompt.taskGate")(function* (lastUser: MessageV2.User) {
2012
+ if ((agentID ?? "main") !== "main") return false
2013
+ // If the main agent has the `task` tool stripped (Permission.disabled),
2014
+ // a nudge to call `task done` is unsatisfiable and would re-loop to
2015
+ // cap. Skip the gate entirely. Mirrors the canWrite skip in
2016
+ // actor/spawn.ts (Permission.disabled(["write"], ...) check on
2017
+ // forkAgentInfo). Per-session resolution means this checks the
2018
+ // agent's static permission only (good enough for v1; session-
2019
+ // level overrides re-enabling task on a denied agent are
2020
+ // pathological and out of scope).
2021
+ const mainAgent = yield* agents.get("main").pipe(Effect.orElseSucceed(() => undefined))
2022
+ if (mainAgent && Permission.disabled(["task"], mainAgent.permission).has("task")) return false
2023
+ // Per-message `tools` is the second tool-strip layer (llm.ts:720
2024
+ // `input.user.tools?.[k] !== false` filter), separate from
2025
+ // Permission.disabled. A slash command pinning a narrow toolset for
2026
+ // its turn can drop `task` even when permission allows it; nudging
2027
+ // is then unsatisfiable. Same skip rationale, narrower window.
2028
+ if (lastUser.tools?.["task"] === false) return false
2029
+
2030
+ const count = yield* taskGateState.get(sessionID)
2031
+ // runLoop is annotated `R = never`; TaskGate.decide raises a
2032
+ // TaskRegistry.Service requirement that we close locally with the
2033
+ // layer-resolved binding so it doesn't leak into runLoop's R-set.
2034
+ const decision = yield* TaskGate.decide({
2035
+ session_id: sessionID,
2036
+ owner: undefined,
2037
+ reactCount: count,
2038
+ maxReact: MAX_TASK_GATE_MAIN_REACT,
2039
+ mode: "main",
2040
+ }).pipe(Effect.provideService(TaskRegistry.Service, taskRegistry))
2041
+ if (!decision.needReentry) {
2042
+ if (decision.capExceeded) {
2043
+ yield* slog.warn("task gate hit cap; allowing stop", {
2044
+ sessionID,
2045
+ incompleteTasks: decision.incompleteTasks,
2046
+ })
2047
+ }
2048
+ yield* taskGateState.clear(sessionID)
2049
+ return false
2050
+ }
2051
+ yield* taskGateState.bump(sessionID)
2052
+ const reentry = yield* sessions.updateMessage({
2053
+ id: MessageID.ascending(),
2054
+ role: "user" as const,
2055
+ sessionID,
2056
+ agentID: lastUser.agentID,
2057
+ agent: lastUser.agent,
2058
+ model: lastUser.model,
2059
+ tools: lastUser.tools,
2060
+ format: lastUser.format,
2061
+ time: { created: Date.now() },
2062
+ })
2063
+ yield* sessions.updatePart({
2064
+ id: PartID.ascending(),
2065
+ messageID: reentry.id,
2066
+ sessionID,
2067
+ type: "text",
2068
+ synthetic: true,
2069
+ text: decision.reentryText,
2070
+ } satisfies MessageV2.TextPart)
2071
+ return true
2072
+ })
2073
+
2074
+ // Goal stop-condition gate (main agent only). Before honoring a stop,
2075
+ // an independent judge model reads the transcript and decides whether
2076
+ // the active goal is satisfied. Not satisfied → inject the judge's
2077
+ // reason as a synthetic user turn and signal the caller to keep working
2078
+ // (return true). This is the main-loop analogue of actor.preStop ReAct
2079
+ // re-entry, which only fires for spawned actors. fail-open on any judge
2080
+ // error so a flaky judge can never trap the user.
2081
+ const goalGate = Effect.fn("SessionPrompt.goalGate")(function* (lastUser: MessageV2.User) {
2082
+ if ((agentID ?? "main") !== "main") return false
2083
+ const active = yield* goal.get(sessionID)
2084
+ if (!active) return false
2085
+
2086
+ const transcriptMsgs = yield* MessageV2.filterCompactedEffect(sessionID, {
2087
+ contextFrom: session.contextFrom,
2088
+ contextWatermark: session.contextWatermark,
2089
+ agentID: "main",
2090
+ })
2091
+ // Anchor the verdict to the assistant turn the judge just evaluated, so
2092
+ // the TUI can render a per-turn marker the user can trace back to.
2093
+ const judgedMessageID = transcriptMsgs.findLast((m) => m.info.role === "assistant")?.info.id
2094
+ const verdict = yield* goal
2095
+ .evaluate({
2096
+ condition: active.condition,
2097
+ msgs: transcriptMsgs,
2098
+ model: lastUser.model,
2099
+ })
2100
+ .pipe(
2101
+ Effect.catch((err) =>
2102
+ Effect.gen(function* () {
2103
+ yield* slog.warn("goal judge failed; allowing stop", { error: String(err) })
2104
+ return { ok: true, reason: "judge error", judgeFailed: true } as Goal.Verdict & {
2105
+ judgeFailed: true
2106
+ }
2107
+ }),
2108
+ ),
2109
+ )
2110
+
2111
+ if (verdict.ok || verdict.impossible) {
2112
+ yield* slog.info("goal satisfied; allowing stop", {
2113
+ sessionID,
2114
+ impossible: verdict.impossible === true,
2115
+ })
2116
+ // Publish the final verdict (goal cleared) so the TUI can render the
2117
+ // ✓/⊘ result line before the indicator disappears. goal.clear also
2118
+ // publishes goal:undefined, but the TUI keeps lastVerdict sticky.
2119
+ yield* bus.publish(Goal.Event.Updated, {
2120
+ sessionID,
2121
+ goal: undefined,
2122
+ lastVerdict: {
2123
+ ...verdict,
2124
+ attempt: active.react,
2125
+ messageID: judgedMessageID,
2126
+ error: "judgeFailed" in verdict ? true : undefined,
2127
+ },
2128
+ })
2129
+ yield* goal.clear(sessionID)
2130
+ return false
2131
+ }
2132
+
2133
+ const count = yield* goal.bumpReact(sessionID)
2134
+ if (count > MAX_GOAL_REACT) {
2135
+ yield* slog.warn("goal hit MAX_GOAL_REACT cap; allowing stop", {
2136
+ sessionID,
2137
+ condition: active.condition,
2138
+ count,
2139
+ })
2140
+ yield* bus.publish(Goal.Event.Updated, {
2141
+ sessionID,
2142
+ goal: undefined,
2143
+ lastVerdict: { ...verdict, attempt: count, messageID: judgedMessageID },
2144
+ })
2145
+ yield* goal.clear(sessionID)
2146
+ return false
2147
+ }
2148
+
2149
+ yield* slog.info("goal not satisfied; re-entering", { sessionID, attempt: count })
2150
+ yield* bus.publish(Goal.Event.Updated, {
2151
+ sessionID,
2152
+ goal: { condition: active.condition },
2153
+ lastVerdict: { ...verdict, attempt: count, messageID: judgedMessageID },
2154
+ })
2155
+ const reentry = yield* sessions.updateMessage({
2156
+ id: MessageID.ascending(),
2157
+ role: "user" as const,
2158
+ sessionID,
2159
+ agentID: lastUser.agentID,
2160
+ agent: lastUser.agent,
2161
+ model: lastUser.model,
2162
+ tools: lastUser.tools,
2163
+ format: lastUser.format,
2164
+ time: { created: Date.now() },
2165
+ })
2166
+ yield* sessions.updatePart({
2167
+ id: PartID.ascending(),
2168
+ messageID: reentry.id,
2169
+ sessionID,
2170
+ type: "text",
2171
+ synthetic: true,
2172
+ text: [
2173
+ "<system-reminder>",
2174
+ `Your goal is not yet satisfied: "${active.condition}".`,
2175
+ "A judge reviewed the transcript and reported what is still missing:",
2176
+ verdict.reason,
2177
+ "Keep working toward the goal. Do not stop until it is genuinely met or impossible.",
2178
+ "</system-reminder>",
2179
+ ].join("\n"),
2180
+ } satisfies MessageV2.TextPart)
2181
+ return true
2182
+ })
2183
+
2184
+ // think-only (reasoning only) / empty (nothing at all) steps finish with
2185
+ // a non-tool stop but carry no usable answer. Without intervention the loop
2186
+ // breaks and hands the user an assistant with no final text. Nudge the model
2187
+ // to produce a final answer or call a real tool; give up (write a terminal
2188
+ // error) once the shared counter is exhausted so we never loop forever.
2189
+ const autoContinueInvalidOutput = Effect.fn("SessionPrompt.autoContinueInvalidOutput")(function* (input: {
2190
+ lastUser: MessageV2.User
2191
+ assistant: MessageV2.Assistant
2192
+ reason: string
2193
+ }) {
2194
+ if (input.assistant.error || input.assistant.summary || input.assistant.structured !== undefined) return false
2195
+ if (invalidContinuations >= INVALID_OUTPUT_CONTINUATION_LIMIT) {
2196
+ input.assistant.error = new MessageV2.InvalidOutputError({ message: input.reason }).toObject()
2197
+ yield* sessions.updateMessage(input.assistant)
2198
+ yield* bus.publish(Session.Event.Error, {
2199
+ sessionID: input.assistant.sessionID,
2200
+ error: input.assistant.error,
2201
+ })
2202
+ return false
2203
+ }
2204
+
2205
+ invalidContinuations++
2206
+ yield* slog.info("auto-continuing invalid output", { attempt: invalidContinuations, reason: input.reason })
2207
+ const msg = yield* sessions.updateMessage({
2208
+ id: MessageID.ascending(),
2209
+ role: "user" as const,
2210
+ sessionID: input.lastUser.sessionID,
2211
+ agentID: input.lastUser.agentID,
2212
+ agent: input.lastUser.agent,
2213
+ model: input.lastUser.model,
2214
+ tools: input.lastUser.tools,
2215
+ format: input.lastUser.format,
2216
+ time: { created: Date.now() },
2217
+ })
2218
+ yield* sessions.updatePart({
2219
+ id: PartID.ascending(),
2220
+ messageID: msg.id,
2221
+ sessionID: msg.sessionID,
2222
+ type: "text",
2223
+ synthetic: true,
2224
+ text: [
2225
+ "<system-reminder>",
2226
+ "Your previous response contained no usable answer (it had only reasoning, or was empty).",
2227
+ "Provide a final answer to the user now, or call a valid tool to make progress on the task.",
2228
+ "Do not respond with only reasoning/thinking.",
2229
+ "</system-reminder>",
2230
+ ].join("\n"),
2231
+ } satisfies MessageV2.TextPart)
2232
+ return true
2233
+ })
2234
+
2235
+ // Text-form tool call recovery. The model serialized a tool call as prose
2236
+ // text instead of a structured tool_use (a degraded state under large
2237
+ // context). The bad assistant turn is DISCARDED from history by setting
2238
+ // assistant.error (toModelMessages skips a message whose info.error is
2239
+ // set, message-v2.ts), so it can neither strand the conversation on an
2240
+ // assistant turn (provider prefill rejection) nor poison later context.
2241
+ // We then retry the request (caller does `continue`, no new message). On
2242
+ // exhaustion the error stays terminal. Returns true ⇒ continue; false ⇒ break.
2243
+ const autoRetryTextToolCall = Effect.fn("SessionPrompt.autoRetryTextToolCall")(function* (input: {
2244
+ lastUser: MessageV2.User
2245
+ assistant: MessageV2.Assistant
2246
+ }) {
2247
+ // Already discarded on a prior pass — let classify fall through to
2248
+ // `failed` instead of re-detecting and burning another retry.
2249
+ if (input.assistant.error) return false
2250
+ // Discard the bad turn from request history: toModelMessages skips a
2251
+ // message whose info.error is set, so it can neither strand the
2252
+ // conversation on an assistant turn nor poison later context.
2253
+ input.assistant.error = new MessageV2.TextToolCallError({
2254
+ message: "Model emitted a tool call as text instead of a structured tool call.",
2255
+ }).toObject()
2256
+ yield* sessions.updateMessage(input.assistant)
2257
+ if (textToolCallRetries >= TEXT_TOOL_CALL_RETRY_LIMIT) {
2258
+ yield* bus.publish(Session.Event.Error, {
2259
+ sessionID: input.assistant.sessionID,
2260
+ error: input.assistant.error,
2261
+ })
2262
+ return false
2263
+ }
2264
+ textToolCallRetries++
2265
+ yield* slog.info("retrying text-form tool call", { attempt: textToolCallRetries })
2266
+ // Append a synthetic user turn so the discarded assistant becomes stale
2267
+ // (classify staleness guard) AND the loop reaches generation — mirrors
2268
+ // autoRetryStructuredOutput. Without this the loop re-enters, re-detects
2269
+ // the same turn, and burns retries with zero model calls.
2270
+ const msg = yield* sessions.updateMessage({
2271
+ id: MessageID.ascending(),
2272
+ role: "user" as const,
2273
+ sessionID: input.lastUser.sessionID,
2274
+ agentID: input.lastUser.agentID,
2275
+ agent: input.lastUser.agent,
2276
+ model: input.lastUser.model,
2277
+ tools: input.lastUser.tools,
2278
+ format: input.lastUser.format,
2279
+ time: { created: Date.now() },
2280
+ })
2281
+ yield* sessions.updatePart({
2282
+ id: PartID.ascending(),
2283
+ messageID: msg.id,
2284
+ sessionID: msg.sessionID,
2285
+ type: "text",
2286
+ synthetic: true,
2287
+ text: [
2288
+ "<system-reminder>",
2289
+ "Your previous response wrote a tool call as plain text instead of invoking the tool.",
2290
+ "Re-issue it through the real tool channel — emit a structured tool call, not text.",
2291
+ "Do not paste the tool call as text again.",
2292
+ "</system-reminder>",
2293
+ ].join("\n"),
2294
+ } satisfies MessageV2.TextPart)
2295
+ return true
2296
+ })
2297
+
2298
+ // json_schema mode but the model never produced structured output (plain
2299
+ // text stop, empty, think-only, or any other non-tool terminal). Retry up
2300
+ // to lastUser.format.retryCount with a repair nudge; on exhaustion write a
2301
+ // StructuredOutputError carrying the *real* retry count. Separate from
2302
+ // invalidContinuations: structured retries are bounded by the per-request
2303
+ // retryCount, not the generic invalid-output limit.
2304
+ const autoRetryStructuredOutput = Effect.fn("SessionPrompt.autoRetryStructuredOutput")(function* (input: {
2305
+ lastUser: MessageV2.User
2306
+ assistant: MessageV2.Assistant
2307
+ }) {
2308
+ if (input.assistant.error || input.assistant.summary || input.assistant.structured !== undefined) return false
2309
+ const limit = input.lastUser.format?.type === "json_schema" ? input.lastUser.format.retryCount : 0
2310
+ if (structuredRetries >= limit) {
2311
+ input.assistant.error = new MessageV2.StructuredOutputError({
2312
+ message: "Model did not produce structured output",
2313
+ retries: structuredRetries,
2314
+ }).toObject()
2315
+ yield* sessions.updateMessage(input.assistant)
2316
+ yield* bus.publish(Session.Event.Error, {
2317
+ sessionID: input.assistant.sessionID,
2318
+ error: input.assistant.error,
2319
+ })
2320
+ return false
2321
+ }
2322
+
2323
+ structuredRetries++
2324
+ yield* slog.info("retrying structured output", { attempt: structuredRetries })
2325
+ const msg = yield* sessions.updateMessage({
2326
+ id: MessageID.ascending(),
2327
+ role: "user" as const,
2328
+ sessionID: input.lastUser.sessionID,
2329
+ agentID: input.lastUser.agentID,
2330
+ agent: input.lastUser.agent,
2331
+ model: input.lastUser.model,
2332
+ tools: input.lastUser.tools,
2333
+ // Must carry format so the next iteration re-registers the StructuredOutput tool.
2334
+ format: input.lastUser.format,
2335
+ time: { created: Date.now() },
2336
+ })
2337
+ yield* sessions.updatePart({
2338
+ id: PartID.ascending(),
2339
+ messageID: msg.id,
2340
+ sessionID: msg.sessionID,
2341
+ type: "text",
2342
+ synthetic: true,
2343
+ text: [
2344
+ "<system-reminder>",
2345
+ "Your previous response did not produce valid structured output via the StructuredOutput tool",
2346
+ "(it was plain text, empty, or only reasoning).",
2347
+ "You MUST call the StructuredOutput tool now, passing JSON that matches the requested schema.",
2348
+ "Do not reply with plain text and do not respond with only reasoning/thinking.",
2349
+ "</system-reminder>",
2350
+ ].join("\n"),
2351
+ } satisfies MessageV2.TextPart)
2352
+ return true
2353
+ })
2354
+
2355
+ // Sliding-window n-gram repetition recovery. Symmetric across main and
2356
+ // fork branches: 1st hit injects REMIND, 2nd hit injects REPLAN, 3rd
2357
+ // hit (>= TEXT_NGRAM_MAX_RECOVERY) writes an error and signals break.
2358
+ const handleTextRepeat = Effect.fn("SessionPrompt.handleTextRepeat")(function* (input: {
2359
+ lastUser: MessageV2.User
2360
+ }) {
2361
+ if (textNgramRecoveryAttempts >= TEXT_NGRAM_MAX_RECOVERY) {
2362
+ yield* slog.info("text n-gram: max recovery exceeded, terminating")
2363
+ yield* bus.publish(Session.Event.Error, {
2364
+ sessionID,
2365
+ error: new NamedError.Unknown({
2366
+ message: `Text repetition detected: repeated n-grams after ${TEXT_NGRAM_MAX_RECOVERY} recovery attempts. Session terminated.`,
2367
+ }).toObject(),
2368
+ })
2369
+ return false
2370
+ }
2371
+ const recoveryText =
2372
+ textNgramRecoveryAttempts === 0 ? TEXT_NGRAM_RECOVERY_REMIND : TEXT_NGRAM_RECOVERY_REPLAN
2373
+ const reentry = yield* sessions.updateMessage({
2374
+ id: MessageID.ascending(),
2375
+ role: "user" as const,
2376
+ sessionID,
2377
+ agentID: input.lastUser.agentID,
2378
+ agent: input.lastUser.agent,
2379
+ model: input.lastUser.model,
2380
+ tools: input.lastUser.tools,
2381
+ format: input.lastUser.format,
2382
+ time: { created: Date.now() },
2383
+ })
2384
+ yield* sessions.updatePart({
2385
+ id: PartID.ascending(),
2386
+ messageID: reentry.id,
2387
+ sessionID,
2388
+ type: "text",
2389
+ synthetic: true,
2390
+ text: recoveryText,
2391
+ } satisfies MessageV2.TextPart)
2392
+ textNgramRecoveryAttempts++
2393
+ yield* slog.info("text n-gram: recovery injected", { attempt: textNgramRecoveryAttempts })
2394
+ return true
2395
+ })
2396
+
2397
+ // content-filter is terminal on first occurrence: re-sending the same
2398
+ // turn would just get filtered again, so there is no nudge / counter.
2399
+ // Write a user-visible error (rendered via the session.error toast) and
2400
+ // let the caller break.
2401
+ const writeContentFilterError = Effect.fn("SessionPrompt.writeContentFilterError")(function* (input: {
2402
+ assistant: MessageV2.Assistant
2403
+ }) {
2404
+ if (input.assistant.error) return
2405
+ input.assistant.error = new MessageV2.ContentFilterError({
2406
+ message: "The response was withheld by the model provider's content safety filter.",
2407
+ }).toObject()
2408
+ yield* sessions.updateMessage(input.assistant)
2409
+ yield* bus.publish(Session.Event.Error, {
2410
+ sessionID: input.assistant.sessionID,
2411
+ error: input.assistant.error,
2412
+ })
2413
+ })
2414
+
2415
+ // A `failed` classification (model "error" finish, or an error already set
2416
+ // by the stream-error path) is terminal. If the step already carries an
2417
+ // error (e.g. APIError written when the stream threw, processor.ts:581),
2418
+ // keep it; otherwise write a ModelError so the loop never breaks silently
2419
+ // without a user-visible failure.
2420
+ const writeModelError = Effect.fn("SessionPrompt.writeModelError")(function* (input: {
2421
+ assistant: MessageV2.Assistant
2422
+ reason: string
2423
+ }) {
2424
+ if (input.assistant.error) return
2425
+ input.assistant.error = new MessageV2.ModelError({ message: input.reason }).toObject()
2426
+ yield* sessions.updateMessage(input.assistant)
2427
+ yield* bus.publish(Session.Event.Error, {
2428
+ sessionID: input.assistant.sessionID,
2429
+ error: input.assistant.error,
2430
+ })
2431
+ })
2432
+
2433
+ while (true) {
2434
+ // F55: only main agent sets session status to busy; subagent runners
2435
+ // must not touch session-level status (Runner.onBusy is Effect.void
2436
+ // for non-main actors per F47).
2437
+ if (!agentID || agentID === "main") yield* status.set(sessionID, { type: "busy" })
2438
+ yield* inbox.drain(sessionID, agentID ?? "main").pipe(Effect.ignore)
2439
+ yield* slog.info("loop", { step })
2440
+
2441
+ // F37: filter by agentID so subagent slices stay isolated from the
2442
+ // main agent's slice within the same session. Without this, an actor
2443
+ // (explore/general/etc) spawned via opendash's shared-sessionID
2444
+ // design would see the parent's full conversation here and drift
2445
+ // off-task. agentID === "main" => main agent slice (agent_id = 'main'
2446
+ // in DB), agentID === "explore-1" => only explore-1's slice.
2447
+ let msgs = yield* MessageV2.filterCompactedEffect(sessionID, {
2448
+ contextFrom: session.contextFrom,
2449
+ contextWatermark: session.contextWatermark,
2450
+ agentID: agentID ?? "main",
2451
+ })
2452
+
2453
+ let lastUser: MessageV2.User | undefined
2454
+ let lastAssistant: MessageV2.Assistant | undefined
2455
+ let lastFinished: MessageV2.Assistant | undefined
2456
+ let tasks: MessageV2.SubtaskPart[] = []
2457
+ for (let i = msgs.length - 1; i >= 0; i--) {
2458
+ const msg = msgs[i]
2459
+ if (!lastUser && msg.info.role === "user") lastUser = msg.info
2460
+ if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
2461
+ if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
2462
+ if (lastUser && lastFinished) break
2463
+ const task = msg.parts.filter((part): part is MessageV2.SubtaskPart => part.type === "subtask")
2464
+ if (task && !lastFinished) tasks.push(...task)
2465
+ }
2466
+
2467
+ if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
2468
+
2469
+ // Per-user-message active recall reminder. Once the session has
2470
+ // any memory artifacts (memory dir populated OR tasks recorded),
2471
+ // append a brief recall protocol so the agent's reflex to query
2472
+ // memory.search / task / actor / Read stays warm across many
2473
+ // post-rebuild turns. Cost ~120 tokens per turn, conditional on
2474
+ // hasMemoryOrTasks.
2475
+ const lastUserMsgForRecall = msgs.findLast((m) => m.info.role === "user")
2476
+ if (lastUserMsgForRecall) {
2477
+ const hasRecallTarget = yield* checkpoint
2478
+ .hasMemoryOrTasks(sessionID)
2479
+ .pipe(Effect.catch(() => Effect.succeed(false)))
2480
+ if (hasRecallTarget) {
2481
+ const sessMemDir = path.join(Global.Path.data, "memory", "sessions", sessionID)
2482
+ const hints = recallHintLines((yield* config.get()).tool)
2483
+ lastUserMsgForRecall.parts.push({
2484
+ id: PartID.ascending(),
2485
+ messageID: lastUserMsgForRecall.info.id,
2486
+ sessionID,
2487
+ type: "text" as const,
2488
+ synthetic: true,
2489
+ text: [
2490
+ "<system-reminder>",
2491
+ `This session has memory at ${sessMemDir}/. Recall content`,
2492
+ "not in your context with:",
2493
+ hints[0],
2494
+ `- Read(file_path="${sessMemDir}/...")`,
2495
+ hints[1],
2496
+ hints[2],
2497
+ "",
2498
+ "Don't ask the user about something memory may already record.",
2499
+ "</system-reminder>",
2500
+ ].join("\n"),
2501
+ })
2502
+ }
2503
+ }
2504
+
2505
+ const lastAssistantMsg = msgs.findLast(
2506
+ (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
2507
+ )
2508
+ // Some providers return "stop" even when the assistant message contains tool calls.
2509
+ // Keep the loop running so tool results can be sent back to the model.
2510
+ // Skip provider-executed tool parts — those were fully handled within the
2511
+ // provider's stream (e.g. DWS Agent Platform) and don't need a re-loop.
2512
+ const hasToolCalls =
2513
+ lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false
2514
+
2515
+ if (
2516
+ lastAssistant?.finish === "length" &&
2517
+ !hasToolCalls &&
2518
+ lastUser.id < lastAssistant.id &&
2519
+ (yield* autoContinueOutputLength({ lastUser, assistant: lastAssistant }))
2520
+ ) {
2521
+ continue
2522
+ }
2523
+
2524
+ if (lastAssistant) {
2525
+ const classification = classifyAssistantStep({
2526
+ phase: "existing-assistant",
2527
+ lastUser,
2528
+ assistant: lastAssistant,
2529
+ parts: lastAssistantMsg?.parts ?? [],
2530
+ })
2531
+ if (classification.type === "filtered") {
2532
+ yield* writeContentFilterError({ assistant: lastAssistant })
2533
+ yield* slog.info("exiting loop", { classification: classification.type })
2534
+ break
2535
+ }
2536
+ if (classification.type === "failed") {
2537
+ yield* writeModelError({ assistant: lastAssistant, reason: classification.reason })
2538
+ yield* slog.info("exiting loop", { classification: classification.type, reason: classification.reason })
2539
+ break
2540
+ }
2541
+ if (classification.type === "text-tool-call") {
2542
+ if (yield* autoRetryTextToolCall({ lastUser, assistant: lastAssistant })) continue
2543
+ yield* slog.info("exiting loop", { classification: classification.type })
2544
+ break
2545
+ }
2546
+ if (classification.type === "think-only" || classification.type === "invalid") {
2547
+ const reason = classification.type === "invalid" ? classification.reason : "think-only"
2548
+ if (yield* autoContinueInvalidOutput({ lastUser, assistant: lastAssistant, reason })) continue
2549
+ yield* slog.info("exiting loop", { classification: classification.type })
2550
+ break
2551
+ }
2552
+ if (classification.type === "final" && classification.degraded)
2553
+ yield* slog.warn("degraded final on abnormal finish", { finish: lastAssistant.finish })
2554
+ if (classification.type !== "continue") {
2555
+ if (yield* taskGate(lastUser)) continue
2556
+ if (yield* goalGate(lastUser)) continue
2557
+ yield* slog.info("exiting loop", { classification: classification.type })
2558
+ break
2559
+ }
2560
+ }
2561
+
2562
+ step++
2563
+ if (step === 1)
2564
+ yield* title({
2565
+ session,
2566
+ modelID: lastUser.model.modelID,
2567
+ providerID: lastUser.model.providerID,
2568
+ history: msgs,
2569
+ }).pipe(Effect.ignore, Effect.forkIn(scope))
2570
+
2571
+ if (step === 1 && !session.parentID) {
2572
+ const cfg = yield* config.get()
2573
+ const dreamTrigger = yield* shouldAutoDream(cfg).pipe(Effect.catch(() => Effect.succeed(false)))
2574
+ const distillTrigger = yield* shouldAutoDistill(cfg).pipe(Effect.catch(() => Effect.succeed(false)))
2575
+ const mdl = { providerID: lastUser.model.providerID, modelID: lastUser.model.modelID }
2576
+ // AppRuntime is imported dynamically (not at module top level) to keep
2577
+ // the session layer out of the app-runtime module-init cycle
2578
+ // (prompt → app-runtime → AppLayer → SessionPrompt). Only loaded when a
2579
+ // trigger actually fires. Detached fire-and-forget on the full runtime.
2580
+ if (dreamTrigger || distillTrigger) {
2581
+ const { AppRuntime } = yield* Effect.promise(() => import("@/effect/app-runtime"))
2582
+ if (dreamTrigger) {
2583
+ AppRuntime.runPromise(
2584
+ Session.Service.use((svc) =>
2585
+ Effect.gen(function* () {
2586
+ const s = yield* svc.create({ title: AUTO_DREAM_TITLE })
2587
+ const sp = yield* Service
2588
+ yield* sp.prompt({ sessionID: s.id, agent: "dream", model: mdl, parts: [{ type: "text", text: DREAM_TASK }] })
2589
+ }),
2590
+ ),
2591
+ ).catch((err) => log.error("auto-dream prompt failed", { error: String(err) }))
2592
+ }
2593
+ if (distillTrigger) {
2594
+ AppRuntime.runPromise(
2595
+ Session.Service.use((svc) =>
2596
+ Effect.gen(function* () {
2597
+ const s = yield* svc.create({ title: AUTO_DISTILL_TITLE })
2598
+ const sp = yield* Service
2599
+ yield* sp.prompt({ sessionID: s.id, agent: "distill", model: mdl, parts: [{ type: "text", text: DISTILL_TASK }] })
2600
+ }),
2601
+ ),
2602
+ ).catch((err) => log.error("auto-distill prompt failed", { error: String(err) }))
2603
+ }
2604
+ }
2605
+ }
2606
+
2607
+ const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
2608
+ lastModelForPrune = model
2609
+ lastFinishedForPrune = lastFinished
2610
+ const task = tasks.pop()
2611
+
2612
+ if (task?.type === "subtask") {
2613
+ yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
2614
+ continue
2615
+ }
2616
+
2617
+ // Detect compaction boundary: if the last user message has a compaction
2618
+ // part, route to compact.process() instead of the normal LLM flow.
2619
+ const lastUserMsgForCompaction = msgs.findLast((m) => m.info.role === "user")
2620
+ if (lastUserMsgForCompaction?.parts.some((p) => p.type === "compaction")) {
2621
+ const compactionPart = lastUserMsgForCompaction.parts.find(
2622
+ (p): p is MessageV2.CompactionPart => p.type === "compaction",
2623
+ )
2624
+ const allMsgs = yield* sessions.messages({ sessionID, agentID: lastUser.agentID ?? "main" })
2625
+ const result = yield* compaction.process({
2626
+ parentID: lastUser.id,
2627
+ messages: allMsgs,
2628
+ sessionID,
2629
+ auto: compactionPart?.auto ?? false,
2630
+ overflow: compactionPart?.overflow,
2631
+ agentID: lastUser.agentID,
2632
+ })
2633
+ if (result === "stop") break
2634
+ continue
2635
+ }
2636
+
2637
+ // Memory flush nudge at high context pressure
2638
+ if (lastFinished && lastFinished.summary !== true && model) {
2639
+ const cfg = yield* config.get()
2640
+ const pressure = pressureLevel({ cfg, tokens: lastFinished.tokens, model })
2641
+ if (pressure >= 2) {
2642
+ // Inject nudge as a synthetic text part on the last user message
2643
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
2644
+ if (
2645
+ lastUserMsg &&
2646
+ !lastUserMsg.parts.some((p) => p.type === "text" && p.text?.includes("Context is filling up"))
2647
+ ) {
2648
+ lastUserMsg.parts.push({
2649
+ id: PartID.ascending(),
2650
+ messageID: lastUserMsg.info.id,
2651
+ sessionID,
2652
+ type: "text",
2653
+ synthetic: true,
2654
+ text: [
2655
+ "<system-reminder>",
2656
+ `Context is filling up (${pressure >= 3 ? ">85%" : ">70%"}).`,
2657
+ "If you have important learnings or decisions from this session,",
2658
+ "consider writing them to memory now before context may be reset.",
2659
+ "</system-reminder>",
2660
+ ].join("\n"),
2661
+ })
2662
+ }
2663
+ }
2664
+ }
2665
+
2666
+ // Repeated-step nudge: if the last REPEATED_STEP_THRESHOLD finished
2667
+ // assistant steps made an identical tool call, the model is likely
2668
+ // stuck looping. Inject a reminder on the last user message asking it
2669
+ // to change approach. Mirrors the memory-flush nudge above (synthetic
2670
+ // text part, deduped per build).
2671
+ if (lastFinished) {
2672
+ const recentSignatures: string[] = []
2673
+ for (let i = msgs.length - 1; i >= 0 && recentSignatures.length < REPEATED_STEP_THRESHOLD; i--) {
2674
+ const m = msgs[i]
2675
+ if (m.info.role !== "assistant" || !m.info.finish) continue
2676
+ const sig = stepSignature(m.parts)
2677
+ if (sig === undefined) break
2678
+ recentSignatures.push(sig)
2679
+ }
2680
+ const repeating =
2681
+ recentSignatures.length === REPEATED_STEP_THRESHOLD &&
2682
+ recentSignatures.every((sig) => sig === recentSignatures[0])
2683
+ if (repeating) {
2684
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
2685
+ if (
2686
+ lastUserMsg &&
2687
+ !lastUserMsg.parts.some(
2688
+ (p) => p.type === "text" && p.text?.includes("repeating the same action"),
2689
+ )
2690
+ ) {
2691
+ lastUserMsg.parts.push({
2692
+ id: PartID.ascending(),
2693
+ messageID: lastUserMsg.info.id,
2694
+ sessionID,
2695
+ type: "text",
2696
+ synthetic: true,
2697
+ text: [
2698
+ "<system-reminder>",
2699
+ `Your last ${REPEATED_STEP_THRESHOLD} steps have been identical — you appear to be`,
2700
+ "repeating the same action without making progress. Stop and reconsider:",
2701
+ "the current approach is not working. Try a different strategy, use a",
2702
+ "different tool, or if you are blocked, explain the blocker to the user",
2703
+ "instead of repeating the same step again.",
2704
+ "</system-reminder>",
2705
+ ].join("\n"),
2706
+ })
2707
+ }
2708
+ }
2709
+ }
2710
+
2711
+ // Resolve the agent for this iteration once. Both the management
2712
+ // hooks below (fireCheckpoints, overflow handler) and the existing
2713
+ // agent-not-found check later in the iteration reuse this binding.
2714
+ // Bounded computation agents (native + hidden — currently title,
2715
+ // summary, checkpoint-writer) are exempt from context management;
2716
+ // see docs/superpowers/specs/2026-04-28-bounded-computation-agents-design.md
2717
+ const agent = yield* agents.get(lastUser.agent)
2718
+ const isBoundedComputation =
2719
+ agent?.native === true && agent?.hidden === true
2720
+
2721
+ // Fire background checkpoint writers for any newly-crossed thresholds
2722
+ // based on the latest completed assistant message's tokens. Must run
2723
+ // BEFORE the overflow/maxThreshold check below so maxCrossed flag is
2724
+ // set in time to trigger rebuild on this same iteration.
2725
+ if (!skipOverflowCheck && !isBoundedComputation && lastFinished && lastFinished.tokens) {
2726
+ const fireOps = yield* ops()
2727
+ yield* prune
2728
+ .fireCheckpoints({
2729
+ sessionID,
2730
+ model,
2731
+ tokens: lastFinished.tokens,
2732
+ promptOps: fireOps,
2733
+ agentID: lastUser.agentID,
2734
+ })
2735
+ .pipe(Effect.ignore)
2736
+ }
2737
+
2738
+ if (
2739
+ !skipOverflowCheck &&
2740
+ !isBoundedComputation &&
2741
+ lastFinished &&
2742
+ lastFinished.summary !== true &&
2743
+ (overflowCheck({ cfg: yield* config.get(), tokens: lastFinished.tokens, model }) ||
2744
+ (yield* prune.maxThresholdCrossed(sessionID)))
2745
+ ) {
2746
+ // Subagent overflow → per-actor compaction (lossy LLM summarization
2747
+ // scoped to the actor's (sessionID, agent_id) slice). Subagents
2748
+ // don't have checkpoints, so checkpoint+discard does not apply.
2749
+ // Gate must exclude agentID="main" — F49+F50 made main carry
2750
+ // agentID="main", so a bare `if (lastUser.agentID)` would route
2751
+ // main to this subagent path and skip the checkpoint rebuild
2752
+ // below. See checkpoint.ts:715 for the matching gate.
2753
+ if (lastUser.agentID && lastUser.agentID !== "main") {
2754
+ yield* compaction
2755
+ .create({
2756
+ sessionID,
2757
+ agent: lastUser.agent,
2758
+ model: { providerID: model.providerID, modelID: model.id },
2759
+ auto: true,
2760
+ agentID: lastUser.agentID,
2761
+ })
2762
+ .pipe(Effect.ignore)
2763
+ // After inserting the boundary, the actor's filterCompactedEffect
2764
+ // slice begins at the boundary marker — context is freed for the
2765
+ // next iteration's stream. Skip the next overflow check so the
2766
+ // model can respond on the trimmed context.
2767
+ skipOverflowCheck = true
2768
+ continue
2769
+ }
2770
+
2771
+ // Main-agent overflow: insert a checkpoint boundary marker (never
2772
+ // deletes DB messages) so the next iteration rebuilds from the
2773
+ // freshest checkpoint. Fall back to compaction only when no boundary
2774
+ // can be produced.
2775
+ const hasCP = yield* checkpoint.hasCheckpoint(sessionID).pipe(Effect.catch(() => Effect.succeed(false)))
2776
+ if (hasCP) {
2777
+ // Wait for any running writer so the freshest checkpoint is available
2778
+ yield* checkpoint.waitForWriter(sessionID).pipe(Effect.ignore)
2779
+
2780
+ const boundary = yield* checkpoint
2781
+ .lastBoundary(sessionID)
2782
+ .pipe(Effect.catch(() => Effect.succeed(undefined)))
2783
+ const boundaryMsg = boundary ? msgs.find((m) => m.info.id === boundary) : undefined
2784
+ const inserted = boundary
2785
+ ? yield* checkpoint
2786
+ .insertRebuildBoundary({
2787
+ sessionID,
2788
+ boundary,
2789
+ lastMessageInfo: computeLastMessageInfo(msgs.map((m) => m.info)),
2790
+ agentID: lastUser.agentID,
2791
+ agent: lastUser.agent,
2792
+ model: { providerID: model.providerID, modelID: model.id },
2793
+ boundaryCreatedAt: boundaryMsg?.info.time.created,
2794
+ })
2795
+ .pipe(Effect.catch(() => Effect.succeed(false)))
2796
+ : false
2797
+
2798
+ if (inserted) {
2799
+ yield* prune.resetThresholds(sessionID)
2800
+ skipOverflowCheck = true
2801
+ continue
2802
+ }
2803
+ }
2804
+
2805
+ // F39: no checkpoint — fall back to compaction (LLM-driven lossy summary).
2806
+ // Better than mechanical trim: preserves semantic content via summary.
2807
+ yield* compaction
2808
+ .create({
2809
+ sessionID,
2810
+ agent: lastUser.agent,
2811
+ model: { providerID: model.providerID, modelID: model.id },
2812
+ auto: true,
2813
+ agentID: lastUser.agentID,
2814
+ })
2815
+ .pipe(Effect.ignore)
2816
+ skipOverflowCheck = true
2817
+ continue
2818
+ }
2819
+ skipOverflowCheck = false
2820
+
2821
+ // `agent` resolved at iteration start; reuse here for the
2822
+ // agent-not-found user-visible error.
2823
+ if (!agent) {
2824
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
2825
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
2826
+ const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
2827
+ yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
2828
+ throw error
2829
+ }
2830
+ const maxSteps = agent.steps ?? Infinity
2831
+ const isLastStep = step >= maxSteps
2832
+ msgs = yield* insertReminders({ messages: msgs, agent, session })
2833
+
2834
+ const msg: MessageV2.Assistant = {
2835
+ id: MessageID.ascending(),
2836
+ parentID: lastUser.id,
2837
+ role: "assistant",
2838
+ agentID: lastUser.agentID,
2839
+ mode: agent.name,
2840
+ agent: agent.name,
2841
+ variant: lastUser.model.variant,
2842
+ path: { cwd: ctx.directory, root: ctx.worktree },
2843
+ cost: 0,
2844
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
2845
+ modelID: model.id,
2846
+ providerID: model.providerID,
2847
+ time: { created: Date.now() },
2848
+ sessionID,
2849
+ }
2850
+ yield* sessions.updateMessage(msg)
2851
+ const handle = yield* processor.create({
2852
+ assistantMessage: msg,
2853
+ sessionID,
2854
+ model,
2855
+ agentMetrics,
2856
+ })
2857
+
2858
+ const outcome: "break" | "continue" = yield* Effect.gen(function* () {
2859
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
2860
+ const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
2861
+
2862
+ const tools = yield* resolveTools({
2863
+ agent,
2864
+ session,
2865
+ model,
2866
+ tools: lastUser.tools,
2867
+ processor: handle,
2868
+ bypassAgentCheck,
2869
+ messages: msgs,
2870
+ agentID: lastUser.agentID,
2871
+ task_id,
2872
+ })
2873
+
2874
+ if (lastUser.format?.type === "json_schema") {
2875
+ tools["StructuredOutput"] = createStructuredOutputTool({
2876
+ schema: lastUser.format.schema,
2877
+ onSuccess(output) {
2878
+ structured = output
2879
+ },
2880
+ })
2881
+ }
2882
+
2883
+ if (step === 1)
2884
+ yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope))
2885
+
2886
+ if (step > 1 && lastFinished) {
2887
+ for (const m of msgs) {
2888
+ if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
2889
+ for (const p of m.parts) {
2890
+ if (p.type !== "text" || p.ignored || p.synthetic) continue
2891
+ if (!p.text.trim()) continue
2892
+ p.text = [
2893
+ "<system-reminder>",
2894
+ "The user sent the following message:",
2895
+ p.text,
2896
+ "",
2897
+ "Please address this message and continue with your tasks.",
2898
+ "</system-reminder>",
2899
+ ].join("\n")
2900
+ }
2901
+ }
2902
+ }
2903
+
2904
+ yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
2905
+
2906
+ const format = lastUser.format ?? { type: "text" as const }
2907
+
2908
+ // Determine if this iteration is for a fork agent (contextMode === "full").
2909
+ // Fork agents use the frozen ForkContext snapshot captured at spawn time
2910
+ // (system + inheritedMessages) rather than recomputing from their own
2911
+ // agent identity — which would diverge from the parent and break the
2912
+ // prefix cache.
2913
+ const actorRecord = lastUser.agentID
2914
+ ? yield* actorRegistry.get(sessionID, lastUser.agentID).pipe(
2915
+ Effect.orElseSucceed(() => undefined),
2916
+ )
2917
+ : undefined
2918
+ // v9 registers main as `mode: "main"` with `contextMode: "full"`.
2919
+ // Only spawned actors (subagent/peer) carry a frozen ForkContext;
2920
+ // main is the captor, never the captured.
2921
+ const isForkAgent =
2922
+ actorRecord?.contextMode === "full" &&
2923
+ (actorRecord.mode === "subagent" || actorRecord.mode === "peer")
2924
+
2925
+ // Fork path: read frozen ForkContext from Actor service (late-bound via
2926
+ // spawnRef to break the Actor → SessionPrompt → Actor layer cycle).
2927
+ // If forkCtx is missing (race / cleanup bug / spawn skipped), fail the
2928
+ // actor so the next prune turn can spawn a fresh fork.
2929
+ if (isForkAgent) {
2930
+ const forkCtxEffect = spawnRef.current?.getForkContext(lastUser.agentID!)
2931
+ const forkCtx = forkCtxEffect ? yield* forkCtxEffect : undefined
2932
+ if (!forkCtx) {
2933
+ yield* slog.warn("fork agent runLoop: missing forkContext, failing actor", {
2934
+ sessionID,
2935
+ agentID: lastUser.agentID,
2936
+ })
2937
+ yield* actorRegistry
2938
+ .updateStatus(sessionID, lastUser.agentID!, { status: "idle", lastOutcome: "failure", lastError: "missing fork context" })
2939
+ .pipe(Effect.ignore)
2940
+ return "break" as const
2941
+ }
2942
+ const ownNew = msgs.filter(
2943
+ (m) => m.info.id > forkCtx.watermarkMsgID && m.info.agentID === lastUser.agentID,
2944
+ )
2945
+ const ownNewModelMsgs = yield* MessageV2.toModelMessagesEffect(ownNew, model)
2946
+ const prebuiltSystem = forkCtx.system
2947
+ const modelMsgs: ModelMessage[] = [...forkCtx.inheritedMessages, ...ownNewModelMsgs]
2948
+ // additions is empty for fork agents: system is taken verbatim from
2949
+ // forkCtx.system. Passed as `system` to handle.process for logging/replay.
2950
+ const additions: string[] = []
2951
+ // Note: fork uses `tools` from resolveTools (not `forkCtx.tools`) — runtime
2952
+ // tool dispatch needs execute closures, which `forkCtx.tools` does not carry.
2953
+ // Schema parity with parent is currently a consequence of checkpoint-writer
2954
+ // having no toolAllowlist (Task 2.6 + agent.test.ts guard). See ForkContext.tools
2955
+ // JSDoc in packages/opencode/src/actor/spawn.ts for the full contract.
2956
+ const queryParts =
2957
+ msgs.findLast((m) => m.info.role === "user" && m.info.id === lastUser.id)?.parts ?? []
2958
+ const query = userQueryText(queryParts)
2959
+ const preQuery = {
2960
+ cancel: undefined as boolean | undefined,
2961
+ cancelReason: undefined as string | undefined,
2962
+ }
2963
+ yield* plugin.trigger(
2964
+ "session.userQuery.pre",
2965
+ { sessionID, agentID: resolvedAgentID, step, messageID: lastUser.id, query },
2966
+ preQuery,
2967
+ )
2968
+ if (preQuery.cancel) {
2969
+ cancelled = true
2970
+ cancelReason = preQuery.cancelReason
2971
+ handle.message.error = new MessageV2.AbortedError({
2972
+ message: preQuery.cancelReason ?? "Step cancelled by plugin",
2973
+ }).toObject()
2974
+ handle.message.finish = "cancelled"
2975
+ yield* sessions.updateMessage(handle.message)
2976
+ yield* plugin.trigger(
2977
+ "session.userQuery.post",
2978
+ {
2979
+ sessionID,
2980
+ agentID: resolvedAgentID,
2981
+ step,
2982
+ messageID: lastUser.id,
2983
+ query,
2984
+ assistantMessageID: handle.message.id,
2985
+ finish: handle.message.finish,
2986
+ error: preQuery.cancelReason,
2987
+ trajectory: trajectoryForStep(msgs, handle.message),
2988
+ },
2989
+ {},
2990
+ )
2991
+ return "break" as const
2992
+ }
2993
+ const result = yield* handle
2994
+ .process({
2995
+ user: lastUser,
2996
+ agent,
2997
+ // Fork inherits the parent agent's permission (captured at spawn into
2998
+ // ForkContext). This drives llm.ts resolveTools/disabled() to the SAME
2999
+ // visible tool set as the parent → prompt-cache parity on the inherited
3000
+ // prefix. Scope: this affects tool VISIBILITY only; the per-call ask
3001
+ // ruleset (built separately in resolveTools' ask closure) is unchanged.
3002
+ // Parity is exact modulo non-default `session.permission`: the parent's
3003
+ // visibility ruleset is merge(parent.permission, session.permission)
3004
+ // while the fork's is merge(writer.permission, parentPermission) — so a
3005
+ // session-level rule pins the parent but not the fork. Still a strict
3006
+ // improvement over the old bespoke "*":"deny" block (which always
3007
+ // diverged). The `?? session.permission` is defense-in-depth only:
3008
+ // parentPermission is a required field (empty `[]` on a missed capture,
3009
+ // which `??` does NOT override), so the fallback fires solely if a future
3010
+ // refactor makes the field optional.
3011
+ permission: forkCtx.parentPermission ?? session.permission,
3012
+ sessionID,
3013
+ parentSessionID: session.parentID,
3014
+ system: additions,
3015
+ prebuiltSystem,
3016
+ messages: [...modelMsgs, ...(isLastStep ? [{ role: "user" as const, content: MAX_STEPS }] : [])],
3017
+ tools,
3018
+ model,
3019
+ toolChoice: isLastStep ? "none" : format.type === "json_schema" ? "required" : undefined,
3020
+ agentID: lastUser.agentID,
3021
+ })
3022
+ .pipe(
3023
+ Effect.onExit((exit) =>
3024
+ plugin
3025
+ .trigger(
3026
+ "session.userQuery.post",
3027
+ {
3028
+ sessionID,
3029
+ agentID: resolvedAgentID,
3030
+ step,
3031
+ messageID: lastUser.id,
3032
+ query,
3033
+ assistantMessageID: handle.message.id,
3034
+ finish: handle.message.finish,
3035
+ error: Exit.isFailure(exit)
3036
+ ? Cause.pretty(exit.cause)
3037
+ : sessionErrorText(handle.message.error),
3038
+ finalText: assistantFinalText(handle.message, MessageV2.parts(handle.message.id)),
3039
+ trajectory: trajectoryForStep(msgs, handle.message),
3040
+ },
3041
+ {},
3042
+ )
3043
+ .pipe(Effect.ignore),
3044
+ ),
3045
+ )
3046
+
3047
+ if (
3048
+ result === "continue" &&
3049
+ (yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))
3050
+ ) {
3051
+ return "continue" as const
3052
+ }
3053
+
3054
+ if (result === "text-repeat") {
3055
+ if (yield* handleTextRepeat({ lastUser })) return "continue" as const
3056
+ return "break" as const
3057
+ }
3058
+
3059
+ if (structured !== undefined) {
3060
+ handle.message.structured = structured
3061
+ handle.message.finish = handle.message.finish ?? "stop"
3062
+ yield* sessions.updateMessage(handle.message)
3063
+ return "break" as const
3064
+ }
3065
+
3066
+ const forkClassification = classifyAssistantStep({
3067
+ phase: "after-process",
3068
+ lastUser,
3069
+ assistant: handle.message,
3070
+ parts: MessageV2.parts(handle.message.id),
3071
+ processResult: result,
3072
+ })
3073
+ if (forkClassification.type === "filtered") {
3074
+ yield* writeContentFilterError({ assistant: handle.message })
3075
+ return "break" as const
3076
+ }
3077
+ if (forkClassification.type === "failed") {
3078
+ yield* writeModelError({ assistant: handle.message, reason: forkClassification.reason })
3079
+ return "break" as const
3080
+ }
3081
+ if (forkClassification.type === "text-tool-call") {
3082
+ if (yield* autoRetryTextToolCall({ lastUser, assistant: handle.message })) return "continue" as const
3083
+ return "break" as const
3084
+ }
3085
+ if (forkClassification.type !== "continue" && !handle.message.error && format.type === "json_schema") {
3086
+ if (yield* autoRetryStructuredOutput({ lastUser, assistant: handle.message }))
3087
+ return "continue" as const
3088
+ return "break" as const
3089
+ }
3090
+
3091
+ if (
3092
+ (forkClassification.type === "think-only" || forkClassification.type === "invalid") &&
3093
+ format.type !== "json_schema"
3094
+ ) {
3095
+ const reason =
3096
+ forkClassification.type === "invalid" ? forkClassification.reason : "think-only"
3097
+ if (yield* autoContinueInvalidOutput({ lastUser, assistant: handle.message, reason }))
3098
+ return "continue" as const
3099
+ return "break" as const
3100
+ }
3101
+
3102
+ if (forkClassification.type === "final" && forkClassification.degraded)
3103
+ yield* slog.warn("degraded final on abnormal finish", { finish: handle.message.finish })
3104
+ if (result === "stop") return "break" as const
3105
+ // Fork agents are always subagents (lastUser.agentID is set); use
3106
+ // per-actor compaction on overflow (same as non-fork subagent path).
3107
+ if (!isBoundedComputation && result === "overflow") {
3108
+ yield* compaction
3109
+ .create({
3110
+ sessionID,
3111
+ agent: lastUser.agent,
3112
+ model: { providerID: model.providerID, modelID: model.id },
3113
+ auto: true,
3114
+ overflow: true,
3115
+ agentID: lastUser.agentID,
3116
+ })
3117
+ .pipe(Effect.ignore)
3118
+ }
3119
+ return "continue" as const
3120
+ }
3121
+
3122
+ const [skills, env, instructions] = yield* Effect.all([
3123
+ sys.skills(agent),
3124
+ Effect.sync(() => sys.environment(model, session.time.created)),
3125
+ instruction.system().pipe(Effect.orDie),
3126
+ ])
3127
+ // Surface which instruction files (CLAUDE.md, AGENTS.md, ...) were loaded.
3128
+ // Only for primary sessions (subagents would be noisy) and once per session.
3129
+ if (!session.parentID && !instructionsNotified.has(sessionID)) {
3130
+ instructionsNotified.add(sessionID)
3131
+ const worktree = (yield* InstanceState.context).worktree
3132
+ const files = Array.from(instructions.paths, (p) => Instruction.display(p, worktree))
3133
+ if (files.length > 0) {
3134
+ yield* bus.publish(TuiEvent.InstructionsLoaded, { files }).pipe(Effect.ignore)
3135
+ }
3136
+ }
3137
+ const additions = [
3138
+ ...env,
3139
+ ...(skills ? [skills] : []),
3140
+ ...instructions.content,
3141
+ ...(format.type === "json_schema" ? [STRUCTURED_OUTPUT_SYSTEM_PROMPT] : []),
3142
+ ]
3143
+ // Note: `buildLLMRequestPrefix` also returns a `tools` field, but we
3144
+ // intentionally don't use it here — the `tools` variable from `resolveTools`
3145
+ // (set earlier via `handle.process({tools: ...})`) carries `execute` closures
3146
+ // the AI SDK needs for runtime tool dispatch, while `buildLLMRequestPrefix`
3147
+ // produces schema-only tools. Schema bytes match between both paths (both call
3148
+ // registry.tools with identical args), so prefix cache parity holds.
3149
+ // Main runLoop: no watermark — LLM must see the full msgs list,
3150
+ // including this turn's intermediate assistant turns (tool reads,
3151
+ // task creates, etc.) so each step doesn't replay from the bare
3152
+ // user prompt. The watermark is for fork capture only (frozen
3153
+ // snapshot of parent-view at spawn time).
3154
+ const { system: prebuiltSystem, inheritedMessages: modelMsgs } =
3155
+ yield* buildLLMRequestPrefix({
3156
+ sessionID,
3157
+ agent,
3158
+ model,
3159
+ msgs,
3160
+ additions,
3161
+ }).pipe(
3162
+ Effect.provideService(LLM.Service, llm),
3163
+ Effect.provideService(ToolRegistry.Service, registry),
3164
+ )
3165
+ const maxModeCfg = (yield* config.get()).experimental?.maxMode
3166
+ const useMaxMode =
3167
+ agent.name === MaxMode.MAX_MODE_AGENT && maxModeCfg !== undefined && format.type !== "json_schema"
3168
+
3169
+ const processArgs = {
3170
+ user: lastUser,
3171
+ agent,
3172
+ permission: session.permission,
3173
+ sessionID,
3174
+ parentSessionID: session.parentID,
3175
+ // system: additions is preserved for non-LLM consumers of StreamInput (e.g.,
3176
+ // MessageV2.User.system for logging/replay); llm.stream itself uses prebuiltSystem.
3177
+ system: additions,
3178
+ prebuiltSystem,
3179
+ messages: [...modelMsgs, ...(isLastStep ? [{ role: "user" as const, content: MAX_STEPS }] : [])],
3180
+ tools,
3181
+ model,
3182
+ toolChoice: isLastStep ? ("none" as const) : format.type === "json_schema" ? ("required" as const) : undefined,
3183
+ agentID: lastUser.agentID,
3184
+ }
3185
+
3186
+ const queryParts =
3187
+ msgs.findLast((m) => m.info.role === "user" && m.info.id === lastUser.id)?.parts ?? []
3188
+ const query = userQueryText(queryParts)
3189
+ const preQuery = {
3190
+ cancel: undefined as boolean | undefined,
3191
+ cancelReason: undefined as string | undefined,
3192
+ }
3193
+ yield* plugin.trigger(
3194
+ "session.userQuery.pre",
3195
+ { sessionID, agentID: resolvedAgentID, step, messageID: lastUser.id, query },
3196
+ preQuery,
3197
+ )
3198
+ if (preQuery.cancel) {
3199
+ cancelled = true
3200
+ cancelReason = preQuery.cancelReason
3201
+ handle.message.error = new MessageV2.AbortedError({
3202
+ message: preQuery.cancelReason ?? "Step cancelled by plugin",
3203
+ }).toObject()
3204
+ handle.message.finish = "cancelled"
3205
+ yield* sessions.updateMessage(handle.message)
3206
+ yield* plugin.trigger(
3207
+ "session.userQuery.post",
3208
+ {
3209
+ sessionID,
3210
+ agentID: resolvedAgentID,
3211
+ step,
3212
+ messageID: lastUser.id,
3213
+ query,
3214
+ assistantMessageID: handle.message.id,
3215
+ finish: handle.message.finish,
3216
+ error: preQuery.cancelReason,
3217
+ trajectory: trajectoryForStep(msgs, handle.message),
3218
+ },
3219
+ {},
3220
+ )
3221
+ return "break" as const
3222
+ }
3223
+
3224
+ const stepEffect = useMaxMode
3225
+ ? MaxMode.runMaxStep({
3226
+ // runMaxStep reuses the identical per-step args as handle.process,
3227
+ // plus the orchestration handles it needs.
3228
+ ...processArgs,
3229
+ handle,
3230
+ llm,
3231
+ candidates: maxModeCfg?.candidates,
3232
+ setStatus: (message) =>
3233
+ status.set(sessionID, message ? { type: "busy", message } : { type: "busy" }),
3234
+ })
3235
+ : handle.process(processArgs)
3236
+
3237
+ const result = yield* stepEffect.pipe(
3238
+ Effect.onExit((exit) =>
3239
+ plugin
3240
+ .trigger(
3241
+ "session.userQuery.post",
3242
+ {
3243
+ sessionID,
3244
+ agentID: resolvedAgentID,
3245
+ step,
3246
+ messageID: lastUser.id,
3247
+ query,
3248
+ assistantMessageID: handle.message.id,
3249
+ finish: handle.message.finish,
3250
+ error: Exit.isFailure(exit)
3251
+ ? Cause.pretty(exit.cause)
3252
+ : sessionErrorText(handle.message.error),
3253
+ finalText: assistantFinalText(handle.message, MessageV2.parts(handle.message.id)),
3254
+ trajectory: trajectoryForStep(msgs, handle.message),
3255
+ },
3256
+ {},
3257
+ )
3258
+ .pipe(Effect.ignore),
3259
+ ),
3260
+ )
3261
+
3262
+ if (
3263
+ result === "continue" &&
3264
+ (yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))
3265
+ ) {
3266
+ return "continue" as const
3267
+ }
3268
+
3269
+ if (result === "text-repeat") {
3270
+ if (yield* handleTextRepeat({ lastUser })) return "continue" as const
3271
+ return "break" as const
3272
+ }
3273
+
3274
+ if (structured !== undefined) {
3275
+ handle.message.structured = structured
3276
+ handle.message.finish = handle.message.finish ?? "stop"
3277
+ yield* sessions.updateMessage(handle.message)
3278
+ return "break" as const
3279
+ }
3280
+
3281
+ const classification = classifyAssistantStep({
3282
+ phase: "after-process",
3283
+ lastUser,
3284
+ assistant: handle.message,
3285
+ parts: MessageV2.parts(handle.message.id),
3286
+ processResult: result,
3287
+ })
3288
+ if (classification.type === "filtered") {
3289
+ yield* writeContentFilterError({ assistant: handle.message })
3290
+ return "break" as const
3291
+ }
3292
+ if (classification.type === "failed") {
3293
+ yield* writeModelError({ assistant: handle.message, reason: classification.reason })
3294
+ return "break" as const
3295
+ }
3296
+ if (classification.type === "text-tool-call") {
3297
+ if (yield* autoRetryTextToolCall({ lastUser, assistant: handle.message })) return "continue" as const
3298
+ return "break" as const
3299
+ }
3300
+ if (classification.type !== "continue" && !handle.message.error && format.type === "json_schema") {
3301
+ if (yield* autoRetryStructuredOutput({ lastUser, assistant: handle.message })) return "continue" as const
3302
+ return "break" as const
3303
+ }
3304
+
3305
+ if (
3306
+ (classification.type === "think-only" || classification.type === "invalid") &&
3307
+ format.type !== "json_schema"
3308
+ ) {
3309
+ const reason = classification.type === "invalid" ? classification.reason : "think-only"
3310
+ if (yield* autoContinueInvalidOutput({ lastUser, assistant: handle.message, reason }))
3311
+ return "continue" as const
3312
+ return "break" as const
3313
+ }
3314
+
3315
+ if (classification.type === "final" && classification.degraded)
3316
+ yield* slog.warn("degraded final on abnormal finish", { finish: handle.message.finish })
3317
+ if (result === "stop") return "break" as const
3318
+ if (!isBoundedComputation && result === "overflow") {
3319
+ // Subagent overflow → per-actor compaction. Insert a boundary
3320
+ // tagged with the subagent's agent_id; the next runLoop iteration
3321
+ // will see a trimmed context (filterCompactedEffect stops at
3322
+ // the boundary).
3323
+ // Gate must exclude "main" — see comment at the matching gate
3324
+ // earlier in this file (~line 1716) and at checkpoint.ts:715.
3325
+ if (lastUser.agentID && lastUser.agentID !== "main") {
3326
+ yield* compaction
3327
+ .create({
3328
+ sessionID,
3329
+ agent: lastUser.agent,
3330
+ model: { providerID: model.providerID, modelID: model.id },
3331
+ auto: true,
3332
+ overflow: true,
3333
+ agentID: lastUser.agentID,
3334
+ })
3335
+ .pipe(Effect.ignore)
3336
+ return "continue" as const
3337
+ }
3338
+
3339
+ // Main-agent provider-signalled overflow: insert a checkpoint
3340
+ // boundary marker (never deletes). Prefer rebuild over compaction:
3341
+ // if a writer is running or finished, wait (bounded) and rebuild
3342
+ // from it. Fall back to compaction only when no boundary exists.
3343
+ const writerRunning = yield* checkpoint.isWriterRunning(sessionID)
3344
+ .pipe(Effect.catch(() => Effect.succeed(false)))
3345
+ const hasCP = yield* checkpoint.hasCheckpoint(sessionID)
3346
+ .pipe(Effect.catch(() => Effect.succeed(false)))
3347
+
3348
+ if (writerRunning || hasCP) {
3349
+ yield* checkpoint.waitForWriter(sessionID).pipe(Effect.ignore)
3350
+ const boundary2 = yield* checkpoint.lastBoundary(sessionID)
3351
+ .pipe(Effect.catch(() => Effect.succeed(undefined)))
3352
+ const boundary2Msg = boundary2 ? msgs.find((m) => m.info.id === boundary2) : undefined
3353
+ const inserted2 = boundary2
3354
+ ? yield* checkpoint
3355
+ .insertRebuildBoundary({
3356
+ sessionID,
3357
+ boundary: boundary2,
3358
+ lastMessageInfo: computeLastMessageInfo(msgs.map((m) => m.info)),
3359
+ agentID: lastUser.agentID,
3360
+ agent: lastUser.agent,
3361
+ model: { providerID: model.providerID, modelID: model.id },
3362
+ boundaryCreatedAt: boundary2Msg?.info.time.created,
3363
+ })
3364
+ .pipe(Effect.catch(() => Effect.succeed(false)))
3365
+ : false
3366
+
3367
+ if (inserted2) {
3368
+ yield* prune.resetThresholds(sessionID)
3369
+ return "continue" as const
3370
+ }
3371
+ }
3372
+
3373
+ // F39: no checkpoint — fall back to compaction (LLM-driven lossy summary).
3374
+ yield* compaction
3375
+ .create({
3376
+ sessionID,
3377
+ agent: lastUser.agent,
3378
+ model: { providerID: model.providerID, modelID: model.id },
3379
+ auto: true,
3380
+ overflow: true,
3381
+ agentID: lastUser.agentID,
3382
+ })
3383
+ .pipe(Effect.ignore)
3384
+ }
3385
+ return "continue" as const
3386
+ }).pipe(Effect.ensuring(instruction.clear(handle.message.id)))
3387
+
3388
+ // --- Text Loop Detection (cross-step) ---
3389
+ const completedParts = MessageV2.parts(handle.message.id)
3390
+ const stepText = completedParts
3391
+ .filter((p): p is MessageV2.TextPart => p.type === "text" && !p.synthetic)
3392
+ .map((p) => p.text)
3393
+ .join(" ")
3394
+ if (stepText.trim()) {
3395
+ // Include tool call signatures in the key so same text + different tools ≠ loop
3396
+ const toolSig = completedParts
3397
+ .filter((p): p is MessageV2.ToolPart => p.type === "tool")
3398
+ .map((p) => `${p.tool}:${JSON.stringify(p.state && "input" in p.state ? p.state.input : "")}`)
3399
+ .join("|")
3400
+ const normalized = normalizeForLoopDetection(stepText) + (toolSig ? `\0${toolSig}` : "")
3401
+ textLoopBuffer.push(normalized)
3402
+ if (textLoopBuffer.length > TEXT_LOOP_BUFFER_SIZE) textLoopBuffer.shift()
3403
+
3404
+ if (textLoopBuffer.length >= TEXT_LOOP_TRIGGER_COUNT) {
3405
+ const isTextLoop = detectTextLoop(textLoopBuffer, TEXT_LOOP_TRIGGER_COUNT)
3406
+
3407
+ if (isTextLoop) {
3408
+ if (textLoopRecoveryAttempts >= TEXT_LOOP_MAX_RECOVERY) {
3409
+ yield* slog.info("text loop: max recovery exceeded, terminating")
3410
+ yield* bus.publish(Session.Event.Error, {
3411
+ sessionID,
3412
+ error: new NamedError.Unknown({
3413
+ message: `Text loop detected: model repeated the same output ${TEXT_LOOP_TRIGGER_COUNT} times after ${TEXT_LOOP_MAX_RECOVERY} recovery attempts. Session terminated.`,
3414
+ }).toObject(),
3415
+ })
3416
+ break
3417
+ }
3418
+ const recoveryText =
3419
+ textLoopRecoveryAttempts === 0 ? RECOVERY_PROMPT_MILD : RECOVERY_PROMPT_STRONG
3420
+ // Create a NEW user message at the end of conversation (not append to original)
3421
+ const reentry = yield* sessions.updateMessage({
3422
+ id: MessageID.ascending(),
3423
+ role: "user" as const,
3424
+ sessionID,
3425
+ agentID: lastUser.agentID,
3426
+ agent: lastUser.agent,
3427
+ model: lastUser.model,
3428
+ tools: lastUser.tools,
3429
+ format: lastUser.format,
3430
+ time: { created: Date.now() },
3431
+ })
3432
+ yield* sessions.updatePart({
3433
+ id: PartID.ascending(),
3434
+ messageID: reentry.id,
3435
+ sessionID,
3436
+ type: "text",
3437
+ synthetic: true,
3438
+ text: recoveryText,
3439
+ } satisfies MessageV2.TextPart)
3440
+ textLoopRecoveryAttempts++
3441
+ textLoopBuffer.length = 0
3442
+ yield* slog.info("text loop: recovery injected", { attempt: textLoopRecoveryAttempts })
3443
+ continue
3444
+ }
3445
+ }
3446
+ }
3447
+
3448
+ if (outcome === "break") {
3449
+ if (yield* taskGate(lastUser)) continue
3450
+ if (yield* goalGate(lastUser)) continue
3451
+ break
3452
+ }
3453
+ continue
3454
+ }
3455
+
3456
+ const promptOps = yield* ops()
3457
+ if (lastModelForPrune && lastFinishedForPrune) {
3458
+ yield* prune
3459
+ .prune({
3460
+ sessionID,
3461
+ model: lastModelForPrune,
3462
+ tokens: lastFinishedForPrune.tokens,
3463
+ lastAssistantTime: lastFinishedForPrune.time.completed,
3464
+ promptOps,
3465
+ })
3466
+ .pipe(Effect.ignore, Effect.forkIn(scope))
3467
+ }
3468
+ const final = yield* lastAssistant(sessionID, agentID)
3469
+ const finalIsError = final.info.role === "assistant" && !!final.info.error
3470
+ const lastUserForMetrics = yield* sessions.findMessage(
3471
+ sessionID,
3472
+ (m) => m.info.role === "user",
3473
+ { agentID: "*" },
3474
+ )
3475
+ yield* publishAgentRequest(
3476
+ finalIsError ? "error" : "completed",
3477
+ Option.isSome(lastUserForMetrics) ? lastUserForMetrics.value.info.agent : final.info.agent,
3478
+ )
3479
+ return final
3480
+ }).pipe(Effect.onExit(firePostSession), Effect.orDie)
3481
+ },
3482
+ )
3483
+
3484
+ const loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
3485
+ "SessionPrompt.loop",
3486
+ )(function* (input: z.infer<typeof LoopInput>) {
3487
+ const agentID = input.agentID ?? "main"
3488
+ return yield* state.ensureRunning(
3489
+ input.sessionID,
3490
+ agentID,
3491
+ lastAssistant(input.sessionID, agentID),
3492
+ runLoop(input.sessionID, agentID, input.task_id),
3493
+ )
3494
+ })
3495
+
3496
+ const shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.shell")(
3497
+ function* (input: ShellInput) {
3498
+ return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input))
3499
+ },
3500
+ )
3501
+
3502
+ const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
3503
+ yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
3504
+ const cmd = yield* commands.get(input.command)
3505
+ if (!cmd) {
3506
+ const available = (yield* commands.list()).map((c) => c.name)
3507
+ const hint = available.length ? ` Available commands: ${available.join(", ")}` : ""
3508
+ const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` })
3509
+ yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
3510
+ throw error
3511
+ }
3512
+ const agentName = cmd.agent ?? input.agent ?? (yield* agents.defaultAgent())
3513
+
3514
+ // /goal — set or clear a session-level stop-condition goal. The condition
3515
+ // text itself becomes the prompt for this turn (the working agent starts
3516
+ // pursuing it immediately); the main runLoop then refuses to stop until
3517
+ // the judge says it's satisfied. See session/goal.ts.
3518
+ if (input.command === Command.Default.GOAL) {
3519
+ const condition = input.arguments.trim()
3520
+ if (condition === "" || condition === "clear" || condition === "reset") {
3521
+ yield* goal.clear(input.sessionID)
3522
+ return yield* prompt({
3523
+ sessionID: input.sessionID,
3524
+ messageID: input.messageID,
3525
+ agent: agentName,
3526
+ parts: [{ type: "text", text: "Goal cleared.", synthetic: true }],
3527
+ noReply: true,
3528
+ })
3529
+ }
3530
+ yield* goal.set(input.sessionID, condition)
3531
+ }
3532
+
3533
+ const raw = input.arguments.match(argsRegex) ?? []
3534
+ const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
3535
+ const templateCommand = yield* Effect.promise(async () => cmd.template)
3536
+
3537
+ let template: string
3538
+ if (cmd.source === "skill") {
3539
+ template = input.arguments
3540
+ } else {
3541
+ const placeholders = templateCommand.match(placeholderRegex) ?? []
3542
+ let last = 0
3543
+ for (const item of placeholders) {
3544
+ const value = Number(item.slice(1))
3545
+ if (value > last) last = value
3546
+ }
3547
+
3548
+ const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
3549
+ const position = Number(index)
3550
+ const argIndex = position - 1
3551
+ if (argIndex >= args.length) return ""
3552
+ if (position === last) return args.slice(argIndex).join(" ")
3553
+ return args[argIndex]
3554
+ })
3555
+ const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS")
3556
+ template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
3557
+
3558
+ if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) {
3559
+ template = template + "\n\n" + input.arguments
3560
+ }
3561
+ }
3562
+
3563
+ const shellMatches = ConfigMarkdown.shell(template)
3564
+ if (shellMatches.length > 0) {
3565
+ const sh = Shell.preferred()
3566
+ const results = yield* Effect.promise(() =>
3567
+ Promise.all(
3568
+ shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text),
3569
+ ),
3570
+ )
3571
+ let index = 0
3572
+ template = template.replace(bashRegex, () => results[index++])
3573
+ }
3574
+ template = template.trim()
3575
+
3576
+ const taskModel = yield* Effect.gen(function* () {
3577
+ if (cmd.model) return Provider.parseModel(cmd.model)
3578
+ if (cmd.agent) {
3579
+ const cmdAgent = yield* agents.get(cmd.agent)
3580
+ if (cmdAgent?.model) return cmdAgent.model
3581
+ }
3582
+ if (input.model) return Provider.parseModel(input.model)
3583
+ return yield* lastModel(input.sessionID)
3584
+ })
3585
+
3586
+ yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
3587
+
3588
+ const agent = yield* agents.get(agentName)
3589
+ if (!agent) {
3590
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
3591
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
3592
+ const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
3593
+ yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
3594
+ throw error
3595
+ }
3596
+
3597
+ const templateParts = yield* resolvePromptParts(template)
3598
+ const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
3599
+
3600
+ let parts: PromptInput["parts"]
3601
+ if (isSubtask) {
3602
+ const promptText = cmd.source === "skill"
3603
+ ? templateCommand + (input.arguments.trim() ? "\n\n" + input.arguments : "")
3604
+ : (templateParts.find((y): y is typeof y & { type: "text"; text: string } => y.type === "text"))?.text ?? ""
3605
+ parts = [
3606
+ {
3607
+ type: "subtask" as const,
3608
+ agent: agent.name,
3609
+ description: cmd.description ?? "",
3610
+ command: input.command,
3611
+ model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
3612
+ prompt: promptText,
3613
+ },
3614
+ ]
3615
+ } else if (cmd.source === "skill") {
3616
+ const visibleText = input.arguments.trim()
3617
+ ? `/${input.command} ${input.arguments}`
3618
+ : `/${input.command}`
3619
+ const skillPart = {
3620
+ type: "text" as const,
3621
+ text: `<skill_content name="${input.command}">\n${templateCommand}\n</skill_content>`,
3622
+ synthetic: true,
3623
+ }
3624
+ const attachments = templateParts.filter((p): p is Exclude<typeof p, { type: "text" }> => p.type !== "text")
3625
+ parts = [{ type: "text" as const, text: visibleText }, skillPart, ...attachments, ...(input.parts ?? [])]
3626
+ } else {
3627
+ parts = [...templateParts, ...(input.parts ?? [])]
3628
+ }
3629
+
3630
+ const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultAgent())) : agentName
3631
+ const userModel = isSubtask
3632
+ ? input.model
3633
+ ? Provider.parseModel(input.model)
3634
+ : yield* lastModel(input.sessionID)
3635
+ : taskModel
3636
+
3637
+ yield* plugin.trigger(
3638
+ "command.execute.before",
3639
+ { command: input.command, sessionID: input.sessionID, arguments: input.arguments },
3640
+ { parts },
3641
+ )
3642
+
3643
+ const result = yield* prompt({
3644
+ sessionID: input.sessionID,
3645
+ messageID: input.messageID,
3646
+ model: userModel,
3647
+ agent: userAgent,
3648
+ parts,
3649
+ variant: input.variant,
3650
+ })
3651
+ yield* bus.publish(Command.Event.Executed, {
3652
+ name: input.command,
3653
+ sessionID: input.sessionID,
3654
+ arguments: input.arguments,
3655
+ messageID: result.info.id,
3656
+ })
3657
+ return result
3658
+ })
3659
+
3660
+ const impl = Service.of({
3661
+ cancel,
3662
+ prompt,
3663
+ loop,
3664
+ shell,
3665
+ command,
3666
+ resolvePromptParts,
3667
+ sweepOrphanAssistants,
3668
+ predict,
3669
+ })
3670
+ sessionPromptRef.current = { loop: impl.loop }
3671
+ yield* Effect.addFinalizer(() =>
3672
+ Effect.sync(() => {
3673
+ if (sessionPromptRef.current?.loop === impl.loop) sessionPromptRef.current = undefined
3674
+ }),
3675
+ )
3676
+ return impl
3677
+ }),
3678
+ )
3679
+
3680
+ export const defaultLayer = Layer.suspend(() =>
3681
+ layer.pipe(
3682
+ Layer.provide(SessionRunState.defaultLayer),
3683
+ Layer.provide(SessionStatus.defaultLayer),
3684
+ Layer.provide(SessionPrune.defaultLayer),
3685
+ Layer.provide(SessionCheckpoint.defaultLayer),
3686
+ Layer.provide(SessionCompaction.defaultLayer),
3687
+ Layer.provide(SessionProcessor.defaultLayer),
3688
+ Layer.provide(Command.defaultLayer),
3689
+ Layer.provide(Permission.defaultLayer),
3690
+ Layer.provide(MCP.defaultLayer),
3691
+ Layer.provide(LSP.defaultLayer),
3692
+ Layer.provide(ToolRegistry.defaultLayer),
3693
+ Layer.provide(Truncate.defaultLayer),
3694
+ Layer.provide(Provider.defaultLayer),
3695
+ Layer.provide(Instruction.defaultLayer),
3696
+ Layer.provide(AppFileSystem.defaultLayer),
3697
+ Layer.provide(Plugin.defaultLayer),
3698
+ Layer.provide(Session.defaultLayer),
3699
+ Layer.provide(SessionRevert.defaultLayer),
3700
+ Layer.provide(
3701
+ Layer.mergeAll(
3702
+ Config.defaultLayer,
3703
+ SessionSummary.defaultLayer,
3704
+ Team.defaultLayer,
3705
+ ActorRegistry.defaultLayer,
3706
+ Agent.defaultLayer,
3707
+ SystemPrompt.defaultLayer,
3708
+ LLM.defaultLayer,
3709
+ Bus.layer,
3710
+ CrossSpawnSpawner.defaultLayer,
3711
+ Inbox.defaultLayer,
3712
+ Goal.defaultLayer,
3713
+ TaskGateState.defaultLayer,
3714
+ TaskRegistry.defaultLayer,
3715
+ ),
3716
+ ),
3717
+ ),
3718
+ )
3719
+ export const PromptInput = z.object({
3720
+ sessionID: SessionID.zod,
3721
+ messageID: MessageID.zod.optional(),
3722
+ model: z
3723
+ .object({
3724
+ providerID: ProviderID.zod,
3725
+ modelID: ModelID.zod,
3726
+ })
3727
+ .optional(),
3728
+ modelRef: z
3729
+ .string()
3730
+ .optional()
3731
+ .describe(
3732
+ "Model group/tier name (e.g. ultra/standard/lite) or a literal provider/model. Resolved provider-aware. Takes precedence over `model` when both are set.",
3733
+ ),
3734
+ agent: z.string().optional(),
3735
+ agentID: z.string().optional(),
3736
+ task_id: z.string().optional()
3737
+ .describe("If the spawning caller bound this prompt to a specific user-task (T4 etc), pass its TID. Propagates to Tool.Context.taskId so memory-path-guard allows writes to tasks/<task_id>/*.md."),
3738
+ source: z.enum(["user", "spawn", "hook"]).optional(),
3739
+ provenance: MessageV2.Provenance.optional(),
3740
+ noReply: z.boolean().optional(),
3741
+ tools: z
3742
+ .record(z.string(), z.boolean())
3743
+ .optional()
3744
+ .describe("@deprecated tools and permissions have been merged, you can set permissions on the session itself now"),
3745
+ format: MessageV2.Format.optional(),
3746
+ system: z.string().optional(),
3747
+ variant: z.string().optional(),
3748
+ parts: z.array(
3749
+ z.discriminatedUnion("type", [
3750
+ MessageV2.TextPart.omit({
3751
+ messageID: true,
3752
+ sessionID: true,
3753
+ })
3754
+ .partial({
3755
+ id: true,
3756
+ })
3757
+ .meta({
3758
+ ref: "TextPartInput",
3759
+ }),
3760
+ MessageV2.FilePart.omit({
3761
+ messageID: true,
3762
+ sessionID: true,
3763
+ })
3764
+ .partial({
3765
+ id: true,
3766
+ })
3767
+ .meta({
3768
+ ref: "FilePartInput",
3769
+ }),
3770
+ MessageV2.AgentPart.omit({
3771
+ messageID: true,
3772
+ sessionID: true,
3773
+ })
3774
+ .partial({
3775
+ id: true,
3776
+ })
3777
+ .meta({
3778
+ ref: "AgentPartInput",
3779
+ }),
3780
+ MessageV2.SubtaskPart.omit({
3781
+ messageID: true,
3782
+ sessionID: true,
3783
+ })
3784
+ .partial({
3785
+ id: true,
3786
+ })
3787
+ .meta({
3788
+ ref: "SubtaskPartInput",
3789
+ }),
3790
+ ]),
3791
+ ),
3792
+ })
3793
+ export type PromptInput = z.infer<typeof PromptInput>
3794
+
3795
+ export const LoopInput = z.object({
3796
+ sessionID: SessionID.zod,
3797
+ agentID: z.string().optional(),
3798
+ task_id: z.string().optional(),
3799
+ })
3800
+
3801
+ export const ShellInput = z.object({
3802
+ sessionID: SessionID.zod,
3803
+ messageID: MessageID.zod.optional(),
3804
+ agent: z.string(),
3805
+ model: z
3806
+ .object({
3807
+ providerID: ProviderID.zod,
3808
+ modelID: ModelID.zod,
3809
+ })
3810
+ .optional(),
3811
+ modelRef: z
3812
+ .string()
3813
+ .optional()
3814
+ .describe(
3815
+ "Model group/tier name (e.g. ultra/standard/lite) or a literal provider/model. Resolved provider-aware. Takes precedence over `model` when both are set.",
3816
+ ),
3817
+ command: z.string(),
3818
+ })
3819
+ export type ShellInput = z.infer<typeof ShellInput>
3820
+
3821
+ export const CommandInput = z.object({
3822
+ messageID: MessageID.zod.optional(),
3823
+ sessionID: SessionID.zod,
3824
+ agent: z.string().optional(),
3825
+ model: z.string().optional(),
3826
+ arguments: z.string(),
3827
+ command: z.string(),
3828
+ variant: z.string().optional(),
3829
+ parts: z
3830
+ .array(
3831
+ z.discriminatedUnion("type", [
3832
+ MessageV2.FilePart.omit({
3833
+ messageID: true,
3834
+ sessionID: true,
3835
+ }).partial({
3836
+ id: true,
3837
+ }),
3838
+ ]),
3839
+ )
3840
+ .optional(),
3841
+ })
3842
+ export type CommandInput = z.infer<typeof CommandInput>
3843
+
3844
+ /** @internal Exported for testing */
3845
+ export function createStructuredOutputTool(input: {
3846
+ schema: Record<string, any>
3847
+ onSuccess: (output: unknown) => void
3848
+ }): AITool {
3849
+ // Remove $schema property if present (not needed for tool input)
3850
+ const { $schema: _, ...toolSchema } = input.schema
3851
+
3852
+ return tool({
3853
+ description: STRUCTURED_OUTPUT_DESCRIPTION,
3854
+ inputSchema: jsonSchema(toolSchema as JSONSchema7),
3855
+ async execute(args) {
3856
+ // AI SDK validates args against inputSchema before calling execute()
3857
+ input.onSuccess(args)
3858
+ return {
3859
+ output: "Structured output captured successfully.",
3860
+ title: "Structured Output",
3861
+ metadata: { valid: true },
3862
+ }
3863
+ },
3864
+ toModelOutput({ output }) {
3865
+ return {
3866
+ type: "text",
3867
+ value: output.output,
3868
+ }
3869
+ },
3870
+ })
3871
+ }
3872
+ const bashRegex = /!`([^`]+)`/g
3873
+ // Match [Image N] as single token, quoted strings, or non-space sequences
3874
+ const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
3875
+ const placeholderRegex = /\$(\d+)/g
3876
+ const quoteTrimRegex = /^["']|["']$/g
3877
+
3878
+ export * as SessionPrompt from "./prompt"