tulingcode 0.1.0

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