mervexai 0.0.97

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 (782) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/bin/mervex +222 -0
  5. package/bunfig.toml +7 -0
  6. package/git +0 -0
  7. package/migration/20260511173437_session-metadata/migration.sql +1 -0
  8. package/migration/20260511173437_session-metadata/snapshot.json +1500 -0
  9. package/package.json +159 -0
  10. package/parsers-config.ts +1 -0
  11. package/script/bench-search.ts +94 -0
  12. package/script/bench-test-suite.ts +52 -0
  13. package/script/build.ts +257 -0
  14. package/script/generate.ts +14 -0
  15. package/script/httpapi-exercise.ts +1 -0
  16. package/script/postinstall.mjs +185 -0
  17. package/script/profile-test-files.ts +42 -0
  18. package/script/publish.ts +213 -0
  19. package/script/run-workspace-server +106 -0
  20. package/script/schema.ts +77 -0
  21. package/script/time.ts +6 -0
  22. package/script/trace-imports.ts +153 -0
  23. package/specs/effect/error-boundaries-plan.md +235 -0
  24. package/specs/effect/errors.md +207 -0
  25. package/specs/effect/facades.md +218 -0
  26. package/specs/effect/guide.md +247 -0
  27. package/specs/effect/instance-context.md +13 -0
  28. package/specs/effect/loose-ends.md +30 -0
  29. package/specs/effect/migration.md +62 -0
  30. package/specs/effect/routes.md +61 -0
  31. package/specs/effect/schema.md +88 -0
  32. package/specs/effect/server-package.md +58 -0
  33. package/specs/effect/todo.md +241 -0
  34. package/specs/effect/tools.md +88 -0
  35. package/specs/openapi-translation-cleanup.md +204 -0
  36. package/specs/tui-plugins.md +544 -0
  37. package/specs/v2/api.ts +67 -0
  38. package/specs/v2/message-shape.md +136 -0
  39. package/specs/v2/notifications.md +13 -0
  40. package/specs/v2/tui-command-shim.md +67 -0
  41. package/src/account/account.ts +481 -0
  42. package/src/account/repo.ts +171 -0
  43. package/src/account/schema.ts +99 -0
  44. package/src/account/url.ts +8 -0
  45. package/src/acp/agent.ts +95 -0
  46. package/src/acp/config-option.ts +203 -0
  47. package/src/acp/content.ts +269 -0
  48. package/src/acp/directory.ts +212 -0
  49. package/src/acp/error.ts +97 -0
  50. package/src/acp/event.ts +421 -0
  51. package/src/acp/permission.ts +254 -0
  52. package/src/acp/profile.ts +42 -0
  53. package/src/acp/service.ts +1105 -0
  54. package/src/acp/session.ts +232 -0
  55. package/src/acp/tool.ts +364 -0
  56. package/src/acp/usage.ts +243 -0
  57. package/src/agent/agent.ts +508 -0
  58. package/src/agent/generate.txt +75 -0
  59. package/src/agent/prompt/architect.txt +7 -0
  60. package/src/agent/prompt/ask.txt +7 -0
  61. package/src/agent/prompt/compaction.txt +5 -0
  62. package/src/agent/prompt/explore.txt +18 -0
  63. package/src/agent/prompt/summary.txt +11 -0
  64. package/src/agent/prompt/title.txt +44 -0
  65. package/src/agent/subagent-permissions.ts +27 -0
  66. package/src/audio.d.ts +14 -0
  67. package/src/auth/index.ts +97 -0
  68. package/src/background/job.ts +37 -0
  69. package/src/bus/global.ts +22 -0
  70. package/src/cli/bootstrap.ts +11 -0
  71. package/src/cli/cmd/account.ts +264 -0
  72. package/src/cli/cmd/acp.ts +73 -0
  73. package/src/cli/cmd/agent.ts +259 -0
  74. package/src/cli/cmd/attach.ts +148 -0
  75. package/src/cli/cmd/cmd.ts +7 -0
  76. package/src/cli/cmd/db.ts +62 -0
  77. package/src/cli/cmd/debug/agent.handler.ts +193 -0
  78. package/src/cli/cmd/debug/agent.ts +27 -0
  79. package/src/cli/cmd/debug/config.ts +14 -0
  80. package/src/cli/cmd/debug/file.ts +73 -0
  81. package/src/cli/cmd/debug/index.ts +87 -0
  82. package/src/cli/cmd/debug/lsp.ts +50 -0
  83. package/src/cli/cmd/debug/ripgrep.ts +79 -0
  84. package/src/cli/cmd/debug/scrap.ts +16 -0
  85. package/src/cli/cmd/debug/skill.ts +15 -0
  86. package/src/cli/cmd/debug/snapshot.ts +50 -0
  87. package/src/cli/cmd/debug/startup.ts +11 -0
  88. package/src/cli/cmd/debug/v2.ts +42 -0
  89. package/src/cli/cmd/export.ts +292 -0
  90. package/src/cli/cmd/generate.ts +54 -0
  91. package/src/cli/cmd/github.handler.ts +1606 -0
  92. package/src/cli/cmd/github.shared.ts +30 -0
  93. package/src/cli/cmd/github.ts +42 -0
  94. package/src/cli/cmd/import.ts +230 -0
  95. package/src/cli/cmd/mcp.ts +840 -0
  96. package/src/cli/cmd/models.ts +66 -0
  97. package/src/cli/cmd/plug.ts +230 -0
  98. package/src/cli/cmd/pr.ts +115 -0
  99. package/src/cli/cmd/prompt-display.ts +1 -0
  100. package/src/cli/cmd/providers.ts +594 -0
  101. package/src/cli/cmd/repair.ts +231 -0
  102. package/src/cli/cmd/run/demo.ts +1274 -0
  103. package/src/cli/cmd/run/entry.body.ts +205 -0
  104. package/src/cli/cmd/run/footer.command.tsx +1070 -0
  105. package/src/cli/cmd/run/footer.menu.tsx +351 -0
  106. package/src/cli/cmd/run/footer.permission.tsx +472 -0
  107. package/src/cli/cmd/run/footer.prompt.tsx +1306 -0
  108. package/src/cli/cmd/run/footer.question.tsx +573 -0
  109. package/src/cli/cmd/run/footer.subagent.tsx +175 -0
  110. package/src/cli/cmd/run/footer.ts +1129 -0
  111. package/src/cli/cmd/run/footer.view.tsx +945 -0
  112. package/src/cli/cmd/run/footer.width.ts +27 -0
  113. package/src/cli/cmd/run/permission.shared.ts +256 -0
  114. package/src/cli/cmd/run/prompt.editor.ts +157 -0
  115. package/src/cli/cmd/run/prompt.shared.ts +153 -0
  116. package/src/cli/cmd/run/question.shared.ts +340 -0
  117. package/src/cli/cmd/run/runtime.boot.ts +202 -0
  118. package/src/cli/cmd/run/runtime.lifecycle.ts +406 -0
  119. package/src/cli/cmd/run/runtime.queue.ts +349 -0
  120. package/src/cli/cmd/run/runtime.shared.ts +17 -0
  121. package/src/cli/cmd/run/runtime.stdin.ts +37 -0
  122. package/src/cli/cmd/run/runtime.ts +814 -0
  123. package/src/cli/cmd/run/scrollback.shared.ts +92 -0
  124. package/src/cli/cmd/run/scrollback.surface.ts +431 -0
  125. package/src/cli/cmd/run/scrollback.writer.tsx +352 -0
  126. package/src/cli/cmd/run/session-data.ts +1113 -0
  127. package/src/cli/cmd/run/session-replay.ts +374 -0
  128. package/src/cli/cmd/run/session.shared.ts +196 -0
  129. package/src/cli/cmd/run/splash.ts +280 -0
  130. package/src/cli/cmd/run/stream.transport.ts +1462 -0
  131. package/src/cli/cmd/run/stream.ts +175 -0
  132. package/src/cli/cmd/run/subagent-data.ts +876 -0
  133. package/src/cli/cmd/run/theme.ts +690 -0
  134. package/src/cli/cmd/run/tool.ts +1486 -0
  135. package/src/cli/cmd/run/trace.ts +94 -0
  136. package/src/cli/cmd/run/turn-summary.ts +47 -0
  137. package/src/cli/cmd/run/types.ts +350 -0
  138. package/src/cli/cmd/run/variant.shared.ts +216 -0
  139. package/src/cli/cmd/run.ts +1104 -0
  140. package/src/cli/cmd/serve.ts +24 -0
  141. package/src/cli/cmd/session.ts +147 -0
  142. package/src/cli/cmd/stats.ts +393 -0
  143. package/src/cli/cmd/tui.ts +317 -0
  144. package/src/cli/cmd/uninstall.ts +353 -0
  145. package/src/cli/cmd/upgrade.ts +74 -0
  146. package/src/cli/cmd/web.ts +84 -0
  147. package/src/cli/effect/prompt.ts +37 -0
  148. package/src/cli/effect-cmd.ts +96 -0
  149. package/src/cli/error.ts +130 -0
  150. package/src/cli/heap.ts +45 -0
  151. package/src/cli/logo.ts +1 -0
  152. package/src/cli/network.ts +80 -0
  153. package/src/cli/tui/layer.ts +8 -0
  154. package/src/cli/tui/validate-session.ts +29 -0
  155. package/src/cli/tui/worker.ts +80 -0
  156. package/src/cli/ui.ts +131 -0
  157. package/src/cli/upgrade.ts +68 -0
  158. package/src/command/index.ts +328 -0
  159. package/src/command/template/initialize.txt +66 -0
  160. package/src/command/template/review.txt +101 -0
  161. package/src/config/agent.ts +59 -0
  162. package/src/config/command.ts +39 -0
  163. package/src/config/config.ts +720 -0
  164. package/src/config/entry-name.ts +19 -0
  165. package/src/config/managed.ts +69 -0
  166. package/src/config/markdown.ts +36 -0
  167. package/src/config/parse.ts +61 -0
  168. package/src/config/paths.ts +45 -0
  169. package/src/config/plugin.ts +79 -0
  170. package/src/config/tui-cwd.ts +5 -0
  171. package/src/config/tui-host-attention.ts +21 -0
  172. package/src/config/tui-migrate.ts +132 -0
  173. package/src/config/tui.ts +276 -0
  174. package/src/config/variable.ts +91 -0
  175. package/src/control-plane/adapters/index.ts +41 -0
  176. package/src/control-plane/adapters/worktree.ts +96 -0
  177. package/src/control-plane/dev/README.md +19 -0
  178. package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
  179. package/src/control-plane/types.ts +59 -0
  180. package/src/control-plane/util.ts +39 -0
  181. package/src/control-plane/workspace-adapter-runtime.ts +51 -0
  182. package/src/control-plane/workspace-context.ts +26 -0
  183. package/src/control-plane/workspace.ts +966 -0
  184. package/src/effect/app-node-builder-v1.ts +12 -0
  185. package/src/effect/app-runtime.ts +135 -0
  186. package/src/effect/bootstrap-runtime.ts +19 -0
  187. package/src/effect/bridge.ts +84 -0
  188. package/src/effect/config-service.ts +67 -0
  189. package/src/effect/instance-ref.ts +11 -0
  190. package/src/effect/instance-registry.ts +12 -0
  191. package/src/effect/instance-state.ts +69 -0
  192. package/src/effect/promise.ts +17 -0
  193. package/src/effect/run-service.ts +47 -0
  194. package/src/effect/runner.ts +217 -0
  195. package/src/effect/runtime-flags.ts +78 -0
  196. package/src/env/index.ts +41 -0
  197. package/src/event-manifest.ts +3 -0
  198. package/src/event-v2-bridge.ts +71 -0
  199. package/src/format/formatter.ts +404 -0
  200. package/src/format/index.ts +203 -0
  201. package/src/format/output-style.ts +84 -0
  202. package/src/git/auto-commit.ts +70 -0
  203. package/src/git/index.ts +348 -0
  204. package/src/hooks/index.ts +172 -0
  205. package/src/id/id.ts +80 -0
  206. package/src/ide/index.ts +54 -0
  207. package/src/image/image.ts +172 -0
  208. package/src/index.ts +229 -0
  209. package/src/installation/index.ts +336 -0
  210. package/src/lsp/client.ts +650 -0
  211. package/src/lsp/diagnostic.ts +29 -0
  212. package/src/lsp/language.ts +121 -0
  213. package/src/lsp/launch.ts +21 -0
  214. package/src/lsp/lsp.ts +507 -0
  215. package/src/lsp/server.ts +1983 -0
  216. package/src/markdown.d.ts +4 -0
  217. package/src/mcp/auth.ts +163 -0
  218. package/src/mcp/browser.ts +37 -0
  219. package/src/mcp/catalog.ts +170 -0
  220. package/src/mcp/index.ts +1007 -0
  221. package/src/mcp/oauth-callback.ts +194 -0
  222. package/src/mcp/oauth-provider.ts +259 -0
  223. package/src/node.ts +4 -0
  224. package/src/patch/index.ts +686 -0
  225. package/src/permission/arity.ts +163 -0
  226. package/src/permission/evaluate.ts +1 -0
  227. package/src/permission/index.ts +223 -0
  228. package/src/permission/sandbox.ts +150 -0
  229. package/src/plugin/azure.ts +26 -0
  230. package/src/plugin/cerebras.ts +11 -0
  231. package/src/plugin/cloudflare.ts +65 -0
  232. package/src/plugin/digitalocean.ts +325 -0
  233. package/src/plugin/index.ts +316 -0
  234. package/src/plugin/install.ts +439 -0
  235. package/src/plugin/loader.ts +237 -0
  236. package/src/plugin/meta.ts +188 -0
  237. package/src/plugin/modal/modal.ts +17 -0
  238. package/src/plugin/modal/models.ts +133 -0
  239. package/src/plugin/openai/README.md +31 -0
  240. package/src/plugin/openai/codex.ts +572 -0
  241. package/src/plugin/openai/ws-pool.ts +271 -0
  242. package/src/plugin/openai/ws.ts +383 -0
  243. package/src/plugin/pty-environment.ts +24 -0
  244. package/src/plugin/shared.ts +323 -0
  245. package/src/plugin/snowflake-cortex.ts +507 -0
  246. package/src/plugin/tui/internal.ts +10 -0
  247. package/src/plugin/tui/runtime.ts +1131 -0
  248. package/src/plugin/xai.ts +339 -0
  249. package/src/project/bootstrap-service.ts +9 -0
  250. package/src/project/bootstrap.ts +58 -0
  251. package/src/project/instance-context.ts +24 -0
  252. package/src/project/instance-runtime.ts +16 -0
  253. package/src/project/instance-store.ts +213 -0
  254. package/src/project/project.ts +483 -0
  255. package/src/project/vcs.ts +423 -0
  256. package/src/provider/auth.ts +229 -0
  257. package/src/provider/error.ts +195 -0
  258. package/src/provider/model-status.ts +8 -0
  259. package/src/provider/provider.ts +2399 -0
  260. package/src/provider/transform.ts +1856 -0
  261. package/src/question/index.ts +161 -0
  262. package/src/question/schema.ts +4 -0
  263. package/src/server/auth.ts +48 -0
  264. package/src/server/event.ts +10 -0
  265. package/src/server/global-lifecycle.ts +28 -0
  266. package/src/server/init-projectors.ts +3 -0
  267. package/src/server/mdns.ts +47 -0
  268. package/src/server/projectors.ts +1 -0
  269. package/src/server/proxy-util.ts +48 -0
  270. package/src/server/routes/instance/httpapi/AGENTS.md +39 -0
  271. package/src/server/routes/instance/httpapi/api.ts +97 -0
  272. package/src/server/routes/instance/httpapi/errors.ts +193 -0
  273. package/src/server/routes/instance/httpapi/groups/config.ts +65 -0
  274. package/src/server/routes/instance/httpapi/groups/control-plane.ts +35 -0
  275. package/src/server/routes/instance/httpapi/groups/control.ts +76 -0
  276. package/src/server/routes/instance/httpapi/groups/event.ts +29 -0
  277. package/src/server/routes/instance/httpapi/groups/experimental.ts +275 -0
  278. package/src/server/routes/instance/httpapi/groups/file.ts +185 -0
  279. package/src/server/routes/instance/httpapi/groups/global.ts +163 -0
  280. package/src/server/routes/instance/httpapi/groups/instance.ts +206 -0
  281. package/src/server/routes/instance/httpapi/groups/mcp.ts +156 -0
  282. package/src/server/routes/instance/httpapi/groups/metadata.ts +18 -0
  283. package/src/server/routes/instance/httpapi/groups/permission.ts +61 -0
  284. package/src/server/routes/instance/httpapi/groups/project-copy.ts +32 -0
  285. package/src/server/routes/instance/httpapi/groups/project.ts +93 -0
  286. package/src/server/routes/instance/httpapi/groups/provider.ts +101 -0
  287. package/src/server/routes/instance/httpapi/groups/pty.ts +172 -0
  288. package/src/server/routes/instance/httpapi/groups/query.ts +12 -0
  289. package/src/server/routes/instance/httpapi/groups/question.ts +74 -0
  290. package/src/server/routes/instance/httpapi/groups/session.ts +462 -0
  291. package/src/server/routes/instance/httpapi/groups/sync.ts +113 -0
  292. package/src/server/routes/instance/httpapi/groups/tui.ts +208 -0
  293. package/src/server/routes/instance/httpapi/groups/workspace.ts +141 -0
  294. package/src/server/routes/instance/httpapi/handlers/config.ts +34 -0
  295. package/src/server/routes/instance/httpapi/handlers/control-plane.ts +37 -0
  296. package/src/server/routes/instance/httpapi/handlers/control.ts +43 -0
  297. package/src/server/routes/instance/httpapi/handlers/event.ts +99 -0
  298. package/src/server/routes/instance/httpapi/handlers/experimental.ts +193 -0
  299. package/src/server/routes/instance/httpapi/handlers/file.ts +139 -0
  300. package/src/server/routes/instance/httpapi/handlers/global.ts +144 -0
  301. package/src/server/routes/instance/httpapi/handlers/instance.ts +110 -0
  302. package/src/server/routes/instance/httpapi/handlers/mcp.ts +111 -0
  303. package/src/server/routes/instance/httpapi/handlers/permission.ts +41 -0
  304. package/src/server/routes/instance/httpapi/handlers/project-copy.ts +83 -0
  305. package/src/server/routes/instance/httpapi/handlers/project.ts +63 -0
  306. package/src/server/routes/instance/httpapi/handlers/provider.ts +116 -0
  307. package/src/server/routes/instance/httpapi/handlers/pty.ts +273 -0
  308. package/src/server/routes/instance/httpapi/handlers/question.ts +54 -0
  309. package/src/server/routes/instance/httpapi/handlers/session-errors.ts +21 -0
  310. package/src/server/routes/instance/httpapi/handlers/session.ts +442 -0
  311. package/src/server/routes/instance/httpapi/handlers/sync.ts +89 -0
  312. package/src/server/routes/instance/httpapi/handlers/tui.ts +131 -0
  313. package/src/server/routes/instance/httpapi/handlers/workspace.ts +102 -0
  314. package/src/server/routes/instance/httpapi/lifecycle.ts +54 -0
  315. package/src/server/routes/instance/httpapi/middleware/authorization.ts +150 -0
  316. package/src/server/routes/instance/httpapi/middleware/compression.ts +64 -0
  317. package/src/server/routes/instance/httpapi/middleware/cors-vary.ts +29 -0
  318. package/src/server/routes/instance/httpapi/middleware/error.ts +43 -0
  319. package/src/server/routes/instance/httpapi/middleware/fence.ts +25 -0
  320. package/src/server/routes/instance/httpapi/middleware/instance-context.ts +43 -0
  321. package/src/server/routes/instance/httpapi/middleware/proxy.ts +131 -0
  322. package/src/server/routes/instance/httpapi/middleware/schema-error.ts +41 -0
  323. package/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +250 -0
  324. package/src/server/routes/instance/httpapi/public.ts +537 -0
  325. package/src/server/routes/instance/httpapi/server.ts +325 -0
  326. package/src/server/routes/instance/httpapi/websocket-tracker.ts +60 -0
  327. package/src/server/server.ts +227 -0
  328. package/src/server/shared/fence.ts +60 -0
  329. package/src/server/shared/pty-ticket.ts +15 -0
  330. package/src/server/shared/public-ui.ts +12 -0
  331. package/src/server/shared/tui-control.ts +28 -0
  332. package/src/server/shared/ui.ts +108 -0
  333. package/src/server/shared/workspace-routing.ts +45 -0
  334. package/src/server/tui-event.ts +1 -0
  335. package/src/session/auto-skill.ts +71 -0
  336. package/src/session/compaction.ts +637 -0
  337. package/src/session/instruction.ts +237 -0
  338. package/src/session/llm/AGENTS.md +90 -0
  339. package/src/session/llm/ai-sdk.ts +291 -0
  340. package/src/session/llm/native-request.ts +196 -0
  341. package/src/session/llm/native-runtime.ts +195 -0
  342. package/src/session/llm/request.ts +229 -0
  343. package/src/session/llm.ts +404 -0
  344. package/src/session/message-error.ts +14 -0
  345. package/src/session/message-v2.ts +737 -0
  346. package/src/session/message.ts +148 -0
  347. package/src/session/overflow.ts +69 -0
  348. package/src/session/processor.ts +718 -0
  349. package/src/session/prompt/anthropic.txt +105 -0
  350. package/src/session/prompt/beast.txt +147 -0
  351. package/src/session/prompt/build-switch.txt +5 -0
  352. package/src/session/prompt/codex.txt +79 -0
  353. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  354. package/src/session/prompt/default.txt +95 -0
  355. package/src/session/prompt/gemini.txt +155 -0
  356. package/src/session/prompt/gpt.txt +107 -0
  357. package/src/session/prompt/kimi.txt +95 -0
  358. package/src/session/prompt/meta.txt +65 -0
  359. package/src/session/prompt/plan-mode.txt +70 -0
  360. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  361. package/src/session/prompt/plan.txt +26 -0
  362. package/src/session/prompt/trinity.txt +97 -0
  363. package/src/session/prompt.ts +2151 -0
  364. package/src/session/rate-limit.ts +176 -0
  365. package/src/session/reminders.ts +92 -0
  366. package/src/session/retry.ts +211 -0
  367. package/src/session/revert.ts +136 -0
  368. package/src/session/run-state.ts +151 -0
  369. package/src/session/schema.ts +26 -0
  370. package/src/session/session.ts +1050 -0
  371. package/src/session/speculation.ts +88 -0
  372. package/src/session/status.ts +56 -0
  373. package/src/session/summary.ts +164 -0
  374. package/src/session/system.ts +152 -0
  375. package/src/session/todo.ts +74 -0
  376. package/src/session/tools.ts +626 -0
  377. package/src/session/watch.ts +86 -0
  378. package/src/share/session.ts +58 -0
  379. package/src/share/share-next.ts +371 -0
  380. package/src/skill/discovery.ts +140 -0
  381. package/src/skill/index.ts +430 -0
  382. package/src/skill/restriction.ts +62 -0
  383. package/src/snapshot/index.ts +807 -0
  384. package/src/sql.d.ts +4 -0
  385. package/src/storage/schema.ts +5 -0
  386. package/src/storage/storage.ts +327 -0
  387. package/src/sync/README.md +179 -0
  388. package/src/sync/schema.ts +11 -0
  389. package/src/temporary.ts +31 -0
  390. package/src/tool/apply_patch.ts +313 -0
  391. package/src/tool/apply_patch.txt +33 -0
  392. package/src/tool/audio-gen.ts +50 -0
  393. package/src/tool/code-mode.ts +310 -0
  394. package/src/tool/diff-context.ts +64 -0
  395. package/src/tool/edit.ts +749 -0
  396. package/src/tool/edit.txt +10 -0
  397. package/src/tool/external-directory.ts +49 -0
  398. package/src/tool/glob.ts +76 -0
  399. package/src/tool/glob.txt +6 -0
  400. package/src/tool/grep.ts +115 -0
  401. package/src/tool/grep.txt +8 -0
  402. package/src/tool/image-gen.ts +50 -0
  403. package/src/tool/invalid.ts +21 -0
  404. package/src/tool/json-schema.ts +164 -0
  405. package/src/tool/lsp.ts +113 -0
  406. package/src/tool/lsp.txt +24 -0
  407. package/src/tool/mcp-websearch.ts +96 -0
  408. package/src/tool/plan-enter.txt +14 -0
  409. package/src/tool/plan-exit.txt +13 -0
  410. package/src/tool/plan.ts +79 -0
  411. package/src/tool/question.ts +44 -0
  412. package/src/tool/question.txt +10 -0
  413. package/src/tool/read.ts +386 -0
  414. package/src/tool/read.txt +14 -0
  415. package/src/tool/registry.ts +478 -0
  416. package/src/tool/repomap.ts +287 -0
  417. package/src/tool/schema.ts +14 -0
  418. package/src/tool/shell/id.ts +19 -0
  419. package/src/tool/shell/prompt.ts +295 -0
  420. package/src/tool/shell/shell.txt +21 -0
  421. package/src/tool/shell.ts +684 -0
  422. package/src/tool/skill.ts +81 -0
  423. package/src/tool/skill.txt +5 -0
  424. package/src/tool/task.ts +461 -0
  425. package/src/tool/task.txt +21 -0
  426. package/src/tool/todo.ts +46 -0
  427. package/src/tool/todowrite.txt +44 -0
  428. package/src/tool/tool.ts +183 -0
  429. package/src/tool/truncate.ts +156 -0
  430. package/src/tool/truncation-dir.ts +4 -0
  431. package/src/tool/video-gen.ts +50 -0
  432. package/src/tool/webfetch.ts +192 -0
  433. package/src/tool/webfetch.txt +13 -0
  434. package/src/tool/websearch.ts +143 -0
  435. package/src/tool/websearch.txt +14 -0
  436. package/src/tool/write.ts +117 -0
  437. package/src/tool/write.txt +8 -0
  438. package/src/util/archive.ts +17 -0
  439. package/src/util/bom.ts +27 -0
  440. package/src/util/data-url.ts +9 -0
  441. package/src/util/defer.ts +10 -0
  442. package/src/util/effect-http-client.ts +11 -0
  443. package/src/util/error.ts +1 -0
  444. package/src/util/filesystem.ts +251 -0
  445. package/src/util/html.ts +8 -0
  446. package/src/util/iife.ts +3 -0
  447. package/src/util/lazy.ts +20 -0
  448. package/src/util/local-context.ts +25 -0
  449. package/src/util/locale.ts +2 -0
  450. package/src/util/media.ts +26 -0
  451. package/src/util/process.ts +177 -0
  452. package/src/util/proxy-env.ts +72 -0
  453. package/src/util/queue.ts +32 -0
  454. package/src/util/record.ts +1 -0
  455. package/src/util/repository.ts +232 -0
  456. package/src/util/rpc.ts +66 -0
  457. package/src/util/signal.ts +12 -0
  458. package/src/util/timeout.ts +13 -0
  459. package/src/util/token.ts +1 -0
  460. package/src/util/wildcard.ts +59 -0
  461. package/src/verify/verify.ts +106 -0
  462. package/src/worktree/index.ts +623 -0
  463. package/sst-env.d.ts +10 -0
  464. package/test/AGENTS.md +204 -0
  465. package/test/EFFECT_TEST_MIGRATION.md +169 -0
  466. package/test/account/repo.test.ts +355 -0
  467. package/test/account/service.test.ts +524 -0
  468. package/test/acp/config-option.test.ts +229 -0
  469. package/test/acp/content.test.ts +235 -0
  470. package/test/acp/directory.test.ts +188 -0
  471. package/test/acp/error.test.ts +67 -0
  472. package/test/acp/event.test.ts +751 -0
  473. package/test/acp/permission.test.ts +401 -0
  474. package/test/acp/service-session.test.ts +1374 -0
  475. package/test/acp/session.test.ts +201 -0
  476. package/test/acp/tool.test.ts +298 -0
  477. package/test/acp/usage.test.ts +318 -0
  478. package/test/agent/agent.test.ts +755 -0
  479. package/test/agent/plan-mode-subagent-bypass.test.ts +160 -0
  480. package/test/agent/plugin-agent-regression.test.ts +51 -0
  481. package/test/auth/auth.test.ts +75 -0
  482. package/test/background/job.test.ts +244 -0
  483. package/test/cli/account.test.ts +30 -0
  484. package/test/cli/acp/acp-test-client.ts +97 -0
  485. package/test/cli/acp/config-options.test.ts +103 -0
  486. package/test/cli/acp/helpers.ts +96 -0
  487. package/test/cli/acp/initialize-auth.test.ts +61 -0
  488. package/test/cli/acp/lifecycle.test.ts +118 -0
  489. package/test/cli/acp/prompt-content.test.ts +97 -0
  490. package/test/cli/acp/skills.test.ts +38 -0
  491. package/test/cli/cmd/tui/attention.test.ts +484 -0
  492. package/test/cli/effect-cmd-instance-als.test.ts +40 -0
  493. package/test/cli/error.test.ts +95 -0
  494. package/test/cli/github-action.test.ts +199 -0
  495. package/test/cli/github-remote.test.ts +90 -0
  496. package/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +623 -0
  497. package/test/cli/help/help-snapshots.test.ts +140 -0
  498. package/test/cli/import.test.ts +90 -0
  499. package/test/cli/mcp-add.test.ts +74 -0
  500. package/test/cli/plugin-auth-picker.test.ts +120 -0
  501. package/test/cli/run/entry.body.test.ts +536 -0
  502. package/test/cli/run/footer.menu.test.ts +43 -0
  503. package/test/cli/run/footer.view.test.tsx +1411 -0
  504. package/test/cli/run/footer.width.test.ts +35 -0
  505. package/test/cli/run/permission.shared.test.ts +144 -0
  506. package/test/cli/run/prompt.editor.test.ts +101 -0
  507. package/test/cli/run/prompt.shared.test.ts +101 -0
  508. package/test/cli/run/question.shared.test.ts +115 -0
  509. package/test/cli/run/run-process.test.ts +338 -0
  510. package/test/cli/run/runtime.boot.test.ts +283 -0
  511. package/test/cli/run/runtime.queue.test.ts +481 -0
  512. package/test/cli/run/runtime.stdin.test.ts +71 -0
  513. package/test/cli/run/runtime.test.ts +238 -0
  514. package/test/cli/run/scrollback.surface.test.ts +1092 -0
  515. package/test/cli/run/session-data.test.ts +593 -0
  516. package/test/cli/run/session-replay.test.ts +691 -0
  517. package/test/cli/run/session.shared.test.ts +247 -0
  518. package/test/cli/run/stream.test.ts +56 -0
  519. package/test/cli/run/stream.transport.test.ts +2363 -0
  520. package/test/cli/run/subagent-data.test.ts +547 -0
  521. package/test/cli/run/theme.test.ts +177 -0
  522. package/test/cli/run/variant.shared.test.ts +218 -0
  523. package/test/cli/serve/serve-process.test.ts +61 -0
  524. package/test/cli/smokes/read-only.test.ts +115 -0
  525. package/test/cli/tui/attach.test.ts +11 -0
  526. package/test/cli/tui/editor-context-zed.test.ts +379 -0
  527. package/test/cli/tui/editor-context.test.tsx +297 -0
  528. package/test/cli/tui/plugin-add.test.ts +110 -0
  529. package/test/cli/tui/plugin-install.test.ts +87 -0
  530. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  531. package/test/cli/tui/plugin-loader-entrypoint.test.ts +485 -0
  532. package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
  533. package/test/cli/tui/plugin-loader.test.ts +1332 -0
  534. package/test/cli/tui/plugin-toggle.test.ts +264 -0
  535. package/test/cli/tui/thread.test.ts +101 -0
  536. package/test/config/agent-color.test.ts +47 -0
  537. package/test/config/config.test.ts +2047 -0
  538. package/test/config/entry-name.test.ts +57 -0
  539. package/test/config/fixtures/empty-frontmatter.md +4 -0
  540. package/test/config/fixtures/frontmatter.md +28 -0
  541. package/test/config/fixtures/markdown-header.md +11 -0
  542. package/test/config/fixtures/no-frontmatter.md +1 -0
  543. package/test/config/fixtures/weird-model-id.md +13 -0
  544. package/test/config/lsp.test.ts +69 -0
  545. package/test/config/markdown.test.ts +228 -0
  546. package/test/config/plugin.test.ts +0 -0
  547. package/test/config/tui.test.ts +894 -0
  548. package/test/control-plane/adapters.test.ts +71 -0
  549. package/test/control-plane/workspace.test.ts +1717 -0
  550. package/test/effect/app-runtime-logger.test.ts +101 -0
  551. package/test/effect/config-service.test.ts +65 -0
  552. package/test/effect/instance-state.test.ts +392 -0
  553. package/test/effect/run-service.test.ts +89 -0
  554. package/test/effect/runner.test.ts +514 -0
  555. package/test/effect/runtime-flags.test.ts +374 -0
  556. package/test/event-manifest.test.ts +24 -0
  557. package/test/fake/account.ts +9 -0
  558. package/test/fake/auth.ts +8 -0
  559. package/test/fake/npm.ts +8 -0
  560. package/test/fake/provider.ts +83 -0
  561. package/test/fake/skill.ts +8 -0
  562. package/test/filesystem/filesystem.test.ts +318 -0
  563. package/test/fixture/agent-plugin.constants.ts +6 -0
  564. package/test/fixture/agent-plugin.ts +12 -0
  565. package/test/fixture/config.ts +24 -0
  566. package/test/fixture/db.ts +11 -0
  567. package/test/fixture/fixture.test.ts +26 -0
  568. package/test/fixture/fixture.ts +228 -0
  569. package/test/fixture/flag.ts +20 -0
  570. package/test/fixture/flock-worker.ts +72 -0
  571. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  572. package/test/fixture/mcp-lifecycle-stdio.ts +26 -0
  573. package/test/fixture/mcp-session-recovery.ts +50 -0
  574. package/test/fixture/plug-worker.ts +93 -0
  575. package/test/fixture/plugin-meta-worker.ts +19 -0
  576. package/test/fixture/plugin.ts +10 -0
  577. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  578. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  579. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  580. package/test/fixture/skills/index.json +6 -0
  581. package/test/fixture/tui-environment.tsx +32 -0
  582. package/test/fixture/tui-plugin.ts +355 -0
  583. package/test/fixture/tui-runtime.ts +56 -0
  584. package/test/fixture/tui-sdk.ts +82 -0
  585. package/test/fixture/workspace.ts +34 -0
  586. package/test/fixtures/recordings/session/native-anthropic-tool-loop.json +49 -0
  587. package/test/fixtures/recordings/session/native-openai-oauth-tool-loop.json +45 -0
  588. package/test/fixtures/recordings/session/native-zen-tool-loop.json +49 -0
  589. package/test/format/format.test.ts +235 -0
  590. package/test/git/auto-commit.test.ts +63 -0
  591. package/test/git/git.test.ts +179 -0
  592. package/test/ide/ide.test.ts +82 -0
  593. package/test/image/fixtures/picture-5mb-base64.png +0 -0
  594. package/test/image/image.test.ts +121 -0
  595. package/test/installation/installation.test.ts +240 -0
  596. package/test/lib/cli-process.ts +535 -0
  597. package/test/lib/effect.ts +177 -0
  598. package/test/lib/filesystem.ts +10 -0
  599. package/test/lib/llm-server.ts +779 -0
  600. package/test/lib/snapshot.ts +73 -0
  601. package/test/lib/test-provider.ts +37 -0
  602. package/test/lib/websocket.ts +46 -0
  603. package/test/lsp/client.test.ts +488 -0
  604. package/test/lsp/index.test.ts +231 -0
  605. package/test/lsp/jdtls-root.test.ts +459 -0
  606. package/test/lsp/launch.test.ts +22 -0
  607. package/test/lsp/lifecycle.test.ts +160 -0
  608. package/test/mcp/auth.test.ts +78 -0
  609. package/test/mcp/catalog.test.ts +107 -0
  610. package/test/mcp/headers.test.ts +102 -0
  611. package/test/mcp/lifecycle.test.ts +560 -0
  612. package/test/mcp/oauth-auto-connect.test.ts +300 -0
  613. package/test/mcp/oauth-browser.test.ts +225 -0
  614. package/test/mcp/oauth-callback.test.ts +114 -0
  615. package/test/mcp/oauth-provider.test.ts +102 -0
  616. package/test/mcp/session-recovery.test.ts +27 -0
  617. package/test/mcp/transport.test.ts +38 -0
  618. package/test/patch/patch.test.ts +384 -0
  619. package/test/permission/arity.test.ts +33 -0
  620. package/test/permission/next.test.ts +1174 -0
  621. package/test/permission/sandbox.test.ts +89 -0
  622. package/test/permission-task.test.ts +319 -0
  623. package/test/plugin/auth-override.test.ts +101 -0
  624. package/test/plugin/cerebras.test.ts +47 -0
  625. package/test/plugin/cloudflare.test.ts +25 -0
  626. package/test/plugin/codex.test.ts +463 -0
  627. package/test/plugin/install-concurrency.test.ts +140 -0
  628. package/test/plugin/install.test.ts +570 -0
  629. package/test/plugin/loader-shared.test.ts +1306 -0
  630. package/test/plugin/meta.test.ts +137 -0
  631. package/test/plugin/modal-models.test.ts +198 -0
  632. package/test/plugin/openai-rollout.test.ts +17 -0
  633. package/test/plugin/openai-ws.test.ts +909 -0
  634. package/test/plugin/shared.test.ts +88 -0
  635. package/test/plugin/snowflake-cortex.test.ts +278 -0
  636. package/test/plugin/trigger.test.ts +108 -0
  637. package/test/plugin/workspace-adapter.test.ts +111 -0
  638. package/test/plugin/xai.test.ts +585 -0
  639. package/test/preload.ts +92 -0
  640. package/test/project/instance-bootstrap.test.ts +115 -0
  641. package/test/project/instance.test.ts +248 -0
  642. package/test/project/migrate-global.test.ts +168 -0
  643. package/test/project/project-directory.test.ts +202 -0
  644. package/test/project/project.test.ts +808 -0
  645. package/test/project/vcs.test.ts +335 -0
  646. package/test/project/worktree-remove.test.ts +128 -0
  647. package/test/project/worktree.test.ts +324 -0
  648. package/test/provider/amazon-bedrock.test.ts +361 -0
  649. package/test/provider/cf-ai-gateway-e2e.test.ts +320 -0
  650. package/test/provider/digitalocean.test.ts +124 -0
  651. package/test/provider/error.test.ts +24 -0
  652. package/test/provider/gitlab-duo.test.ts +412 -0
  653. package/test/provider/header-timeout.test.ts +234 -0
  654. package/test/provider/model-status.test.ts +61 -0
  655. package/test/provider/provider.test.ts +2118 -0
  656. package/test/provider/transform.test.ts +5682 -0
  657. package/test/question/question.test.ts +459 -0
  658. package/test/server/AGENTS.md +15 -0
  659. package/test/server/auth.test.ts +59 -0
  660. package/test/server/global-bus.ts +31 -0
  661. package/test/server/global-session-list.test.ts +108 -0
  662. package/test/server/httpapi-authorization.test.ts +174 -0
  663. package/test/server/httpapi-compression.test.ts +151 -0
  664. package/test/server/httpapi-config.test.ts +110 -0
  665. package/test/server/httpapi-control-plane.test.ts +63 -0
  666. package/test/server/httpapi-cors-vary.test.ts +63 -0
  667. package/test/server/httpapi-cors.test.ts +122 -0
  668. package/test/server/httpapi-error-middleware.test.ts +101 -0
  669. package/test/server/httpapi-event.test.ts +94 -0
  670. package/test/server/httpapi-exercise/assertions.ts +64 -0
  671. package/test/server/httpapi-exercise/backend.ts +144 -0
  672. package/test/server/httpapi-exercise/dsl.ts +210 -0
  673. package/test/server/httpapi-exercise/environment.ts +40 -0
  674. package/test/server/httpapi-exercise/index.ts +1802 -0
  675. package/test/server/httpapi-exercise/report.ts +66 -0
  676. package/test/server/httpapi-exercise/routing.ts +96 -0
  677. package/test/server/httpapi-exercise/runner.ts +267 -0
  678. package/test/server/httpapi-exercise/runtime.ts +52 -0
  679. package/test/server/httpapi-exercise/types.ts +127 -0
  680. package/test/server/httpapi-experimental.test.ts +298 -0
  681. package/test/server/httpapi-file.test.ts +83 -0
  682. package/test/server/httpapi-global.test.ts +90 -0
  683. package/test/server/httpapi-instance-context.test.ts +352 -0
  684. package/test/server/httpapi-instance-route-auth.test.ts +81 -0
  685. package/test/server/httpapi-instance.test.ts +265 -0
  686. package/test/server/httpapi-layer.ts +33 -0
  687. package/test/server/httpapi-listen.test.ts +465 -0
  688. package/test/server/httpapi-mcp-oauth.test.ts +73 -0
  689. package/test/server/httpapi-mcp.test.ts +223 -0
  690. package/test/server/httpapi-mdns.test.ts +79 -0
  691. package/test/server/httpapi-promptasync-context.test.ts +212 -0
  692. package/test/server/httpapi-provider.test.ts +401 -0
  693. package/test/server/httpapi-pty.test.ts +299 -0
  694. package/test/server/httpapi-public-openapi.test.ts +350 -0
  695. package/test/server/httpapi-query-schema-drift.test.ts +330 -0
  696. package/test/server/httpapi-reference.test.ts +63 -0
  697. package/test/server/httpapi-schema-error-body.test.ts +166 -0
  698. package/test/server/httpapi-sdk.test.ts +913 -0
  699. package/test/server/httpapi-session.test.ts +1090 -0
  700. package/test/server/httpapi-sync.test.ts +149 -0
  701. package/test/server/httpapi-ui.test.ts +457 -0
  702. package/test/server/httpapi-v2-location.test.ts +126 -0
  703. package/test/server/httpapi-v2-pty.test.ts +250 -0
  704. package/test/server/httpapi-workspace-routing.test.ts +552 -0
  705. package/test/server/httpapi-workspace.test.ts +506 -0
  706. package/test/server/negative-tokens-regression.test.ts +84 -0
  707. package/test/server/project-copy.test.ts +127 -0
  708. package/test/server/project-init-git.test.ts +118 -0
  709. package/test/server/proxy-util.test.ts +113 -0
  710. package/test/server/sdk-error-shape.test.ts +81 -0
  711. package/test/server/sdk-v1-smoke.test.ts +57 -0
  712. package/test/server/session-actions.test.ts +110 -0
  713. package/test/server/session-diff-missing-patch.test.ts +97 -0
  714. package/test/server/session-list.test.ts +302 -0
  715. package/test/server/session-messages.test.ts +180 -0
  716. package/test/server/session-select.test.ts +67 -0
  717. package/test/server/workspace-proxy.test.ts +181 -0
  718. package/test/server/workspace-routing.test.ts +101 -0
  719. package/test/server/worktree-endpoint-repro.test.ts +307 -0
  720. package/test/session/compaction.test.ts +1975 -0
  721. package/test/session/instruction.test.ts +264 -0
  722. package/test/session/llm-native-recorded.test.ts +413 -0
  723. package/test/session/llm-native.test.ts +761 -0
  724. package/test/session/llm.test.ts +2249 -0
  725. package/test/session/message-v2.test.ts +1729 -0
  726. package/test/session/messages-pagination.test.ts +1057 -0
  727. package/test/session/processor-effect.test.ts +1171 -0
  728. package/test/session/prompt.test.ts +2470 -0
  729. package/test/session/retry.test.ts +521 -0
  730. package/test/session/revert-compact.test.ts +683 -0
  731. package/test/session/schema-decoding.test.ts +313 -0
  732. package/test/session/session-schema.test.ts +78 -0
  733. package/test/session/session.test.ts +285 -0
  734. package/test/session/snapshot-tool-race.test.ts +189 -0
  735. package/test/session/structured-output-integration.test.ts +235 -0
  736. package/test/session/structured-output.test.ts +387 -0
  737. package/test/session/system.test.ts +168 -0
  738. package/test/share/share-next.test.ts +324 -0
  739. package/test/skill/discovery.test.ts +186 -0
  740. package/test/skill/restriction.test.ts +57 -0
  741. package/test/skill/skill.test.ts +588 -0
  742. package/test/snapshot/snapshot.test.ts +1218 -0
  743. package/test/storage/storage.test.ts +297 -0
  744. package/test/tool/__snapshots__/parameters.test.ts.snap +466 -0
  745. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  746. package/test/tool/apply_patch.test.ts +529 -0
  747. package/test/tool/code-mode-integration.test.ts +331 -0
  748. package/test/tool/code-mode.test.ts +730 -0
  749. package/test/tool/edit.test.ts +576 -0
  750. package/test/tool/external-directory.test.ts +156 -0
  751. package/test/tool/fixtures/large-image.png +0 -0
  752. package/test/tool/fixtures/models-api.json +172692 -0
  753. package/test/tool/glob.test.ts +132 -0
  754. package/test/tool/grep.test.ts +223 -0
  755. package/test/tool/lsp.test.ts +184 -0
  756. package/test/tool/parameters.test.ts +290 -0
  757. package/test/tool/question.test.ts +133 -0
  758. package/test/tool/read.test.ts +608 -0
  759. package/test/tool/registry.test.ts +572 -0
  760. package/test/tool/shell.test.ts +1199 -0
  761. package/test/tool/skill.test.ts +134 -0
  762. package/test/tool/task.test.ts +1101 -0
  763. package/test/tool/tool-define.test.ts +154 -0
  764. package/test/tool/truncation.test.ts +266 -0
  765. package/test/tool/webfetch.test.ts +119 -0
  766. package/test/tool/websearch.test.ts +100 -0
  767. package/test/tool/write.test.ts +281 -0
  768. package/test/util/data-url.test.ts +14 -0
  769. package/test/util/error.test.ts +16 -0
  770. package/test/util/filesystem.test.ts +656 -0
  771. package/test/util/glob.test.ts +164 -0
  772. package/test/util/html.test.ts +15 -0
  773. package/test/util/iife.test.ts +36 -0
  774. package/test/util/lazy.test.ts +50 -0
  775. package/test/util/module.test.ts +59 -0
  776. package/test/util/process.test.ts +128 -0
  777. package/test/util/repository.test.ts +93 -0
  778. package/test/util/timeout.test.ts +21 -0
  779. package/test/util/wildcard.test.ts +90 -0
  780. package/test/v2/session-message-updater.test.ts +269 -0
  781. package/test/verify/verify.test.ts +61 -0
  782. package/tsconfig.json +16 -0
@@ -0,0 +1,2151 @@
1
+ import { LayerNode } from "@msrfteam/core/effect/layer-node"
2
+ import { PermissionV1 } from "@msrfteam/core/v1/permission"
3
+ import path from "path"
4
+ import { SessionV1 } from "@msrfteam/core/v1/session"
5
+ import os from "os"
6
+ import { SessionID, MessageID, PartID } from "./schema"
7
+ import { MessageV2 } from "./message-v2"
8
+ import { SessionRetry } from "./retry"
9
+ import { SessionRevert } from "./revert"
10
+ import { Session } from "./session"
11
+ import { Agent } from "../agent/agent"
12
+ import { Provider } from "@/provider/provider"
13
+ import { ProviderTransform } from "@/provider/transform"
14
+ import { estimateInputTokens } from "./overflow"
15
+
16
+ import { type Tool as AITool, tool, jsonSchema } from "ai"
17
+ import type { JSONSchema7 } from "@ai-sdk/provider"
18
+ import { SessionCompaction } from "./compaction"
19
+ import { SystemPrompt } from "./system"
20
+ import { repomapFromContext } from "../tool/repomap"
21
+ import { getDiffContextImpl } from "../tool/diff-context"
22
+ import { Instruction } from "./instruction"
23
+ import { Plugin } from "../plugin"
24
+ import { MAX_STEPS_PROMPT } from "@msrfteam/core/session/runner/max-steps"
25
+ import { ToolRegistry } from "@/tool/registry"
26
+ import { MCP } from "../mcp"
27
+ import { LSP } from "@/lsp/lsp"
28
+ import { ulid } from "ulid"
29
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
30
+ import { CrossSpawnSpawner } from "@msrfteam/core/cross-spawn-spawner"
31
+ import * as Stream from "effect/Stream"
32
+ import { Command } from "../command"
33
+ import { pathToFileURL, fileURLToPath } from "url"
34
+ import { Config } from "@/config/config"
35
+ import { ConfigMarkdown } from "@/config/markdown"
36
+ import { SessionSummary } from "./summary"
37
+ import { NamedError } from "@msrfteam/core/util/error"
38
+ import { SessionProcessor } from "./processor"
39
+ import { Tool } from "@/tool/tool"
40
+ import { Permission } from "@/permission"
41
+ import { SessionStatus } from "./status"
42
+ import { LLM } from "./llm"
43
+ import { Shell } from "@msrfteam/core/shell"
44
+ import { ShellID } from "@/tool/shell/id"
45
+ import { FSUtil } from "@msrfteam/core/fs-util"
46
+ import { Ripgrep } from "@msrfteam/core/ripgrep"
47
+ import { Git } from "@msrfteam/core/git"
48
+ import { AppProcess } from "@msrfteam/core/process"
49
+ import { Truncate } from "@/tool/truncate"
50
+ import { Image } from "@/image/image"
51
+ import { decodeDataUrl } from "@/util/data-url"
52
+ import { Process } from "@/util/process"
53
+ import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
54
+ import { detectSkills, dedupeSessionSkills } from "./auto-skill"
55
+ import { Skill } from "../skill"
56
+ import { InstanceState } from "@/effect/instance-state"
57
+ import { TaskTool, type TaskPromptOps } from "@/tool/task"
58
+ import { SessionRunState } from "./run-state"
59
+ import { RuntimeFlags } from "@/effect/runtime-flags"
60
+ import { GitAutoCommit } from "@/git/auto-commit"
61
+ import { Hooks } from "@/hooks"
62
+ import { SkillRestriction } from "@/skill/restriction"
63
+ import { EventV2Bridge } from "@/event-v2-bridge"
64
+ import { Database } from "@msrfteam/core/database/database"
65
+ import { ModelV2 } from "@msrfteam/core/model"
66
+ import { ProviderV2 } from "@msrfteam/core/provider"
67
+ import { eq } from "drizzle-orm"
68
+ import { SessionTable } from "@msrfteam/core/session/sql"
69
+ import { SessionReminders } from "./reminders"
70
+ import { SessionTools } from "./tools"
71
+ import { LLMEvent } from "@msrfteam/llm"
72
+ import { RateLimit } from "./rate-limit"
73
+ import { Token } from "@/util/token"
74
+
75
+ // @ts-ignore
76
+ globalThis.AI_SDK_LOG_WARNINGS = false
77
+
78
+ const decodeMessageInfo = Schema.decodeUnknownExit(SessionV1.Info)
79
+ const decodeMessagePart = Schema.decodeUnknownExit(SessionV1.Part)
80
+ const MAX_MCP_RESOURCE_BLOB_BYTES = 10 * 1024 * 1024
81
+ const SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES = new Set([
82
+ "application/pdf",
83
+ "image/gif",
84
+ "image/jpeg",
85
+ "image/png",
86
+ "image/webp",
87
+ ])
88
+
89
+ const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
90
+
91
+ IMPORTANT:
92
+ - You MUST call this tool exactly once at the end of your response
93
+ - The input must be valid JSON matching the required schema
94
+ - Complete all necessary research and tool calls BEFORE calling this tool
95
+ - This tool provides your final answer - no further actions are taken after calling it`
96
+
97
+ 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.`
98
+
99
+ function mcpResourceBase64Size(value: string) {
100
+ const trimmed = value.replace(/\s/g, "")
101
+ const padding = trimmed.endsWith("==") ? 2 : trimmed.endsWith("=") ? 1 : 0
102
+ return Math.max(0, Math.floor((trimmed.length * 3) / 4) - padding)
103
+ }
104
+
105
+ function formatMcpResourceBytes(value: number) {
106
+ if (value < 1024) return `${value} B`
107
+ if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`
108
+ return `${Math.ceil(value / (1024 * 1024))} MB`
109
+ }
110
+
111
+ function isOrphanedInterruptedTool(part: SessionV1.ToolPart) {
112
+ // cleanup() marks abandoned tool_use blocks this way after retries/aborts.
113
+ // They are not pending work and must not trigger an assistant-prefill request.
114
+ return part.state.status === "error" && part.state.metadata?.interrupted === true
115
+ }
116
+
117
+ const AUTH_MESSAGE =
118
+ /401|403|unauthorized|unauthorised|forbidden|invalid[_-\s]?api[_-\s]?key|invalid[_-\s]?auth|authentication|access denied|account.*(suspend|deactiv|disabled)/i
119
+
120
+ // Provider reliability: auth (401/403), rate-limit/overload, and context overflow errors are
121
+ // eligible for automatic model fallback instead of killing the turn.
122
+ function isFallbackEligible(error: SessionRetry.Err): boolean {
123
+ // Context overflow errors are eligible for fallback to a larger-context model
124
+ if (SessionV1.ContextOverflowError.isInstance(error)) return true
125
+ if (SessionV1.APIError.isInstance(error)) {
126
+ const status = error.data.statusCode
127
+ if (status === 401 || status === 403) return true
128
+ if (SessionRetry.retryable(error, "") !== undefined) return true
129
+ if (typeof error.data.message === "string" && AUTH_MESSAGE.test(error.data.message)) return true
130
+ if (typeof error.data.responseBody === "string" && AUTH_MESSAGE.test(error.data.responseBody)) return true
131
+ return false
132
+ }
133
+ try {
134
+ return AUTH_MESSAGE.test(JSON.stringify(error))
135
+ } catch {
136
+ return false
137
+ }
138
+ }
139
+
140
+ // Pinned files: absolute paths stored in session metadata and
141
+ // injected as file parts into every new user message.
142
+ const PINNED_KEY = "mervex.pinnedFiles"
143
+
144
+ function readPinned(metadata: { readonly [key: string]: any } | undefined): string[] {
145
+ const value = metadata?.[PINNED_KEY]
146
+ if (!Array.isArray(value)) return []
147
+ return value.filter((item): item is string => typeof item === "string")
148
+ }
149
+
150
+ function pinnedFileParts(session: Session.Info, parts: PromptInput["parts"]) {
151
+ const existing = new Set<string>()
152
+ for (const part of parts) {
153
+ if (part.type === "file" && part.url.startsWith("file:")) {
154
+ existing.add(path.normalize(fileURLToPath(part.url)))
155
+ }
156
+ }
157
+ return readPinned(session.metadata)
158
+ .filter((file) => !existing.has(path.normalize(file)))
159
+ .map((file) => ({
160
+ type: "file" as const,
161
+ mime: "text/plain",
162
+ filename: path.basename(file),
163
+ url: pathToFileURL(file).href,
164
+ }))
165
+ }
166
+
167
+ export interface Interface {
168
+ readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
169
+ readonly prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
170
+ readonly loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts>
171
+ readonly shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
172
+ readonly command: (input: CommandInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
173
+ readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
174
+ }
175
+
176
+ export class Service extends Context.Service<Service, Interface>()("@mervex/SessionPrompt") {}
177
+
178
+ const layer = Layer.effect(
179
+ Service,
180
+ Effect.gen(function* () {
181
+ const status = yield* SessionStatus.Service
182
+ const sessions = yield* Session.Service
183
+ const agents = yield* Agent.Service
184
+ const provider = yield* Provider.Service
185
+ const processor = yield* SessionProcessor.Service
186
+ const compaction = yield* SessionCompaction.Service
187
+ const plugin = yield* Plugin.Service
188
+ const commands = yield* Command.Service
189
+ const config = yield* Config.Service
190
+ const permission = yield* Permission.Service
191
+ const fsys = yield* FSUtil.Service
192
+ const ripgrep = yield* Ripgrep.Service
193
+ const git = yield* Git.Service
194
+ const mcp = yield* MCP.Service
195
+ const lsp = yield* LSP.Service
196
+ const registry = yield* ToolRegistry.Service
197
+ const truncate = yield* Truncate.Service
198
+ const image = yield* Image.Service
199
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
200
+ const scope = yield* Scope.Scope
201
+ const instruction = yield* Instruction.Service
202
+ const state = yield* SessionRunState.Service
203
+ const revert = yield* SessionRevert.Service
204
+ const summary = yield* SessionSummary.Service
205
+ const sys = yield* SystemPrompt.Service
206
+ const llm = yield* LLM.Service
207
+ const events = yield* EventV2Bridge.Service
208
+ const flags = yield* RuntimeFlags.Service
209
+ const gitAutoCommit = yield* GitAutoCommit.Service
210
+ const hooks = yield* Hooks.Service
211
+ const skillRestriction = yield* SkillRestriction.Service
212
+ const skill = yield* Skill.Service
213
+ const database = yield* Database.Service
214
+ const rateLimit = yield* RateLimit.Service
215
+ const { db } = database
216
+ const ops = Effect.fn("SessionPrompt.ops")(function* () {
217
+ return {
218
+ cancel: (sessionID: SessionID) => cancel(sessionID),
219
+ resolvePromptParts: (template: string) => resolvePromptParts(template),
220
+ prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)),
221
+ } satisfies TaskPromptOps
222
+ })
223
+
224
+ const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
225
+ yield* Effect.logInfo("cancel", { "session.id": sessionID })
226
+ yield* state.cancel(sessionID)
227
+ })
228
+
229
+ const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
230
+ const ctx = yield* InstanceState.context
231
+ const parts: Types.DeepMutable<PromptInput["parts"]> = [{ type: "text", text: template }]
232
+ const files = ConfigMarkdown.files(template)
233
+ const seen = new Set<string>()
234
+ yield* Effect.forEach(
235
+ files,
236
+ Effect.fnUntraced(function* (match) {
237
+ const name = match[1]
238
+ if (!name) return
239
+ if (seen.has(name)) return
240
+ seen.add(name)
241
+
242
+ const filepath = name.startsWith("~/")
243
+ ? path.join(os.homedir(), name.slice(2))
244
+ : path.resolve(ctx.worktree, name)
245
+
246
+ const info = yield* fsys.stat(filepath).pipe(Effect.option)
247
+ if (Option.isNone(info)) {
248
+ const found = yield* agents.get(name)
249
+ if (found) parts.push({ type: "agent", name: found.name })
250
+ return
251
+ }
252
+ const stat = info.value
253
+ parts.push({
254
+ type: "file",
255
+ url: pathToFileURL(filepath).href,
256
+ filename: name,
257
+ mime: stat.type === "Directory" ? "application/x-directory" : "text/plain",
258
+ })
259
+ }),
260
+ { concurrency: "unbounded", discard: true },
261
+ )
262
+ return parts
263
+ })
264
+
265
+ const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
266
+ session: Session.Info
267
+ history: SessionV1.WithParts[]
268
+ providerID: ProviderV2.ID
269
+ modelID: ModelV2.ID
270
+ }) {
271
+ yield* Effect.logDebug("ensureTitle called", { sessionID: input.session.id, title: input.session.title })
272
+ if (input.session.parentID) return
273
+ if (!Session.isDefaultTitle(input.session.title)) return
274
+
275
+ const real = (m: SessionV1.WithParts) =>
276
+ m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
277
+ const idx = input.history.findIndex(real)
278
+ if (idx === -1) return
279
+ if (input.history.filter(real).length !== 1) return
280
+
281
+ const context = input.history.slice(0, idx + 1)
282
+ const firstUser = context[idx]
283
+ if (!firstUser || firstUser.info.role !== "user") return
284
+ const firstInfo = firstUser.info
285
+
286
+ const subtasks = firstUser.parts.filter((p): p is SessionV1.SubtaskPart => p.type === "subtask")
287
+ const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
288
+
289
+ const ag = yield* agents.get("title")
290
+ if (!ag) {
291
+ yield* Effect.logWarning("title agent not found, skipping title generation")
292
+ return
293
+ }
294
+ const mdl = ag.model
295
+ ? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
296
+ : ((yield* provider.getSmallModel(input.providerID)) ??
297
+ (yield* provider.getModel(input.providerID, input.modelID)))
298
+ if (!mdl) {
299
+ yield* Effect.logWarning("no model available for title generation")
300
+ return
301
+ }
302
+ yield* Effect.logDebug("generating title", { provider: input.providerID, model: mdl.id })
303
+ const msgs = onlySubtasks
304
+ ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
305
+ : yield* MessageV2.toModelMessagesEffect(context, mdl)
306
+ const text = yield* llm
307
+ .stream({
308
+ agent: ag,
309
+ user: firstInfo,
310
+ system: [],
311
+ small: true,
312
+ tools: {},
313
+ model: mdl,
314
+ sessionID: input.session.id,
315
+ retries: 2,
316
+ messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
317
+ })
318
+ .pipe(
319
+ Stream.filter(LLMEvent.is.textDelta),
320
+ Stream.map((e) => e.text),
321
+ Stream.mkString,
322
+ Effect.catchCause((cause) =>
323
+ Effect.logError("title generation LLM failed", { error: Cause.squash(cause) }).pipe(
324
+ Effect.map(() => ""),
325
+ ),
326
+ ),
327
+ )
328
+ if (!text) return
329
+ const cleaned = text
330
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
331
+ .split("\n")
332
+ .map((line) => line.trim())
333
+ .find((line) => line.length > 0)
334
+ if (!cleaned) return
335
+ const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
336
+ yield* Effect.logDebug("title generated", { title: t })
337
+ yield* sessions
338
+ .setTitle({ sessionID: input.session.id, title: t })
339
+ .pipe(Effect.catchCause((cause) => Effect.logError("failed to generate title", { error: Cause.squash(cause) })))
340
+ })
341
+
342
+ const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
343
+ task: SessionV1.SubtaskPart
344
+ model: Provider.Model
345
+ lastUser: SessionV1.User
346
+ sessionID: SessionID
347
+ session: Session.Info
348
+ msgs: SessionV1.WithParts[]
349
+ }) {
350
+ const { task, model, lastUser, sessionID, session, msgs } = input
351
+ const ctx = yield* InstanceState.context
352
+ const promptOps = yield* ops()
353
+ const { task: taskTool } = yield* registry.named()
354
+ const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
355
+ const assistantMessage: SessionV1.Assistant = yield* sessions.updateMessage({
356
+ id: MessageID.ascending(),
357
+ role: "assistant",
358
+ parentID: lastUser.id,
359
+ sessionID,
360
+ mode: task.agent,
361
+ agent: task.agent,
362
+ variant: lastUser.model.variant,
363
+ path: { cwd: ctx.directory, root: ctx.worktree },
364
+ cost: 0,
365
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
366
+ modelID: taskModel.id,
367
+ providerID: taskModel.providerID,
368
+ time: { created: Date.now() },
369
+ })
370
+ let part: SessionV1.ToolPart = yield* sessions.updatePart({
371
+ id: PartID.ascending(),
372
+ messageID: assistantMessage.id,
373
+ sessionID: assistantMessage.sessionID,
374
+ type: "tool",
375
+ callID: ulid(),
376
+ tool: TaskTool.id,
377
+ state: {
378
+ status: "running",
379
+ input: {
380
+ prompt: task.prompt,
381
+ description: task.description,
382
+ subagent_type: task.agent,
383
+ command: task.command,
384
+ },
385
+ time: { start: Date.now() },
386
+ },
387
+ })
388
+ const taskArgs = {
389
+ prompt: task.prompt,
390
+ description: task.description,
391
+ subagent_type: task.agent,
392
+ command: task.command,
393
+ }
394
+ yield* plugin.trigger(
395
+ "tool.execute.before",
396
+ { tool: TaskTool.id, sessionID, callID: part.id },
397
+ { args: taskArgs },
398
+ )
399
+
400
+ const taskAgent = yield* agents.get(task.agent)
401
+ if (!taskAgent) {
402
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
403
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
404
+ const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` })
405
+ yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
406
+ throw error
407
+ }
408
+
409
+ let error: Error | undefined
410
+ const taskAbort = new AbortController()
411
+ const result = yield* taskTool
412
+ .execute(taskArgs, {
413
+ agent: task.agent,
414
+ messageID: assistantMessage.id,
415
+ sessionID,
416
+ abort: taskAbort.signal,
417
+ callID: part.callID,
418
+ extra: { bypassAgentCheck: true, promptOps },
419
+ messages: msgs,
420
+ metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
421
+ Effect.gen(function* () {
422
+ part = yield* sessions.updatePart({
423
+ ...part,
424
+ type: "tool",
425
+ state: { ...part.state, ...val },
426
+ } satisfies SessionV1.ToolPart)
427
+ }),
428
+ ask: (req: any) =>
429
+ permission
430
+ .ask({
431
+ ...req,
432
+ sessionID,
433
+ ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
434
+ })
435
+ .pipe(Effect.orDie),
436
+ })
437
+ .pipe(
438
+ Effect.catchCause((cause) => {
439
+ const defect = Cause.squash(cause)
440
+ error = defect instanceof Error ? defect : new Error(String(defect))
441
+ return Effect.logError("subtask execution failed", {
442
+ error,
443
+ agent: task.agent,
444
+ description: task.description,
445
+ })
446
+ }),
447
+ Effect.onInterrupt(() =>
448
+ Effect.gen(function* () {
449
+ taskAbort.abort()
450
+ assistantMessage.finish = "tool-calls"
451
+ assistantMessage.time.completed = Date.now()
452
+ yield* sessions.updateMessage(assistantMessage)
453
+ if (part.state.status === "running") {
454
+ yield* sessions.updatePart({
455
+ ...part,
456
+ state: {
457
+ status: "error",
458
+ error: "Cancelled",
459
+ time: { start: part.state.time.start, end: Date.now() },
460
+ metadata: part.state.metadata,
461
+ input: part.state.input,
462
+ },
463
+ } satisfies SessionV1.ToolPart)
464
+ }
465
+ }),
466
+ ),
467
+ )
468
+
469
+ const attachments = result?.attachments?.map((attachment) => ({
470
+ ...attachment,
471
+ id: PartID.ascending(),
472
+ sessionID,
473
+ messageID: assistantMessage.id,
474
+ }))
475
+
476
+ yield* plugin.trigger(
477
+ "tool.execute.after",
478
+ { tool: TaskTool.id, sessionID, callID: part.id, args: taskArgs },
479
+ result,
480
+ )
481
+
482
+ assistantMessage.finish = "tool-calls"
483
+ assistantMessage.time.completed = Date.now()
484
+ yield* sessions.updateMessage(assistantMessage)
485
+
486
+ if (result && part.state.status === "running") {
487
+ yield* sessions.updatePart({
488
+ ...part,
489
+ state: {
490
+ status: "completed",
491
+ input: part.state.input,
492
+ title: result.title,
493
+ metadata: result.metadata,
494
+ output: result.output,
495
+ attachments,
496
+ time: { ...part.state.time, end: Date.now() },
497
+ },
498
+ } satisfies SessionV1.ToolPart)
499
+ }
500
+
501
+ if (!result) {
502
+ yield* sessions.updatePart({
503
+ ...part,
504
+ state: {
505
+ status: "error",
506
+ error: error ? `Tool execution failed: ${error.message}` : "Tool execution failed",
507
+ time: {
508
+ start: part.state.status === "running" ? part.state.time.start : Date.now(),
509
+ end: Date.now(),
510
+ },
511
+ metadata: part.state.status === "pending" ? undefined : part.state.metadata,
512
+ input: part.state.input,
513
+ },
514
+ } satisfies SessionV1.ToolPart)
515
+ }
516
+
517
+ if (!task.command) return
518
+
519
+ const summaryUserMsg: SessionV1.User = {
520
+ id: MessageID.ascending(),
521
+ sessionID,
522
+ role: "user",
523
+ time: { created: Date.now() },
524
+ agent: lastUser.agent,
525
+ model: lastUser.model,
526
+ }
527
+ yield* sessions.updateMessage(summaryUserMsg)
528
+ yield* sessions.updatePart({
529
+ id: PartID.ascending(),
530
+ messageID: summaryUserMsg.id,
531
+ sessionID,
532
+ type: "text",
533
+ text: "Summarize the task tool output above and continue with your task.",
534
+ synthetic: true,
535
+ } satisfies SessionV1.TextPart)
536
+ })
537
+
538
+ const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput, ready?: Latch.Latch) {
539
+ return yield* Effect.uninterruptibleMask((restore) =>
540
+ Effect.gen(function* () {
541
+ const markReady = ready ? ready.open.pipe(Effect.asVoid) : Effect.void
542
+ const { msg, part, cwd } = yield* Effect.gen(function* () {
543
+ const ctx = yield* InstanceState.context
544
+ const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
545
+ if (session.revert) {
546
+ yield* revert.cleanup(session)
547
+ }
548
+ const agent = yield* agents.get(input.agent)
549
+ if (!agent) {
550
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
551
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
552
+ const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` })
553
+ yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
554
+ throw error
555
+ }
556
+ const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
557
+ const userMsg: SessionV1.User = {
558
+ id: input.messageID ?? MessageID.ascending(),
559
+ sessionID: input.sessionID,
560
+ time: { created: Date.now() },
561
+ role: "user",
562
+ agent: input.agent,
563
+ model: { providerID: model.providerID, modelID: model.modelID },
564
+ }
565
+ yield* sessions.updateMessage(userMsg)
566
+ const userPart: SessionV1.Part = {
567
+ type: "text",
568
+ id: PartID.ascending(),
569
+ messageID: userMsg.id,
570
+ sessionID: input.sessionID,
571
+ text: "The following tool was executed by the user",
572
+ synthetic: true,
573
+ }
574
+ yield* sessions.updatePart(userPart)
575
+
576
+ const msg: SessionV1.Assistant = {
577
+ id: MessageID.ascending(),
578
+ sessionID: input.sessionID,
579
+ parentID: userMsg.id,
580
+ mode: input.agent,
581
+ agent: input.agent,
582
+ cost: 0,
583
+ path: { cwd: ctx.directory, root: ctx.worktree },
584
+ time: { created: Date.now() },
585
+ role: "assistant",
586
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
587
+ modelID: model.modelID,
588
+ providerID: model.providerID,
589
+ }
590
+ yield* sessions.updateMessage(msg)
591
+ const started = Date.now()
592
+ const part: SessionV1.ToolPart = {
593
+ type: "tool",
594
+ id: PartID.ascending(),
595
+ messageID: msg.id,
596
+ sessionID: input.sessionID,
597
+ tool: ShellID.ToolID,
598
+ callID: ulid(),
599
+ state: {
600
+ status: "running",
601
+ time: { start: started },
602
+ input: { command: input.command },
603
+ },
604
+ }
605
+ yield* sessions.updatePart(part)
606
+ return { msg, part, cwd: ctx.directory }
607
+ }).pipe(Effect.ensuring(markReady))
608
+
609
+ const cfg = yield* config.get()
610
+ const sh = Shell.preferred(cfg.shell)
611
+ const args = Shell.args(sh, input.command, cwd)
612
+ let output = ""
613
+ let aborted = false
614
+
615
+ const finish = Effect.uninterruptible(
616
+ Effect.gen(function* () {
617
+ if (aborted) {
618
+ output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
619
+ }
620
+ const completed = Date.now()
621
+ if (!msg.time.completed) {
622
+ msg.time.completed = completed
623
+ yield* sessions.updateMessage(msg)
624
+ }
625
+ if (part.state.status === "running") {
626
+ part.state = {
627
+ status: "completed",
628
+ time: { ...part.state.time, end: completed },
629
+ input: part.state.input,
630
+ title: "",
631
+ metadata: { output },
632
+ output,
633
+ }
634
+ yield* sessions.updatePart(part)
635
+ }
636
+ }),
637
+ )
638
+
639
+ const exit = yield* restore(
640
+ Effect.gen(function* () {
641
+ const shellEnv = yield* plugin.trigger(
642
+ "shell.env",
643
+ { cwd, sessionID: input.sessionID, callID: part.callID },
644
+ { env: {} },
645
+ )
646
+ const cmd = ChildProcess.make(sh, args, {
647
+ cwd,
648
+ extendEnv: true,
649
+ env: { ...shellEnv.env, TERM: "dumb" },
650
+ stdin: "ignore",
651
+ forceKillAfter: "3 seconds",
652
+ })
653
+ const handle = yield* spawner.spawn(cmd)
654
+ yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
655
+ Effect.gen(function* () {
656
+ output += chunk
657
+ if (part.state.status === "running") {
658
+ part.state.metadata = { output }
659
+ yield* sessions.updatePart(part)
660
+ }
661
+ }),
662
+ )
663
+ yield* handle.exitCode
664
+ }).pipe(Effect.scoped, Effect.orDie),
665
+ ).pipe(Effect.exit)
666
+
667
+ if (Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause) && !Cause.hasDies(exit.cause)) {
668
+ aborted = true
669
+ }
670
+ yield* finish
671
+
672
+ if (Exit.isFailure(exit) && !aborted && !Cause.hasInterruptsOnly(exit.cause)) {
673
+ return yield* Effect.failCause(exit.cause)
674
+ }
675
+
676
+ return { info: msg, parts: [part] }
677
+ }),
678
+ )
679
+ })
680
+
681
+ const getModel = Effect.fn("SessionPrompt.getModel")(function* (
682
+ providerID: ProviderV2.ID,
683
+ modelID: ModelV2.ID,
684
+ sessionID: SessionID,
685
+ ) {
686
+ const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
687
+ if (Exit.isSuccess(exit)) return exit.value
688
+ const err = Cause.squash(exit.cause)
689
+ if (Provider.ModelNotFoundError.isInstance(err)) {
690
+ const hint = err.suggestions?.length ? ` Did you mean: ${err.suggestions.join(", ")}?` : ""
691
+ yield* events.publish(Session.Event.Error, {
692
+ sessionID,
693
+ error: new NamedError.Unknown({
694
+ message: `Model not found: ${err.providerID}/${err.modelID}.${hint}`,
695
+ }).toObject(),
696
+ })
697
+ }
698
+ return yield* Effect.die(err)
699
+ })
700
+
701
+ const currentModel = Effect.fnUntraced(function* (sessionID: SessionID) {
702
+ const current = yield* db
703
+ .select({ model: SessionTable.model })
704
+ .from(SessionTable)
705
+ .where(eq(SessionTable.id, sessionID))
706
+ .get()
707
+ .pipe(Effect.orDie)
708
+ if (current?.model) {
709
+ return {
710
+ providerID: ProviderV2.ID.make(current.model.providerID),
711
+ modelID: ModelV2.ID.make(current.model.id),
712
+ ...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}),
713
+ }
714
+ }
715
+ const match = yield* sessions
716
+ .findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
717
+ .pipe(Effect.orDie)
718
+ if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
719
+ return yield* provider.defaultModel().pipe(Effect.orDie)
720
+ })
721
+
722
+ const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
723
+ const agentName = input.agent
724
+ const ag = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
725
+ if (!ag) {
726
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
727
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
728
+ const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
729
+ yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
730
+ throw error
731
+ }
732
+
733
+ const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID))
734
+ const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
735
+ const full =
736
+ !input.variant && ag.variant && same
737
+ ? yield* provider
738
+ .getModel(model.providerID, model.modelID)
739
+ .pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined)))
740
+ : undefined
741
+ const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
742
+
743
+ const info: SessionV1.User = {
744
+ id: input.messageID ?? MessageID.ascending(),
745
+ role: "user",
746
+ sessionID: input.sessionID,
747
+ time: { created: Date.now() },
748
+ tools: input.tools,
749
+ agent: ag.name,
750
+ model: {
751
+ providerID: model.providerID,
752
+ modelID: model.modelID,
753
+ variant,
754
+ },
755
+ system: input.system,
756
+ format: input.format,
757
+ }
758
+
759
+ const current = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
760
+ if (
761
+ current.agent !== info.agent ||
762
+ current.model?.providerID !== info.model.providerID ||
763
+ current.model?.id !== info.model.modelID ||
764
+ (current.model?.variant === "default" ? undefined : current.model?.variant) !== info.model.variant
765
+ ) {
766
+ yield* sessions.setAgentModel({
767
+ sessionID: input.sessionID,
768
+ agent: info.agent,
769
+ model: {
770
+ id: info.model.modelID,
771
+ providerID: info.model.providerID,
772
+ variant: info.model.variant ?? "default",
773
+ },
774
+ time: info.time.created,
775
+ })
776
+ }
777
+
778
+ yield* Effect.addFinalizer(() => instruction.clear(info.id))
779
+
780
+ type Draft<T> = T extends SessionV1.Part ? Omit<T, "id"> & { id?: string } : never
781
+ const assign = (part: Draft<SessionV1.Part>): SessionV1.Part => ({
782
+ ...part,
783
+ id: part.id ? PartID.make(part.id) : PartID.ascending(),
784
+ })
785
+
786
+ const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
787
+ "SessionPrompt.resolveUserPart",
788
+ )(function* (part) {
789
+ if (part.type === "file") {
790
+ if (part.source?.type === "resource") {
791
+ const { clientName, uri } = part.source
792
+ yield* Effect.logInfo("mcp resource", { clientName, uri, mime: part.mime })
793
+ const pieces: Draft<SessionV1.Part>[] = [
794
+ {
795
+ messageID: info.id,
796
+ sessionID: input.sessionID,
797
+ type: "text",
798
+ synthetic: true,
799
+ text: `Reading MCP resource: ${part.filename} (${uri})`,
800
+ },
801
+ ]
802
+ const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit)
803
+ if (Exit.isSuccess(exit)) {
804
+ const content = exit.value
805
+ if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`)
806
+ const items = Array.isArray(content.contents) ? content.contents : [content.contents]
807
+ for (const c of items) {
808
+ if (!c || typeof c !== "object") continue
809
+ if ("text" in c && typeof c.text === "string" && c.text) {
810
+ pieces.push({
811
+ messageID: info.id,
812
+ sessionID: input.sessionID,
813
+ type: "text",
814
+ synthetic: true,
815
+ text: c.text,
816
+ })
817
+ } else if ("blob" in c && typeof c.blob === "string" && c.blob) {
818
+ const mime = "mimeType" in c && typeof c.mimeType === "string" ? c.mimeType : part.mime
819
+ const filename = "uri" in c && typeof c.uri === "string" ? c.uri : part.filename
820
+ const size = mcpResourceBase64Size(c.blob)
821
+ if (!SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES.has(mime)) {
822
+ pieces.push({
823
+ messageID: info.id,
824
+ sessionID: input.sessionID,
825
+ type: "text",
826
+ synthetic: true,
827
+ text: `[Binary MCP resource omitted: ${filename ?? uri} (${mime}, ${formatMcpResourceBytes(size)}) is not a supported attachment type]`,
828
+ })
829
+ continue
830
+ }
831
+ if (size > MAX_MCP_RESOURCE_BLOB_BYTES) {
832
+ pieces.push({
833
+ messageID: info.id,
834
+ sessionID: input.sessionID,
835
+ type: "text",
836
+ synthetic: true,
837
+ text: `[Binary MCP resource omitted: ${filename ?? uri} (${mime}, ${formatMcpResourceBytes(size)}) exceeds ${formatMcpResourceBytes(MAX_MCP_RESOURCE_BLOB_BYTES)}]`,
838
+ })
839
+ continue
840
+ }
841
+ pieces.push({
842
+ messageID: info.id,
843
+ sessionID: input.sessionID,
844
+ type: "text",
845
+ synthetic: true,
846
+ text: `[Binary MCP resource attached: ${filename ?? uri} (${mime})]`,
847
+ })
848
+ pieces.push({
849
+ messageID: info.id,
850
+ sessionID: input.sessionID,
851
+ type: "file",
852
+ mime,
853
+ filename,
854
+ url: `data:${mime};base64,${c.blob}`,
855
+ })
856
+ }
857
+ }
858
+ } else {
859
+ const error = Cause.squash(exit.cause)
860
+ yield* Effect.logError("failed to read MCP resource", { error, clientName, uri })
861
+ const message = error instanceof Error ? error.message : String(error)
862
+ pieces.push({
863
+ messageID: info.id,
864
+ sessionID: input.sessionID,
865
+ type: "text",
866
+ synthetic: true,
867
+ text: `Failed to read MCP resource ${part.filename}: ${message}`,
868
+ })
869
+ }
870
+ return pieces
871
+ }
872
+ const url = new URL(part.url)
873
+ switch (url.protocol) {
874
+ case "data:":
875
+ if (part.mime === "text/plain") {
876
+ return [
877
+ {
878
+ messageID: info.id,
879
+ sessionID: input.sessionID,
880
+ type: "text",
881
+ synthetic: true,
882
+ text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
883
+ },
884
+ {
885
+ messageID: info.id,
886
+ sessionID: input.sessionID,
887
+ type: "text",
888
+ synthetic: true,
889
+ text: decodeDataUrl(part.url),
890
+ },
891
+ { ...part, messageID: info.id, sessionID: input.sessionID },
892
+ ]
893
+ }
894
+ break
895
+ case "file:": {
896
+ yield* Effect.logInfo("file", { mime: part.mime })
897
+ const filepath = fileURLToPath(part.url)
898
+ const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
899
+
900
+ const { read } = yield* registry.named()
901
+ const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
902
+ const controller = new AbortController()
903
+ return read
904
+ .execute(args, {
905
+ sessionID: input.sessionID,
906
+ abort: controller.signal,
907
+ agent: input.agent!,
908
+ messageID: info.id,
909
+ extra: { bypassCwdCheck: true, ...extra },
910
+ messages: [],
911
+ metadata: () => Effect.void,
912
+ ask: () => Effect.void,
913
+ })
914
+ .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
915
+ }
916
+
917
+ if (mime === "text/plain") {
918
+ let offset: number | undefined
919
+ let limit: number | undefined
920
+ const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") }
921
+ if (range.start != null) {
922
+ const filePathURI = part.url.split("?")[0]
923
+ let start = parseInt(range.start)
924
+ let end = range.end ? parseInt(range.end) : undefined
925
+ if (start === end) {
926
+ const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([])))
927
+ for (const symbol of symbols) {
928
+ let r: LSP.Range | undefined
929
+ if ("range" in symbol) r = symbol.range
930
+ else if ("location" in symbol) r = symbol.location.range
931
+ if (r?.start?.line && r?.start?.line === start) {
932
+ start = r.start.line
933
+ end = r?.end?.line ?? start
934
+ break
935
+ }
936
+ }
937
+ }
938
+ offset = Math.max(start, 1)
939
+ if (end) limit = end - (offset - 1)
940
+ }
941
+ const args = { filePath: filepath, offset, limit }
942
+ const pieces: Draft<SessionV1.Part>[] = [
943
+ {
944
+ messageID: info.id,
945
+ sessionID: input.sessionID,
946
+ type: "text",
947
+ synthetic: true,
948
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
949
+ },
950
+ ]
951
+ const exit = yield* provider.getModel(info.model.providerID, info.model.modelID).pipe(
952
+ Effect.flatMap((mdl) => execRead(args, { model: mdl })),
953
+ Effect.exit,
954
+ )
955
+ if (Exit.isSuccess(exit)) {
956
+ const result = exit.value
957
+ pieces.push({
958
+ messageID: info.id,
959
+ sessionID: input.sessionID,
960
+ type: "text",
961
+ synthetic: true,
962
+ text: result.output,
963
+ })
964
+ if (result.attachments?.length) {
965
+ pieces.push(
966
+ ...result.attachments.map((a) => ({
967
+ ...a,
968
+ synthetic: true,
969
+ filename: a.filename ?? part.filename,
970
+ messageID: info.id,
971
+ sessionID: input.sessionID,
972
+ })),
973
+ )
974
+ } else {
975
+ pieces.push({ ...part, mime, messageID: info.id, sessionID: input.sessionID })
976
+ }
977
+ } else {
978
+ const error = Cause.squash(exit.cause)
979
+ yield* Effect.logError("failed to read file", { error, filepath })
980
+ const message = error instanceof Error ? error.message : String(error)
981
+ yield* events.publish(Session.Event.Error, {
982
+ sessionID: input.sessionID,
983
+ error: new NamedError.Unknown({ message }).toObject(),
984
+ })
985
+ pieces.push({
986
+ messageID: info.id,
987
+ sessionID: input.sessionID,
988
+ type: "text",
989
+ synthetic: true,
990
+ text: `Read tool failed to read ${filepath} with the following error: ${message}`,
991
+ })
992
+ }
993
+ return pieces
994
+ }
995
+
996
+ if (mime === "application/x-directory") {
997
+ const args = { filePath: filepath }
998
+ const exit = yield* execRead(args).pipe(Effect.exit)
999
+ if (Exit.isFailure(exit)) {
1000
+ const error = Cause.squash(exit.cause)
1001
+ yield* Effect.logError("failed to read directory", { error, filepath })
1002
+ const message = error instanceof Error ? error.message : String(error)
1003
+ yield* events.publish(Session.Event.Error, {
1004
+ sessionID: input.sessionID,
1005
+ error: new NamedError.Unknown({ message }).toObject(),
1006
+ })
1007
+ return [
1008
+ {
1009
+ messageID: info.id,
1010
+ sessionID: input.sessionID,
1011
+ type: "text",
1012
+ synthetic: true,
1013
+ text: `Read tool failed to read ${filepath} with the following error: ${message}`,
1014
+ },
1015
+ ]
1016
+ }
1017
+ return [
1018
+ {
1019
+ messageID: info.id,
1020
+ sessionID: input.sessionID,
1021
+ type: "text",
1022
+ synthetic: true,
1023
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
1024
+ },
1025
+ {
1026
+ messageID: info.id,
1027
+ sessionID: input.sessionID,
1028
+ type: "text",
1029
+ synthetic: true,
1030
+ text: exit.value.output,
1031
+ },
1032
+ { ...part, mime, messageID: info.id, sessionID: input.sessionID },
1033
+ ]
1034
+ }
1035
+
1036
+ return [
1037
+ {
1038
+ messageID: info.id,
1039
+ sessionID: input.sessionID,
1040
+ type: "text",
1041
+ synthetic: true,
1042
+ text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
1043
+ },
1044
+ {
1045
+ id: part.id,
1046
+ messageID: info.id,
1047
+ sessionID: input.sessionID,
1048
+ type: "file",
1049
+ url:
1050
+ `data:${mime};base64,` +
1051
+ Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
1052
+ mime,
1053
+ filename: part.filename!,
1054
+ source: part.source,
1055
+ },
1056
+ ]
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ if (part.type === "agent") {
1062
+ const perm = Permission.evaluate("task", part.name, ag.permission)
1063
+ const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
1064
+ return [
1065
+ { ...part, messageID: info.id, sessionID: input.sessionID },
1066
+ {
1067
+ messageID: info.id,
1068
+ sessionID: input.sessionID,
1069
+ type: "text",
1070
+ synthetic: true,
1071
+ text:
1072
+ " Use the above message and context to generate a prompt and call the task tool with subagent: " +
1073
+ part.name +
1074
+ hint,
1075
+ },
1076
+ ]
1077
+ }
1078
+
1079
+ return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
1080
+ })
1081
+
1082
+ const resolvedParts = yield* Effect.forEach(
1083
+ [...input.parts, ...pinnedFileParts(current, input.parts)],
1084
+ resolvePart,
1085
+ { concurrency: "unbounded" },
1086
+ ).pipe(Effect.map((x) => x.flat().map(assign)))
1087
+
1088
+ yield* plugin.trigger(
1089
+ "chat.message",
1090
+ {
1091
+ sessionID: input.sessionID,
1092
+ agent: input.agent,
1093
+ model: input.model,
1094
+ messageID: input.messageID,
1095
+ variant: input.variant,
1096
+ },
1097
+ { message: info, parts: resolvedParts },
1098
+ )
1099
+
1100
+ const parts = yield* Effect.forEach(resolvedParts, (part) =>
1101
+ part.type === "file" && part.mime.startsWith("image/")
1102
+ ? image.normalize(part).pipe(
1103
+ Effect.catchIf(
1104
+ (error) => error instanceof Image.ResizerUnavailableError,
1105
+ () => Effect.succeed(part),
1106
+ ),
1107
+ )
1108
+ : Effect.succeed(part),
1109
+ )
1110
+
1111
+ const parsed = decodeMessageInfo(info, { errors: "all", propertyOrder: "original" })
1112
+ if (Exit.isFailure(parsed)) {
1113
+ yield* Effect.logError("invalid user message before save", {
1114
+ sessionID: input.sessionID,
1115
+ messageID: info.id,
1116
+ agent: info.agent,
1117
+ model: info.model,
1118
+ cause: Cause.pretty(parsed.cause),
1119
+ })
1120
+ }
1121
+ for (const [index, part] of parts.entries()) {
1122
+ const p = decodeMessagePart(part, { errors: "all", propertyOrder: "original" })
1123
+ if (Exit.isSuccess(p)) continue
1124
+ yield* Effect.logError("invalid user part before save", {
1125
+ sessionID: input.sessionID,
1126
+ messageID: info.id,
1127
+ partID: part.id,
1128
+ partType: part.type,
1129
+ index,
1130
+ cause: Cause.pretty(p.cause),
1131
+ part,
1132
+ })
1133
+ }
1134
+
1135
+ yield* sessions.updateMessage(info)
1136
+ for (const part of parts) yield* sessions.updatePart(part)
1137
+
1138
+ return { info, parts }
1139
+ }, Effect.scoped)
1140
+
1141
+ const prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error> = Effect.fn(
1142
+ "SessionPrompt.prompt",
1143
+ )(function* (input: PromptInput) {
1144
+ const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
1145
+ yield* revert.cleanup(session)
1146
+ const message = yield* createUserMessage(input)
1147
+ yield* sessions.touch(input.sessionID)
1148
+
1149
+ const permissions: PermissionV1.Rule[] = []
1150
+ for (const [t, enabled] of Object.entries(input.tools ?? {})) {
1151
+ permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
1152
+ }
1153
+ if (permissions.length > 0) {
1154
+ session.permission = permissions
1155
+ yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
1156
+ }
1157
+
1158
+ if (input.noReply === true) return message
1159
+ return yield* loop({ sessionID: input.sessionID })
1160
+ })
1161
+
1162
+ const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) {
1163
+ const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user").pipe(Effect.orDie)
1164
+ if (Option.isSome(match)) return match.value
1165
+ const msgs = yield* sessions.messages({ sessionID, limit: 1 }).pipe(Effect.orDie)
1166
+ if (msgs.length > 0) return msgs[0]
1167
+ throw new Error("Impossible")
1168
+ })
1169
+
1170
+ const runLoop = Effect.fn("SessionPrompt.run")(
1171
+ function* (sessionID: SessionID) {
1172
+ const ctx = yield* InstanceState.context
1173
+ const cfg = yield* config.get()
1174
+ let structured: unknown
1175
+ let step = 0
1176
+ let modelOverride: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
1177
+ const session = yield* sessions.get(sessionID).pipe(Effect.orDie)
1178
+
1179
+ while (true) {
1180
+ yield* status.set(sessionID, { type: "busy" })
1181
+ yield* Effect.logInfo("loop", { "session.id": sessionID, step })
1182
+
1183
+ let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
1184
+ Effect.provideService(Database.Service, database),
1185
+ )
1186
+
1187
+ const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
1188
+
1189
+ if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
1190
+
1191
+ const lastAssistantMsg = msgs.findLast(
1192
+ (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
1193
+ )
1194
+ // Some providers return "stop" even when the assistant message contains
1195
+ // tool calls. Keep the loop running so tool results can be sent back to
1196
+ // the model, but ignore cleanup-marked interrupted orphans.
1197
+ const hasToolCalls =
1198
+ lastAssistantMsg?.parts.some(
1199
+ (part) => part.type === "tool" && !part.metadata?.providerExecuted && !isOrphanedInterruptedTool(part),
1200
+ ) ?? false
1201
+
1202
+ if (
1203
+ lastAssistant?.finish &&
1204
+ !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
1205
+ !hasToolCalls &&
1206
+ lastAssistant.parentID === lastUser.id
1207
+ ) {
1208
+ const orphan = lastAssistantMsg?.parts.find(
1209
+ (part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
1210
+ )
1211
+ if (orphan) {
1212
+ yield* Effect.logWarning("loop exit with orphaned interrupted tool", {
1213
+ "session.id": sessionID,
1214
+ messageID: lastAssistant.id,
1215
+ tool: orphan.tool,
1216
+ callID: orphan.callID,
1217
+ })
1218
+ }
1219
+ yield* Effect.logInfo("exiting loop", { "session.id": sessionID })
1220
+ break
1221
+ }
1222
+
1223
+ step++
1224
+ if (step === 1) {
1225
+ yield* title({
1226
+ session,
1227
+ modelID: lastUser.model.modelID,
1228
+ providerID: lastUser.model.providerID,
1229
+ history: msgs,
1230
+ }).pipe(Effect.ignore, Effect.forkIn(scope))
1231
+
1232
+ // Auto-load skills based on user context
1233
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
1234
+ if (lastUserMsg) {
1235
+ const userText = lastUserMsg.parts
1236
+ .filter((p) => p.type === "text")
1237
+ .map((p) => p.text)
1238
+ .join(" ")
1239
+ if (userText) {
1240
+ const allSkills = yield* skill.available()
1241
+ const detected = detectSkills({
1242
+ userMessage: userText,
1243
+ availableSkills: allSkills.map((s) => ({ name: s.name, description: s.description })),
1244
+ })
1245
+ // Tejamkorlik: sessiyada allaqachon yuklangan skill'ni qayta yuklamaslik.
1246
+ const fresh = dedupeSessionSkills(sessionID, detected)
1247
+ for (const skillName of fresh) {
1248
+ yield* skill.require(skillName).pipe(Effect.catchTag("Skill.NotFoundError", () => Effect.void))
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ const ref = modelOverride ?? { providerID: lastUser.model.providerID, modelID: lastUser.model.modelID }
1255
+ const model = yield* getModel(ref.providerID, ref.modelID, sessionID)
1256
+
1257
+ // Check rate limits before making LLM call.
1258
+ // Soft per-minute caps wait; hard daily caps and oversized inputs block.
1259
+ const providerConfig = yield* provider.getProvider(model.providerID).pipe(Effect.catch(() => Effect.succeed(undefined)))
1260
+ const rateLimitConfig = providerConfig?.rateLimit
1261
+ if (rateLimitConfig) {
1262
+ yield* rateLimit.waitIfExceeded(model.providerID, rateLimitConfig)
1263
+ const estimatedInput = Token.estimate(JSON.stringify(msgs))
1264
+ const blocked = yield* rateLimit.enforce(model.providerID, rateLimitConfig, estimatedInput)
1265
+ if (blocked) {
1266
+ const error = new NamedError.Unknown({ message: blocked })
1267
+ yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
1268
+ throw error
1269
+ }
1270
+ }
1271
+
1272
+ const task = tasks.pop()
1273
+
1274
+ if (task?.type === "subtask") {
1275
+ yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
1276
+ continue
1277
+ }
1278
+
1279
+ if (task?.type === "compaction") {
1280
+ const result = yield* compaction.process({
1281
+ messages: msgs,
1282
+ parentID: lastUser.id,
1283
+ sessionID,
1284
+ auto: task.auto,
1285
+ overflow: task.overflow,
1286
+ })
1287
+ if (result === "stop") break
1288
+ continue
1289
+ }
1290
+
1291
+ if (
1292
+ lastFinished &&
1293
+ lastFinished.summary !== true &&
1294
+ (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model }))
1295
+ ) {
1296
+ yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
1297
+ continue
1298
+ }
1299
+
1300
+ const agent = yield* agents.get(lastUser.agent)
1301
+ if (!agent) {
1302
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
1303
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1304
+ const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
1305
+ yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
1306
+ throw error
1307
+ }
1308
+ const maxSteps = agent.steps ?? Infinity
1309
+ const isLastStep = step >= maxSteps
1310
+ msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(
1311
+ Effect.provideService(RuntimeFlags.Service, flags),
1312
+ Effect.provideService(FSUtil.Service, fsys),
1313
+ Effect.provideService(Session.Service, sessions),
1314
+ )
1315
+
1316
+ const msg: SessionV1.Assistant = {
1317
+ id: MessageID.ascending(),
1318
+ parentID: lastUser.id,
1319
+ role: "assistant",
1320
+ mode: agent.name,
1321
+ agent: agent.name,
1322
+ variant: lastUser.model.variant,
1323
+ path: { cwd: ctx.directory, root: ctx.worktree },
1324
+ cost: 0,
1325
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1326
+ modelID: model.id,
1327
+ providerID: model.providerID,
1328
+ time: { created: Date.now() },
1329
+ sessionID,
1330
+ }
1331
+ yield* sessions.updateMessage(msg)
1332
+
1333
+ const finalizeInterruptedAssistant = Effect.gen(function* () {
1334
+ if (msg.time.completed) return
1335
+ msg.error ??= MessageV2.fromError(new DOMException("Aborted", "AbortError"), {
1336
+ providerID: msg.providerID,
1337
+ aborted: true,
1338
+ })
1339
+ msg.time.completed = Date.now()
1340
+ yield* sessions.updateMessage(msg)
1341
+ })
1342
+
1343
+ const handle = yield* processor
1344
+ .create({
1345
+ assistantMessage: msg,
1346
+ sessionID,
1347
+ model,
1348
+ })
1349
+ .pipe(Effect.onInterrupt(() => finalizeInterruptedAssistant))
1350
+
1351
+ const outcome: "break" | "continue" = yield* Effect.gen(function* () {
1352
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
1353
+ const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
1354
+ const promptOps = yield* ops()
1355
+
1356
+ const tools = yield* SessionTools.resolve({
1357
+ agent,
1358
+ session,
1359
+ model,
1360
+ processor: handle,
1361
+ bypassAgentCheck,
1362
+ messages: msgs,
1363
+ promptOps,
1364
+ }).pipe(
1365
+ Effect.provideService(Plugin.Service, plugin),
1366
+ Effect.provideService(Permission.Service, permission),
1367
+ Effect.provideService(ToolRegistry.Service, registry),
1368
+ Effect.provideService(MCP.Service, mcp),
1369
+ Effect.provideService(Truncate.Service, truncate),
1370
+ Effect.provideService(RuntimeFlags.Service, flags),
1371
+ Effect.provideService(Hooks.Service, hooks),
1372
+ Effect.provideService(SkillRestriction.Service, skillRestriction),
1373
+ )
1374
+
1375
+ if (lastUser.format?.type === "json_schema") {
1376
+ tools["StructuredOutput"] = createStructuredOutputTool({
1377
+ schema: lastUser.format.schema,
1378
+ onSuccess(output) {
1379
+ structured = output
1380
+ },
1381
+ })
1382
+ }
1383
+
1384
+ if (step === 1)
1385
+ yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope))
1386
+
1387
+ yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
1388
+
1389
+ const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([
1390
+ sys.skills(agent),
1391
+ sys.environment(model),
1392
+ instruction.system().pipe(Effect.orDie),
1393
+ sys.mcp(agent, session.permission),
1394
+ MessageV2.toModelMessagesEffect(msgs, model),
1395
+ ])
1396
+ const repomap = yield* repomapFromContext(fsys, ripgrep, ctx.directory).pipe(
1397
+ Effect.orDie,
1398
+ Effect.catchCause(() => Effect.succeed(undefined)),
1399
+ )
1400
+ const diffCtx = yield* getDiffContextImpl(git)(ctx.directory).pipe(
1401
+ Effect.orDie,
1402
+ Effect.catchCause(() => Effect.succeed(undefined)),
1403
+ )
1404
+ const system = [
1405
+ ...env,
1406
+ ...(repomap ? [repomap] : []),
1407
+ ...(diffCtx ? [diffCtx] : []),
1408
+ ...instructions,
1409
+ ...(mcpInstructions ? [mcpInstructions] : []),
1410
+ ...(skills ? [skills] : []),
1411
+ ]
1412
+ const format = lastUser.format ?? { type: "text" as const }
1413
+ if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
1414
+ // Pre-flight guard: the overflow check at loop start relies on the
1415
+ // last turn's reported usage, which can be missing or stale. Measure
1416
+ // the actual outgoing request and compact before the provider can
1417
+ // reject it for exceeding the model's combined token limit.
1418
+ const preflightInput = estimateInputTokens({ system, messages: modelMsgs, tools })
1419
+ const preflightMaxOut = ProviderTransform.maxOutputTokens(model, flags.outputTokenMax)
1420
+ if (model.limit.context > 0 && preflightInput + preflightMaxOut > model.limit.context) {
1421
+ yield* Effect.logWarning("preflight overflow, compacting before send", {
1422
+ "session.id": sessionID,
1423
+ estimated: preflightInput + preflightMaxOut,
1424
+ limit: model.limit.context,
1425
+ })
1426
+ yield* compaction.create({
1427
+ sessionID,
1428
+ agent: lastUser.agent,
1429
+ model: lastUser.model,
1430
+ auto: true,
1431
+ })
1432
+ return "continue" as const
1433
+ }
1434
+ const result = yield* handle.process({
1435
+ user: lastUser,
1436
+ agent,
1437
+ permission: session.permission,
1438
+ sessionID,
1439
+ parentSessionID: session.parentID,
1440
+ system,
1441
+ messages: [
1442
+ ...modelMsgs,
1443
+ ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS_PROMPT }] : []),
1444
+ ],
1445
+ tools,
1446
+ model,
1447
+ toolChoice: format.type === "json_schema" ? "required" : undefined,
1448
+ })
1449
+
1450
+ if (structured !== undefined) {
1451
+ handle.message.structured = structured
1452
+ handle.message.finish = handle.message.finish ?? "stop"
1453
+ yield* sessions.updateMessage(handle.message)
1454
+ return "break" as const
1455
+ }
1456
+
1457
+ const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
1458
+ if (finished && !handle.message.error) {
1459
+ // Surface any content-filter finish (e.g. Anthropic stop_reason:
1460
+ // refusal) as an error. These turns may have produced no visible
1461
+ // output at all — previously the session went idle silently — or
1462
+ // partial text that was cut off by the provider's filter.
1463
+ if (handle.message.finish === "content-filter") {
1464
+ handle.message.error = new SessionV1.ContentFilterError({
1465
+ message: "The response was blocked by the provider's content filter",
1466
+ }).toObject()
1467
+ yield* sessions.updateMessage(handle.message)
1468
+ yield* events.publish(Session.Event.Error, { sessionID, error: handle.message.error })
1469
+ return "break" as const
1470
+ }
1471
+ if (format.type === "json_schema") {
1472
+ handle.message.error = new SessionV1.StructuredOutputError({
1473
+ message: "Model did not produce structured output",
1474
+ retries: 0,
1475
+ }).toObject()
1476
+ yield* sessions.updateMessage(handle.message)
1477
+ return "break" as const
1478
+ }
1479
+ }
1480
+
1481
+ if (result === "stop") return "break" as const
1482
+ if (result === "compact") {
1483
+ yield* compaction.create({
1484
+ sessionID,
1485
+ agent: lastUser.agent,
1486
+ model: lastUser.model,
1487
+ auto: true,
1488
+ overflow: !handle.message.finish,
1489
+ })
1490
+ }
1491
+ return "continue" as const
1492
+ }).pipe(
1493
+ Effect.ensuring(instruction.clear(handle.message.id)),
1494
+ Effect.onInterrupt(() => finalizeInterruptedAssistant),
1495
+ )
1496
+ // Track token spend for daily budgets (tpd). Uses real usage when
1497
+ // reported, falls back to estimating the assistant text.
1498
+ if (rateLimitConfig) {
1499
+ const t = handle.message.tokens
1500
+ const spent =
1501
+ t.input + t.output + t.reasoning > 0
1502
+ ? t.input + t.output + t.reasoning
1503
+ : Token.estimate(JSON.stringify(handle.message))
1504
+ yield* rateLimit.record(model.providerID, rateLimitConfig, spent).pipe(Effect.ignore)
1505
+ }
1506
+ if (outcome === "break") {
1507
+ const failed = handle.message.error
1508
+ if (failed && isFallbackEligible(failed)) {
1509
+ const primary = { providerID: lastUser.model.providerID, modelID: lastUser.model.modelID }
1510
+ const chain = [primary]
1511
+ for (const entry of cfg.model_fallback ?? []) {
1512
+ const parsed = Provider.parseModel(entry)
1513
+ if (!parsed.providerID || !parsed.modelID) continue
1514
+ if (chain.some((m) => m.providerID === parsed.providerID && m.modelID === parsed.modelID)) continue
1515
+ chain.push(parsed)
1516
+ }
1517
+ const active = modelOverride ?? primary
1518
+ const index = chain.findIndex(
1519
+ (m) => m.providerID === active.providerID && m.modelID === active.modelID,
1520
+ )
1521
+ for (const candidate of index === -1 ? [] : chain.slice(index + 1)) {
1522
+ const resolved = yield* provider
1523
+ .getModel(candidate.providerID, candidate.modelID)
1524
+ .pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined)))
1525
+ if (!resolved) continue
1526
+ modelOverride = { providerID: resolved.providerID, modelID: resolved.id }
1527
+ yield* sessions
1528
+ .setAgentModel({
1529
+ sessionID,
1530
+ agent: lastUser.agent,
1531
+ model: {
1532
+ id: resolved.id,
1533
+ providerID: resolved.providerID,
1534
+ variant: lastUser.model.variant ?? "default",
1535
+ },
1536
+ time: Date.now(),
1537
+ })
1538
+ .pipe(Effect.orDie)
1539
+ yield* Effect.logWarning("model fallback", {
1540
+ "session.id": sessionID,
1541
+ from: `${active.providerID}/${active.modelID}`,
1542
+ to: `${resolved.providerID}/${resolved.id}`,
1543
+ })
1544
+ break
1545
+ }
1546
+ if (modelOverride && (modelOverride.providerID !== active.providerID || modelOverride.modelID !== active.modelID)) {
1547
+ continue
1548
+ }
1549
+ }
1550
+ break
1551
+ }
1552
+
1553
+ const gitCfg = cfg.git
1554
+ if (gitCfg?.auto_commit) {
1555
+ const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
1556
+ const summary = lastUserMsg
1557
+ ? lastUserMsg.parts
1558
+ .filter((p): p is SessionV1.TextPart => p.type === "text")
1559
+ .map((p) => p.text)
1560
+ .join(" ")
1561
+ .slice(0, 72)
1562
+ : ""
1563
+ yield* gitAutoCommit
1564
+ .commit({
1565
+ worktree: ctx.worktree,
1566
+ summary,
1567
+ })
1568
+ .pipe(Effect.ignore, Effect.forkIn(scope))
1569
+ }
1570
+ continue
1571
+ }
1572
+
1573
+ yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope))
1574
+ return yield* lastAssistant(sessionID)
1575
+ },
1576
+ )
1577
+
1578
+ const loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
1579
+ input: LoopInput,
1580
+ ) {
1581
+ return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
1582
+ })
1583
+
1584
+ const shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError> = Effect.fn(
1585
+ "SessionPrompt.shell",
1586
+ )(function* (input: ShellInput) {
1587
+ const ready = yield* Latch.make()
1588
+ return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input, ready), ready)
1589
+ })
1590
+
1591
+ const pinCommand = Effect.fn("SessionPrompt.pinCommand")(function* (input: CommandInput) {
1592
+ const ctx = yield* InstanceState.context
1593
+ const raw = input.arguments.match(argsRegex) ?? []
1594
+ const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
1595
+ const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
1596
+ const current = readPinned(session.metadata)
1597
+ const resolveArg = (arg: string) =>
1598
+ path.isAbsolute(arg) ? path.normalize(arg) : path.resolve(ctx.worktree, arg)
1599
+ const rel = (file: string) => path.relative(ctx.worktree, file) || file
1600
+
1601
+ const next = [...current]
1602
+ const changed: string[] = []
1603
+ const missing: string[] = []
1604
+ if (input.command === "add") {
1605
+ for (const arg of args) {
1606
+ const file = resolveArg(arg)
1607
+ const exists = yield* fsys.stat(file).pipe(Effect.option)
1608
+ if (Option.isNone(exists)) {
1609
+ missing.push(arg)
1610
+ continue
1611
+ }
1612
+ if (next.includes(file)) continue
1613
+ next.push(file)
1614
+ changed.push(file)
1615
+ }
1616
+ } else {
1617
+ for (const arg of args) {
1618
+ const file = resolveArg(arg)
1619
+ const index = next.findIndex((item) => item === file || item.endsWith(`/${arg}`))
1620
+ if (index === -1) {
1621
+ missing.push(arg)
1622
+ continue
1623
+ }
1624
+ changed.push(next[index]!)
1625
+ next.splice(index, 1)
1626
+ }
1627
+ }
1628
+ yield* sessions
1629
+ .setMetadata({ sessionID: input.sessionID, metadata: { ...(session.metadata ?? {}), [PINNED_KEY]: next } })
1630
+ .pipe(Effect.orDie)
1631
+
1632
+ const lines: string[] = []
1633
+ if (input.command === "add") {
1634
+ for (const file of changed) lines.push(`Pinned ${rel(file)} — included in every reply context.`)
1635
+ for (const arg of missing) lines.push(`Not found: ${arg}`)
1636
+ } else {
1637
+ for (const file of changed) lines.push(`Unpinned ${rel(file)}.`)
1638
+ for (const arg of missing) lines.push(`Not pinned: ${arg}`)
1639
+ }
1640
+ if (next.length) {
1641
+ lines.push("", "Pinned files:")
1642
+ for (const file of next) lines.push(`- ${rel(file)}`)
1643
+ } else {
1644
+ lines.push("", "No pinned files. Use /add <file...> to pin files into context.")
1645
+ }
1646
+ const result = yield* prompt({
1647
+ sessionID: input.sessionID,
1648
+ agent: input.agent,
1649
+ parts: [{ type: "text", text: lines.join("\n") }],
1650
+ noReply: true,
1651
+ })
1652
+ yield* events.publish(Command.Event.Executed, {
1653
+ name: input.command,
1654
+ sessionID: input.sessionID,
1655
+ arguments: input.arguments,
1656
+ messageID: result.info.id,
1657
+ })
1658
+ return result
1659
+ })
1660
+
1661
+ const commitCommand = Effect.fn("SessionPrompt.commitCommand")(function* (input: CommandInput) {
1662
+ const ctx = yield* InstanceState.context
1663
+ const worktree = ctx.worktree
1664
+ const confirm = (text: string) =>
1665
+ prompt({
1666
+ sessionID: input.sessionID,
1667
+ agent: input.agent,
1668
+ parts: [{ type: "text", text }],
1669
+ noReply: true,
1670
+ }).pipe(
1671
+ Effect.tap((result) =>
1672
+ events.publish(Command.Event.Executed, {
1673
+ name: input.command,
1674
+ sessionID: input.sessionID,
1675
+ arguments: input.arguments,
1676
+ messageID: result.info.id,
1677
+ }),
1678
+ ),
1679
+ )
1680
+ const pending = yield* gitAutoCommit.pending(worktree)
1681
+ if (pending.length === 0) {
1682
+ const inside = yield* fsys.stat(path.join(worktree, ".git")).pipe(
1683
+ Effect.map(() => true),
1684
+ Effect.catch(() => Effect.succeed(false)),
1685
+ )
1686
+ return yield* confirm(
1687
+ inside ? "Nothing to commit — working tree is clean." : "Cannot commit: not a git repository.",
1688
+ )
1689
+ }
1690
+ const diff = yield* gitAutoCommit.patch(worktree)
1691
+ const files = pending.map((s) => `${s.code} ${s.file}`).join("\n")
1692
+ const current = yield* currentModel(input.sessionID)
1693
+ const mdl =
1694
+ (yield* provider.getSmallModel(current.providerID)) ??
1695
+ (yield* provider
1696
+ .getModel(current.providerID, current.modelID)
1697
+ .pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined))))
1698
+ let message = ""
1699
+ const ag = yield* agents.defaultInfo()
1700
+ if (mdl && ag) {
1701
+ const user: SessionV1.User = {
1702
+ id: MessageID.ascending(),
1703
+ role: "user",
1704
+ sessionID: input.sessionID,
1705
+ time: { created: Date.now() },
1706
+ agent: ag.name,
1707
+ model: { providerID: mdl.providerID, modelID: mdl.id },
1708
+ }
1709
+ const raw = yield* llm
1710
+ .stream({
1711
+ agent: ag,
1712
+ user,
1713
+ system: [],
1714
+ small: true,
1715
+ tools: {},
1716
+ model: mdl,
1717
+ sessionID: input.sessionID,
1718
+ retries: 2,
1719
+ messages: [
1720
+ {
1721
+ role: "user",
1722
+ content: `Write a single conventional commit message (one line like "feat(scope): summary", no body, no quotes, no markdown) for these changes:\n\n${files}\n\n${diff.slice(0, 12000)}`,
1723
+ },
1724
+ ],
1725
+ })
1726
+ .pipe(
1727
+ Stream.filter(LLMEvent.is.textDelta),
1728
+ Stream.map((e) => e.text),
1729
+ Stream.mkString,
1730
+ Effect.catchCause(() => Effect.succeed("")),
1731
+ )
1732
+ message =
1733
+ raw
1734
+ .split("\n")
1735
+ .map((line) => line.trim().replace(/^["'`]|["'`]$/g, ""))
1736
+ .find((line) => line.length > 0) ?? ""
1737
+ }
1738
+ if (!message) {
1739
+ message = `feat: update ${pending.length} file(s): ${pending
1740
+ .slice(0, 3)
1741
+ .map((s) => s.file)
1742
+ .join(", ")}`
1743
+ }
1744
+ const error = yield* gitAutoCommit.commitAll({ worktree, message })
1745
+ if (error) {
1746
+ return yield* confirm(`Commit failed: ${error}`)
1747
+ }
1748
+ return yield* confirm(`Committed ${pending.length} file(s):\n${message}`)
1749
+ })
1750
+
1751
+ const doctorCommand = Effect.fn("SessionPrompt.doctorCommand")(function* (input: CommandInput) {
1752
+ const ctx = yield* InstanceState.context
1753
+ const cfg = yield* config.get()
1754
+ const lines = ["Mervex doctor", ""]
1755
+ const check = (label: string, ok: boolean, detail?: string) =>
1756
+ lines.push(`${ok ? "ok" : "FAIL"} - ${label}${detail ? ` (${detail})` : ""}`)
1757
+ const resolveModel = (ref: string) => {
1758
+ const parsed = Provider.parseModel(ref)
1759
+ return provider
1760
+ .getModel(parsed.providerID, parsed.modelID)
1761
+ .pipe(Effect.catchIf(Provider.ModelNotFoundError.isInstance, () => Effect.succeed(undefined)))
1762
+ }
1763
+
1764
+ if (cfg.model) {
1765
+ const mdl = yield* resolveModel(cfg.model)
1766
+ check("model", !!mdl, cfg.model)
1767
+ } else {
1768
+ check("model", false, "not configured")
1769
+ }
1770
+ if (cfg.small_model) {
1771
+ const mdl = yield* resolveModel(cfg.small_model)
1772
+ check("small model", !!mdl, cfg.small_model)
1773
+ }
1774
+ for (const entry of cfg.model_fallback ?? []) {
1775
+ const mdl = yield* resolveModel(entry)
1776
+ check("fallback", !!mdl, entry)
1777
+ }
1778
+ const providers = yield* provider.list().pipe(Effect.catch(() => Effect.succeed({})))
1779
+ const ids = Object.keys(providers)
1780
+ check("providers", ids.length > 0, ids.length ? ids.join(", ") : "none configured")
1781
+
1782
+ const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
1783
+ check("pinned files", true, `${readPinned(session.metadata).length} pinned`)
1784
+ check("snapshots", cfg.snapshot !== false)
1785
+ check("directory", true, ctx.worktree)
1786
+ const isRepo = yield* fsys
1787
+ .stat(path.join(ctx.worktree, ".git"))
1788
+ .pipe(
1789
+ Effect.map(() => true),
1790
+ Effect.catch(() => Effect.succeed(false)),
1791
+ )
1792
+ check("git repo", isRepo)
1793
+
1794
+ const result = yield* prompt({
1795
+ sessionID: input.sessionID,
1796
+ agent: input.agent,
1797
+ parts: [{ type: "text", text: lines.join("\n") }],
1798
+ noReply: true,
1799
+ })
1800
+ yield* events.publish(Command.Event.Executed, {
1801
+ name: input.command,
1802
+ sessionID: input.sessionID,
1803
+ arguments: input.arguments,
1804
+ messageID: result.info.id,
1805
+ })
1806
+ return result
1807
+ })
1808
+
1809
+ const usageCommand = Effect.fn("SessionPrompt.usageCommand")(function* (input: CommandInput) {
1810
+ const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
1811
+ const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
1812
+ const assistants = all.filter((m) => m.info.role === "assistant")
1813
+ const users = all.filter((m) => m.info.role === "user")
1814
+ const tokens = { input: 0, output: 0, reasoning: 0 }
1815
+ for (const m of all) {
1816
+ if (m.info.role !== "assistant") continue
1817
+ const t = m.info.tokens
1818
+ if (!t) continue
1819
+ tokens.input += t.input ?? 0
1820
+ tokens.output += t.output ?? 0
1821
+ tokens.reasoning += t.reasoning ?? 0
1822
+ }
1823
+ const diffs = yield* sessions.diff(input.sessionID).pipe(Effect.catch(() => Effect.succeed([])))
1824
+ const formatTokens = (n: number) => {
1825
+ if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`
1826
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
1827
+ return `${n}`
1828
+ }
1829
+ const formatDuration = (ms: number) => {
1830
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`
1831
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`
1832
+ return `${Math.floor(ms / 3600000)}h ${Math.floor((ms % 3600000) / 60000)}m`
1833
+ }
1834
+ const lines = [
1835
+ "Session usage",
1836
+ "",
1837
+ `Messages ${users.length} user / ${assistants.length} assistant`,
1838
+ `Tokens ${formatTokens(tokens.input)} in / ${formatTokens(tokens.output)} out${tokens.reasoning ? ` / ${formatTokens(tokens.reasoning)} reasoning` : ""}`,
1839
+ ]
1840
+ if ((session.cost ?? 0) > 0) lines.push(`Cost $${(session.cost ?? 0).toFixed(4)}`)
1841
+ const created = session.time.created
1842
+ const updated = session.time.updated
1843
+ if (created && updated && updated > created) lines.push(`Duration ${formatDuration(updated - created)}`)
1844
+ lines.push(`Files ${diffs.length} changed`)
1845
+ const pinned = readPinned(session.metadata)
1846
+ if (pinned.length) {
1847
+ lines.push(`Pinned ${pinned.length} file(s) in context`)
1848
+ }
1849
+ const result = yield* prompt({
1850
+ sessionID: input.sessionID,
1851
+ agent: input.agent,
1852
+ parts: [{ type: "text", text: lines.join("\n") }],
1853
+ noReply: true,
1854
+ })
1855
+ yield* events.publish(Command.Event.Executed, {
1856
+ name: input.command,
1857
+ sessionID: input.sessionID,
1858
+ arguments: input.arguments,
1859
+ messageID: result.info.id,
1860
+ })
1861
+ return result
1862
+ })
1863
+
1864
+ const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
1865
+ if (input.command === "add" || input.command === "drop") return yield* pinCommand(input)
1866
+ if (input.command === "commit") return yield* commitCommand(input)
1867
+ if (input.command === "doctor") return yield* doctorCommand(input)
1868
+ if (input.command === "usage") return yield* usageCommand(input)
1869
+ yield* Effect.logInfo("command", {
1870
+ "session.id": input.sessionID,
1871
+ command: input.command,
1872
+ agent: input.agent,
1873
+ })
1874
+ const cmd = yield* commands.get(input.command)
1875
+ if (!cmd) {
1876
+ const available = (yield* commands.list()).map((c) => c.name)
1877
+ const hint = available.length ? ` Available commands: ${available.join(", ")}` : ""
1878
+ const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` })
1879
+ yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
1880
+ throw error
1881
+ }
1882
+ const agentName = cmd.agent ?? input.agent
1883
+
1884
+ const raw = input.arguments.match(argsRegex) ?? []
1885
+ const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
1886
+ const templateCommand = yield* Effect.promise(async () => cmd.template)
1887
+
1888
+ const placeholders = templateCommand.match(placeholderRegex) ?? []
1889
+ let last = 0
1890
+ for (const item of placeholders) {
1891
+ const value = Number(item.slice(1))
1892
+ if (value > last) last = value
1893
+ }
1894
+
1895
+ const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
1896
+ const position = Number(index)
1897
+ const argIndex = position - 1
1898
+ if (argIndex >= args.length) return ""
1899
+ if (position === last) return args.slice(argIndex).join(" ")
1900
+ return args[argIndex]
1901
+ })
1902
+ const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS")
1903
+ let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
1904
+
1905
+ if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) {
1906
+ template = template + "\n\n" + input.arguments
1907
+ }
1908
+
1909
+ const shellMatches = ConfigMarkdown.shell(template)
1910
+ if (shellMatches.length > 0) {
1911
+ const cfg = yield* config.get()
1912
+ const sh = Shell.preferred(cfg.shell)
1913
+ const results = yield* Effect.promise(() =>
1914
+ Promise.all(
1915
+ shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text),
1916
+ ),
1917
+ )
1918
+ let index = 0
1919
+ template = template.replace(bashRegex, () => results[index++])
1920
+ }
1921
+ template = template.trim()
1922
+
1923
+ const taskModel = yield* Effect.gen(function* () {
1924
+ if (cmd.model) return Provider.parseModel(cmd.model)
1925
+ if (cmd.agent) {
1926
+ const cmdAgent = yield* agents.get(cmd.agent)
1927
+ if (cmdAgent?.model) return cmdAgent.model
1928
+ }
1929
+ if (input.model) return Provider.parseModel(input.model)
1930
+ return yield* currentModel(input.sessionID)
1931
+ })
1932
+
1933
+ yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
1934
+
1935
+ const agent = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
1936
+ if (!agent) {
1937
+ const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
1938
+ const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
1939
+ const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
1940
+ yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
1941
+ throw error
1942
+ }
1943
+
1944
+ const templateParts = yield* resolvePromptParts(template)
1945
+ const inputFiles = new Set(
1946
+ input.parts?.filter((part) => new URL(part.url).protocol === "file:").map((part) => fileURLToPath(part.url)),
1947
+ )
1948
+ const uniqueTemplateParts = templateParts.filter(
1949
+ (part) => part.type !== "file" || !inputFiles.has(fileURLToPath(part.url)),
1950
+ )
1951
+ const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
1952
+ const parts = isSubtask
1953
+ ? [
1954
+ {
1955
+ type: "subtask" as const,
1956
+ agent: agent.name,
1957
+ description: cmd.description ?? "",
1958
+ command: input.command,
1959
+ model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
1960
+ prompt: templateParts.find((y) => y.type === "text")?.text ?? "",
1961
+ },
1962
+ ]
1963
+ : [...uniqueTemplateParts, ...(input.parts ?? [])]
1964
+
1965
+ const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultInfo()).name) : agent.name
1966
+ const userModel = isSubtask
1967
+ ? input.model
1968
+ ? Provider.parseModel(input.model)
1969
+ : yield* currentModel(input.sessionID)
1970
+ : taskModel
1971
+
1972
+ yield* plugin.trigger(
1973
+ "command.execute.before",
1974
+ { command: input.command, sessionID: input.sessionID, arguments: input.arguments },
1975
+ { parts },
1976
+ )
1977
+
1978
+ const result = yield* prompt({
1979
+ sessionID: input.sessionID,
1980
+ messageID: input.messageID,
1981
+ model: userModel,
1982
+ agent: userAgent,
1983
+ parts,
1984
+ variant: input.variant,
1985
+ })
1986
+ yield* events.publish(Command.Event.Executed, {
1987
+ name: input.command,
1988
+ sessionID: input.sessionID,
1989
+ arguments: input.arguments,
1990
+ messageID: result.info.id,
1991
+ })
1992
+ return result
1993
+ })
1994
+
1995
+ return Service.of({
1996
+ cancel,
1997
+ prompt,
1998
+ loop,
1999
+ shell,
2000
+ command,
2001
+ resolvePromptParts,
2002
+ })
2003
+ }),
2004
+ )
2005
+
2006
+ const ModelRef = Schema.Struct({
2007
+ providerID: ProviderV2.ID,
2008
+ modelID: ModelV2.ID,
2009
+ })
2010
+
2011
+ export const PromptInput = Schema.Struct({
2012
+ sessionID: SessionID,
2013
+ messageID: Schema.optional(MessageID),
2014
+ model: Schema.optional(ModelRef),
2015
+ agent: Schema.optional(Schema.String),
2016
+ noReply: Schema.optional(Schema.Boolean),
2017
+ tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
2018
+ description:
2019
+ "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
2020
+ }),
2021
+ format: Schema.optional(SessionV1.Format),
2022
+ system: Schema.optional(Schema.String),
2023
+ variant: Schema.optional(Schema.String),
2024
+ parts: Schema.Array(
2025
+ Schema.Union([
2026
+ SessionV1.TextPartInput,
2027
+ SessionV1.FilePartInput,
2028
+ SessionV1.AgentPartInput,
2029
+ SessionV1.SubtaskPartInput,
2030
+ ]).annotate({ discriminator: "type" }),
2031
+ ),
2032
+ })
2033
+ export type PromptInput = Schema.Schema.Type<typeof PromptInput>
2034
+
2035
+ export class LoopInput extends Schema.Class<LoopInput>("SessionPrompt.LoopInput")({
2036
+ sessionID: SessionID,
2037
+ }) {}
2038
+
2039
+ export const ShellInput = Schema.Struct({
2040
+ sessionID: SessionID,
2041
+ messageID: Schema.optional(MessageID),
2042
+ agent: Schema.String,
2043
+ model: Schema.optional(ModelRef),
2044
+ command: Schema.String,
2045
+ })
2046
+ export type ShellInput = Schema.Schema.Type<typeof ShellInput>
2047
+
2048
+ export const CommandInput = Schema.Struct({
2049
+ messageID: Schema.optional(MessageID),
2050
+ sessionID: SessionID,
2051
+ agent: Schema.optional(Schema.String),
2052
+ model: Schema.optional(Schema.String),
2053
+ arguments: Schema.String,
2054
+ command: Schema.String,
2055
+ variant: Schema.optional(Schema.String),
2056
+ // Inlined (no identifier annotation) to keep the original SDK output — the
2057
+ // PromptInput call site below references FilePartInput by ref via the
2058
+ // Schema export in message-v2.ts.
2059
+ parts: Schema.optional(
2060
+ Schema.Array(
2061
+ Schema.Union([
2062
+ Schema.Struct({
2063
+ id: Schema.optional(PartID),
2064
+ type: Schema.Literal("file"),
2065
+ mime: Schema.String,
2066
+ filename: Schema.optional(Schema.String),
2067
+ url: Schema.String,
2068
+ source: Schema.optional(SessionV1.FilePartSource),
2069
+ }),
2070
+ ]).annotate({ discriminator: "type" }),
2071
+ ),
2072
+ ),
2073
+ })
2074
+ export type CommandInput = Schema.Schema.Type<typeof CommandInput>
2075
+
2076
+ /** @internal Exported for testing */
2077
+ export function createStructuredOutputTool(input: {
2078
+ schema: Record<string, any>
2079
+ onSuccess: (output: unknown) => void
2080
+ }): AITool {
2081
+ // Remove $schema property if present (not needed for tool input)
2082
+ const { $schema: _, ...toolSchema } = input.schema
2083
+
2084
+ return tool({
2085
+ description: STRUCTURED_OUTPUT_DESCRIPTION,
2086
+ inputSchema: jsonSchema(toolSchema as JSONSchema7),
2087
+ async execute(args) {
2088
+ // AI SDK validates args against inputSchema before calling execute()
2089
+ input.onSuccess(args)
2090
+ return {
2091
+ output: "Structured output captured successfully.",
2092
+ title: "Structured Output",
2093
+ metadata: { valid: true },
2094
+ }
2095
+ },
2096
+ toModelOutput({ output }) {
2097
+ return {
2098
+ type: "text",
2099
+ value: output.output,
2100
+ }
2101
+ },
2102
+ })
2103
+ }
2104
+ const bashRegex = /!`([^`]+)`/g
2105
+ // Match [Image N] as single token, quoted strings, or non-space sequences
2106
+ const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
2107
+ const placeholderRegex = /\$(\d+)/g
2108
+ const quoteTrimRegex = /^["']|["']$/g
2109
+
2110
+ export const node = LayerNode.make({
2111
+ service: Service,
2112
+ layer: layer,
2113
+ deps: [
2114
+ SessionStatus.node,
2115
+ Session.node,
2116
+ Agent.node,
2117
+ Provider.node,
2118
+ SessionProcessor.node,
2119
+ SessionCompaction.node,
2120
+ Plugin.node,
2121
+ Command.node,
2122
+ Config.node,
2123
+ Permission.node,
2124
+ FSUtil.node,
2125
+ Ripgrep.node,
2126
+ MCP.node,
2127
+ LSP.node,
2128
+ ToolRegistry.node,
2129
+ Truncate.node,
2130
+ Image.node,
2131
+ CrossSpawnSpawner.node,
2132
+ Instruction.node,
2133
+ SessionRunState.node,
2134
+ SessionRevert.node,
2135
+ SessionSummary.node,
2136
+ SystemPrompt.node,
2137
+ LLM.node,
2138
+ EventV2Bridge.node,
2139
+ RuntimeFlags.node,
2140
+ Database.node,
2141
+ GitAutoCommit.node,
2142
+ Hooks.node,
2143
+ SkillRestriction.node,
2144
+ Skill.node,
2145
+ Git.node,
2146
+ AppProcess.node,
2147
+ RateLimit.node,
2148
+ ],
2149
+ })
2150
+
2151
+ export * as SessionPrompt from "./prompt"