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