@sleepy-ai/cli 0.1.16 → 0.1.17

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