mccode-ai 1.18.21

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 (766) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/bin/mccode +199 -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 +171 -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 +246 -0
  14. package/script/generate.ts +14 -0
  15. package/script/httpapi-exercise.ts +1 -0
  16. package/script/postinstall.mjs +189 -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 +476 -0
  58. package/src/agent/generate.txt +75 -0
  59. package/src/agent/prompt/compaction.txt +5 -0
  60. package/src/agent/prompt/explore.txt +18 -0
  61. package/src/agent/prompt/mc.txt +52 -0
  62. package/src/agent/prompt/summary.txt +11 -0
  63. package/src/agent/prompt/title.txt +44 -0
  64. package/src/agent/subagent-permissions.ts +27 -0
  65. package/src/audio.d.ts +14 -0
  66. package/src/auth/index.ts +97 -0
  67. package/src/background/job.ts +37 -0
  68. package/src/bus/global.ts +22 -0
  69. package/src/cli/bootstrap.ts +11 -0
  70. package/src/cli/cmd/account.ts +264 -0
  71. package/src/cli/cmd/acp.ts +73 -0
  72. package/src/cli/cmd/agent.ts +259 -0
  73. package/src/cli/cmd/attach.ts +148 -0
  74. package/src/cli/cmd/cmd.ts +7 -0
  75. package/src/cli/cmd/db.ts +62 -0
  76. package/src/cli/cmd/debug/agent.handler.ts +193 -0
  77. package/src/cli/cmd/debug/agent.ts +27 -0
  78. package/src/cli/cmd/debug/config.ts +14 -0
  79. package/src/cli/cmd/debug/file.ts +73 -0
  80. package/src/cli/cmd/debug/index.ts +87 -0
  81. package/src/cli/cmd/debug/lsp.ts +50 -0
  82. package/src/cli/cmd/debug/ripgrep.ts +79 -0
  83. package/src/cli/cmd/debug/scrap.ts +16 -0
  84. package/src/cli/cmd/debug/skill.ts +15 -0
  85. package/src/cli/cmd/debug/snapshot.ts +50 -0
  86. package/src/cli/cmd/debug/startup.ts +11 -0
  87. package/src/cli/cmd/debug/v2.ts +42 -0
  88. package/src/cli/cmd/export.ts +292 -0
  89. package/src/cli/cmd/generate.ts +54 -0
  90. package/src/cli/cmd/github.handler.ts +1599 -0
  91. package/src/cli/cmd/github.shared.ts +30 -0
  92. package/src/cli/cmd/github.ts +42 -0
  93. package/src/cli/cmd/import.ts +230 -0
  94. package/src/cli/cmd/mcp.ts +840 -0
  95. package/src/cli/cmd/models.ts +66 -0
  96. package/src/cli/cmd/plug.ts +230 -0
  97. package/src/cli/cmd/pr.ts +115 -0
  98. package/src/cli/cmd/prompt-display.ts +1 -0
  99. package/src/cli/cmd/providers.ts +534 -0
  100. package/src/cli/cmd/run/demo.ts +1274 -0
  101. package/src/cli/cmd/run/entry.body.ts +205 -0
  102. package/src/cli/cmd/run/footer.command.tsx +1070 -0
  103. package/src/cli/cmd/run/footer.menu.tsx +351 -0
  104. package/src/cli/cmd/run/footer.permission.tsx +472 -0
  105. package/src/cli/cmd/run/footer.prompt.tsx +1306 -0
  106. package/src/cli/cmd/run/footer.question.tsx +573 -0
  107. package/src/cli/cmd/run/footer.subagent.tsx +175 -0
  108. package/src/cli/cmd/run/footer.ts +1129 -0
  109. package/src/cli/cmd/run/footer.view.tsx +945 -0
  110. package/src/cli/cmd/run/footer.width.ts +27 -0
  111. package/src/cli/cmd/run/permission.shared.ts +256 -0
  112. package/src/cli/cmd/run/prompt.editor.ts +157 -0
  113. package/src/cli/cmd/run/prompt.shared.ts +153 -0
  114. package/src/cli/cmd/run/question.shared.ts +340 -0
  115. package/src/cli/cmd/run/runtime.boot.ts +202 -0
  116. package/src/cli/cmd/run/runtime.lifecycle.ts +406 -0
  117. package/src/cli/cmd/run/runtime.queue.ts +349 -0
  118. package/src/cli/cmd/run/runtime.shared.ts +17 -0
  119. package/src/cli/cmd/run/runtime.stdin.ts +37 -0
  120. package/src/cli/cmd/run/runtime.ts +814 -0
  121. package/src/cli/cmd/run/scrollback.shared.ts +92 -0
  122. package/src/cli/cmd/run/scrollback.surface.ts +431 -0
  123. package/src/cli/cmd/run/scrollback.writer.tsx +352 -0
  124. package/src/cli/cmd/run/session-data.ts +1113 -0
  125. package/src/cli/cmd/run/session-replay.ts +374 -0
  126. package/src/cli/cmd/run/session.shared.ts +196 -0
  127. package/src/cli/cmd/run/splash.ts +280 -0
  128. package/src/cli/cmd/run/stream.transport.ts +1462 -0
  129. package/src/cli/cmd/run/stream.ts +175 -0
  130. package/src/cli/cmd/run/subagent-data.ts +876 -0
  131. package/src/cli/cmd/run/theme.ts +690 -0
  132. package/src/cli/cmd/run/tool.ts +1486 -0
  133. package/src/cli/cmd/run/trace.ts +94 -0
  134. package/src/cli/cmd/run/turn-summary.ts +47 -0
  135. package/src/cli/cmd/run/types.ts +350 -0
  136. package/src/cli/cmd/run/variant.shared.ts +216 -0
  137. package/src/cli/cmd/run.ts +1016 -0
  138. package/src/cli/cmd/serve.ts +24 -0
  139. package/src/cli/cmd/session.ts +147 -0
  140. package/src/cli/cmd/stats.ts +393 -0
  141. package/src/cli/cmd/tui.ts +309 -0
  142. package/src/cli/cmd/uninstall.ts +353 -0
  143. package/src/cli/cmd/upgrade.ts +74 -0
  144. package/src/cli/cmd/web.ts +84 -0
  145. package/src/cli/effect/prompt.ts +37 -0
  146. package/src/cli/effect-cmd.ts +96 -0
  147. package/src/cli/error.ts +130 -0
  148. package/src/cli/heap.ts +45 -0
  149. package/src/cli/logo.ts +1 -0
  150. package/src/cli/network.ts +80 -0
  151. package/src/cli/tui/layer.ts +8 -0
  152. package/src/cli/tui/validate-session.ts +29 -0
  153. package/src/cli/tui/worker.ts +80 -0
  154. package/src/cli/ui.ts +127 -0
  155. package/src/cli/upgrade.ts +53 -0
  156. package/src/command/index.ts +201 -0
  157. package/src/command/template/initialize.txt +66 -0
  158. package/src/command/template/mcstatus.txt +1 -0
  159. package/src/command/template/new.txt +5 -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 +681 -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/git/index.ts +348 -0
  202. package/src/id/id.ts +80 -0
  203. package/src/ide/index.ts +54 -0
  204. package/src/image/image.ts +172 -0
  205. package/src/index.ts +142 -0
  206. package/src/installation/index.ts +336 -0
  207. package/src/lsp/client.ts +650 -0
  208. package/src/lsp/diagnostic.ts +29 -0
  209. package/src/lsp/language.ts +121 -0
  210. package/src/lsp/launch.ts +21 -0
  211. package/src/lsp/lsp.ts +507 -0
  212. package/src/lsp/server.ts +1983 -0
  213. package/src/markdown.d.ts +4 -0
  214. package/src/mcp/auth.ts +163 -0
  215. package/src/mcp/browser.ts +37 -0
  216. package/src/mcp/catalog.ts +170 -0
  217. package/src/mcp/index.ts +1004 -0
  218. package/src/mcp/oauth-callback.ts +194 -0
  219. package/src/mcp/oauth-provider.ts +259 -0
  220. package/src/node.ts +4 -0
  221. package/src/patch/index.ts +686 -0
  222. package/src/permission/arity.ts +163 -0
  223. package/src/permission/evaluate.ts +1 -0
  224. package/src/permission/index.ts +223 -0
  225. package/src/plugin/azure.ts +26 -0
  226. package/src/plugin/cerebras.ts +11 -0
  227. package/src/plugin/cloudflare.ts +65 -0
  228. package/src/plugin/digitalocean.ts +325 -0
  229. package/src/plugin/github-copilot/copilot.ts +414 -0
  230. package/src/plugin/github-copilot/models.ts +261 -0
  231. package/src/plugin/index.ts +318 -0
  232. package/src/plugin/install.ts +439 -0
  233. package/src/plugin/loader.ts +237 -0
  234. package/src/plugin/meta.ts +188 -0
  235. package/src/plugin/modal/modal.ts +17 -0
  236. package/src/plugin/modal/models.ts +133 -0
  237. package/src/plugin/openai/README.md +31 -0
  238. package/src/plugin/openai/codex.ts +572 -0
  239. package/src/plugin/openai/ws-pool.ts +271 -0
  240. package/src/plugin/openai/ws.ts +383 -0
  241. package/src/plugin/pty-environment.ts +24 -0
  242. package/src/plugin/shared.ts +323 -0
  243. package/src/plugin/snowflake-cortex.ts +507 -0
  244. package/src/plugin/tui/internal.ts +10 -0
  245. package/src/plugin/tui/runtime.ts +1131 -0
  246. package/src/plugin/xai.ts +339 -0
  247. package/src/project/bootstrap-service.ts +9 -0
  248. package/src/project/bootstrap.ts +58 -0
  249. package/src/project/instance-context.ts +24 -0
  250. package/src/project/instance-runtime.ts +16 -0
  251. package/src/project/instance-store.ts +213 -0
  252. package/src/project/project.ts +483 -0
  253. package/src/project/vcs.ts +423 -0
  254. package/src/provider/auth.ts +229 -0
  255. package/src/provider/error.ts +195 -0
  256. package/src/provider/model-status.ts +8 -0
  257. package/src/provider/provider.ts +2047 -0
  258. package/src/provider/transform.ts +1856 -0
  259. package/src/question/index.ts +161 -0
  260. package/src/question/schema.ts +4 -0
  261. package/src/server/auth.ts +48 -0
  262. package/src/server/event.ts +10 -0
  263. package/src/server/global-lifecycle.ts +28 -0
  264. package/src/server/init-projectors.ts +3 -0
  265. package/src/server/mdns.ts +47 -0
  266. package/src/server/projectors.ts +1 -0
  267. package/src/server/proxy-util.ts +48 -0
  268. package/src/server/routes/instance/httpapi/AGENTS.md +39 -0
  269. package/src/server/routes/instance/httpapi/api.ts +97 -0
  270. package/src/server/routes/instance/httpapi/errors.ts +193 -0
  271. package/src/server/routes/instance/httpapi/groups/config.ts +65 -0
  272. package/src/server/routes/instance/httpapi/groups/control-plane.ts +35 -0
  273. package/src/server/routes/instance/httpapi/groups/control.ts +76 -0
  274. package/src/server/routes/instance/httpapi/groups/event.ts +29 -0
  275. package/src/server/routes/instance/httpapi/groups/experimental.ts +275 -0
  276. package/src/server/routes/instance/httpapi/groups/file.ts +185 -0
  277. package/src/server/routes/instance/httpapi/groups/global.ts +136 -0
  278. package/src/server/routes/instance/httpapi/groups/instance.ts +206 -0
  279. package/src/server/routes/instance/httpapi/groups/mcp.ts +156 -0
  280. package/src/server/routes/instance/httpapi/groups/metadata.ts +18 -0
  281. package/src/server/routes/instance/httpapi/groups/permission.ts +61 -0
  282. package/src/server/routes/instance/httpapi/groups/project-copy.ts +32 -0
  283. package/src/server/routes/instance/httpapi/groups/project.ts +93 -0
  284. package/src/server/routes/instance/httpapi/groups/provider.ts +101 -0
  285. package/src/server/routes/instance/httpapi/groups/pty.ts +172 -0
  286. package/src/server/routes/instance/httpapi/groups/query.ts +12 -0
  287. package/src/server/routes/instance/httpapi/groups/question.ts +74 -0
  288. package/src/server/routes/instance/httpapi/groups/session.ts +462 -0
  289. package/src/server/routes/instance/httpapi/groups/sync.ts +113 -0
  290. package/src/server/routes/instance/httpapi/groups/tui.ts +208 -0
  291. package/src/server/routes/instance/httpapi/groups/workspace.ts +141 -0
  292. package/src/server/routes/instance/httpapi/handlers/config.ts +34 -0
  293. package/src/server/routes/instance/httpapi/handlers/control-plane.ts +37 -0
  294. package/src/server/routes/instance/httpapi/handlers/control.ts +43 -0
  295. package/src/server/routes/instance/httpapi/handlers/event.ts +99 -0
  296. package/src/server/routes/instance/httpapi/handlers/experimental.ts +193 -0
  297. package/src/server/routes/instance/httpapi/handlers/file.ts +139 -0
  298. package/src/server/routes/instance/httpapi/handlers/global.ts +156 -0
  299. package/src/server/routes/instance/httpapi/handlers/instance.ts +110 -0
  300. package/src/server/routes/instance/httpapi/handlers/mcp.ts +111 -0
  301. package/src/server/routes/instance/httpapi/handlers/permission.ts +41 -0
  302. package/src/server/routes/instance/httpapi/handlers/project-copy.ts +83 -0
  303. package/src/server/routes/instance/httpapi/handlers/project.ts +63 -0
  304. package/src/server/routes/instance/httpapi/handlers/provider.ts +116 -0
  305. package/src/server/routes/instance/httpapi/handlers/pty.ts +273 -0
  306. package/src/server/routes/instance/httpapi/handlers/question.ts +54 -0
  307. package/src/server/routes/instance/httpapi/handlers/session-errors.ts +21 -0
  308. package/src/server/routes/instance/httpapi/handlers/session.ts +442 -0
  309. package/src/server/routes/instance/httpapi/handlers/sync.ts +89 -0
  310. package/src/server/routes/instance/httpapi/handlers/tui.ts +131 -0
  311. package/src/server/routes/instance/httpapi/handlers/workspace.ts +102 -0
  312. package/src/server/routes/instance/httpapi/lifecycle.ts +54 -0
  313. package/src/server/routes/instance/httpapi/middleware/authorization.ts +150 -0
  314. package/src/server/routes/instance/httpapi/middleware/compression.ts +64 -0
  315. package/src/server/routes/instance/httpapi/middleware/cors-vary.ts +29 -0
  316. package/src/server/routes/instance/httpapi/middleware/error.ts +43 -0
  317. package/src/server/routes/instance/httpapi/middleware/fence.ts +25 -0
  318. package/src/server/routes/instance/httpapi/middleware/instance-context.ts +43 -0
  319. package/src/server/routes/instance/httpapi/middleware/proxy.ts +131 -0
  320. package/src/server/routes/instance/httpapi/middleware/schema-error.ts +41 -0
  321. package/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +250 -0
  322. package/src/server/routes/instance/httpapi/public.ts +537 -0
  323. package/src/server/routes/instance/httpapi/server.ts +325 -0
  324. package/src/server/routes/instance/httpapi/websocket-tracker.ts +60 -0
  325. package/src/server/server.ts +226 -0
  326. package/src/server/shared/fence.ts +60 -0
  327. package/src/server/shared/pty-ticket.ts +15 -0
  328. package/src/server/shared/public-ui.ts +12 -0
  329. package/src/server/shared/tui-control.ts +28 -0
  330. package/src/server/shared/ui.ts +108 -0
  331. package/src/server/shared/workspace-routing.ts +45 -0
  332. package/src/server/tui-event.ts +1 -0
  333. package/src/session/compaction.ts +608 -0
  334. package/src/session/instruction.ts +237 -0
  335. package/src/session/llm/AGENTS.md +90 -0
  336. package/src/session/llm/ai-sdk.ts +291 -0
  337. package/src/session/llm/native-request.ts +196 -0
  338. package/src/session/llm/native-runtime.ts +195 -0
  339. package/src/session/llm/request.ts +226 -0
  340. package/src/session/llm.ts +404 -0
  341. package/src/session/message-error.ts +14 -0
  342. package/src/session/message-v2.ts +737 -0
  343. package/src/session/message.ts +148 -0
  344. package/src/session/overflow.ts +34 -0
  345. package/src/session/processor.ts +718 -0
  346. package/src/session/prompt/anthropic.txt +110 -0
  347. package/src/session/prompt/beast.txt +152 -0
  348. package/src/session/prompt/build-switch.txt +5 -0
  349. package/src/session/prompt/codex.txt +84 -0
  350. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  351. package/src/session/prompt/default.txt +100 -0
  352. package/src/session/prompt/gemini.txt +160 -0
  353. package/src/session/prompt/gpt.txt +112 -0
  354. package/src/session/prompt/kimi.txt +100 -0
  355. package/src/session/prompt/meta.txt +70 -0
  356. package/src/session/prompt/plan-mode.txt +70 -0
  357. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  358. package/src/session/prompt/plan.txt +26 -0
  359. package/src/session/prompt/trinity.txt +102 -0
  360. package/src/session/prompt.ts +1631 -0
  361. package/src/session/reminders.ts +92 -0
  362. package/src/session/retry.ts +209 -0
  363. package/src/session/revert.ts +136 -0
  364. package/src/session/run-state.ts +151 -0
  365. package/src/session/schema.ts +26 -0
  366. package/src/session/session.ts +1016 -0
  367. package/src/session/status.ts +56 -0
  368. package/src/session/summary.ts +160 -0
  369. package/src/session/system.ts +152 -0
  370. package/src/session/todo.ts +74 -0
  371. package/src/session/tools.ts +590 -0
  372. package/src/share/session.ts +58 -0
  373. package/src/share/share-next.ts +371 -0
  374. package/src/skill/discovery.ts +140 -0
  375. package/src/skill/index.ts +354 -0
  376. package/src/snapshot/index.ts +807 -0
  377. package/src/sql.d.ts +4 -0
  378. package/src/storage/schema.ts +5 -0
  379. package/src/storage/storage.ts +327 -0
  380. package/src/sync/README.md +179 -0
  381. package/src/sync/schema.ts +11 -0
  382. package/src/temporary.ts +31 -0
  383. package/src/tool/apply_patch.ts +313 -0
  384. package/src/tool/apply_patch.txt +33 -0
  385. package/src/tool/code-mode.ts +310 -0
  386. package/src/tool/edit.ts +737 -0
  387. package/src/tool/edit.txt +10 -0
  388. package/src/tool/external-directory.ts +49 -0
  389. package/src/tool/glob.ts +76 -0
  390. package/src/tool/glob.txt +6 -0
  391. package/src/tool/grep.ts +115 -0
  392. package/src/tool/grep.txt +8 -0
  393. package/src/tool/invalid.ts +21 -0
  394. package/src/tool/json-schema.ts +164 -0
  395. package/src/tool/lsp.ts +113 -0
  396. package/src/tool/lsp.txt +24 -0
  397. package/src/tool/mcp-websearch.ts +96 -0
  398. package/src/tool/plan-enter.txt +14 -0
  399. package/src/tool/plan-exit.txt +13 -0
  400. package/src/tool/plan.ts +79 -0
  401. package/src/tool/question.ts +44 -0
  402. package/src/tool/question.txt +10 -0
  403. package/src/tool/read.ts +386 -0
  404. package/src/tool/read.txt +14 -0
  405. package/src/tool/registry.ts +455 -0
  406. package/src/tool/schema.ts +14 -0
  407. package/src/tool/shell/id.ts +19 -0
  408. package/src/tool/shell/prompt.ts +293 -0
  409. package/src/tool/shell/shell.txt +21 -0
  410. package/src/tool/shell.ts +645 -0
  411. package/src/tool/skill.ts +70 -0
  412. package/src/tool/skill.txt +5 -0
  413. package/src/tool/task.ts +371 -0
  414. package/src/tool/task.txt +19 -0
  415. package/src/tool/todo.ts +46 -0
  416. package/src/tool/todowrite.txt +44 -0
  417. package/src/tool/tool.ts +183 -0
  418. package/src/tool/truncate.ts +156 -0
  419. package/src/tool/truncation-dir.ts +4 -0
  420. package/src/tool/webfetch.ts +192 -0
  421. package/src/tool/webfetch.txt +13 -0
  422. package/src/tool/websearch.ts +143 -0
  423. package/src/tool/websearch.txt +14 -0
  424. package/src/tool/write.ts +104 -0
  425. package/src/tool/write.txt +8 -0
  426. package/src/util/archive.ts +17 -0
  427. package/src/util/bom.ts +27 -0
  428. package/src/util/data-url.ts +9 -0
  429. package/src/util/defer.ts +10 -0
  430. package/src/util/effect-http-client.ts +11 -0
  431. package/src/util/error.ts +1 -0
  432. package/src/util/filesystem.ts +251 -0
  433. package/src/util/html.ts +8 -0
  434. package/src/util/iife.ts +3 -0
  435. package/src/util/lazy.ts +20 -0
  436. package/src/util/local-context.ts +25 -0
  437. package/src/util/locale.ts +2 -0
  438. package/src/util/media.ts +26 -0
  439. package/src/util/process.ts +177 -0
  440. package/src/util/proxy-env.ts +72 -0
  441. package/src/util/queue.ts +32 -0
  442. package/src/util/record.ts +1 -0
  443. package/src/util/repository.ts +232 -0
  444. package/src/util/rpc.ts +66 -0
  445. package/src/util/signal.ts +12 -0
  446. package/src/util/timeout.ts +13 -0
  447. package/src/util/token.ts +1 -0
  448. package/src/util/wildcard.ts +59 -0
  449. package/src/worktree/index.ts +623 -0
  450. package/sst-env.d.ts +10 -0
  451. package/test/AGENTS.md +204 -0
  452. package/test/EFFECT_TEST_MIGRATION.md +169 -0
  453. package/test/account/repo.test.ts +355 -0
  454. package/test/account/service.test.ts +524 -0
  455. package/test/acp/config-option.test.ts +229 -0
  456. package/test/acp/content.test.ts +235 -0
  457. package/test/acp/directory.test.ts +188 -0
  458. package/test/acp/error.test.ts +67 -0
  459. package/test/acp/event.test.ts +751 -0
  460. package/test/acp/permission.test.ts +401 -0
  461. package/test/acp/service-session.test.ts +1374 -0
  462. package/test/acp/session.test.ts +201 -0
  463. package/test/acp/tool.test.ts +298 -0
  464. package/test/acp/usage.test.ts +318 -0
  465. package/test/agent/agent.test.ts +755 -0
  466. package/test/agent/plan-mode-subagent-bypass.test.ts +160 -0
  467. package/test/agent/plugin-agent-regression.test.ts +51 -0
  468. package/test/auth/auth.test.ts +75 -0
  469. package/test/background/job.test.ts +244 -0
  470. package/test/cli/account.test.ts +30 -0
  471. package/test/cli/acp/acp-test-client.ts +97 -0
  472. package/test/cli/acp/config-options.test.ts +103 -0
  473. package/test/cli/acp/helpers.ts +96 -0
  474. package/test/cli/acp/initialize-auth.test.ts +61 -0
  475. package/test/cli/acp/lifecycle.test.ts +118 -0
  476. package/test/cli/acp/prompt-content.test.ts +97 -0
  477. package/test/cli/acp/skills.test.ts +38 -0
  478. package/test/cli/cmd/tui/attention.test.ts +484 -0
  479. package/test/cli/effect-cmd-instance-als.test.ts +40 -0
  480. package/test/cli/error.test.ts +95 -0
  481. package/test/cli/github-action.test.ts +199 -0
  482. package/test/cli/github-remote.test.ts +90 -0
  483. package/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +623 -0
  484. package/test/cli/help/help-snapshots.test.ts +140 -0
  485. package/test/cli/import.test.ts +90 -0
  486. package/test/cli/mcp-add.test.ts +74 -0
  487. package/test/cli/plugin-auth-picker.test.ts +120 -0
  488. package/test/cli/run/entry.body.test.ts +536 -0
  489. package/test/cli/run/footer.menu.test.ts +43 -0
  490. package/test/cli/run/footer.view.test.tsx +1411 -0
  491. package/test/cli/run/footer.width.test.ts +35 -0
  492. package/test/cli/run/permission.shared.test.ts +144 -0
  493. package/test/cli/run/prompt.editor.test.ts +101 -0
  494. package/test/cli/run/prompt.shared.test.ts +101 -0
  495. package/test/cli/run/question.shared.test.ts +115 -0
  496. package/test/cli/run/run-process.test.ts +338 -0
  497. package/test/cli/run/runtime.boot.test.ts +283 -0
  498. package/test/cli/run/runtime.queue.test.ts +481 -0
  499. package/test/cli/run/runtime.stdin.test.ts +71 -0
  500. package/test/cli/run/runtime.test.ts +238 -0
  501. package/test/cli/run/scrollback.surface.test.ts +1092 -0
  502. package/test/cli/run/session-data.test.ts +593 -0
  503. package/test/cli/run/session-replay.test.ts +691 -0
  504. package/test/cli/run/session.shared.test.ts +247 -0
  505. package/test/cli/run/stream.test.ts +56 -0
  506. package/test/cli/run/stream.transport.test.ts +2363 -0
  507. package/test/cli/run/subagent-data.test.ts +547 -0
  508. package/test/cli/run/theme.test.ts +177 -0
  509. package/test/cli/run/variant.shared.test.ts +218 -0
  510. package/test/cli/serve/serve-process.test.ts +61 -0
  511. package/test/cli/smokes/read-only.test.ts +115 -0
  512. package/test/cli/tui/attach.test.ts +11 -0
  513. package/test/cli/tui/editor-context-zed.test.ts +379 -0
  514. package/test/cli/tui/editor-context.test.tsx +297 -0
  515. package/test/cli/tui/plugin-add.test.ts +110 -0
  516. package/test/cli/tui/plugin-install.test.ts +87 -0
  517. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  518. package/test/cli/tui/plugin-loader-entrypoint.test.ts +485 -0
  519. package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
  520. package/test/cli/tui/plugin-loader.test.ts +1332 -0
  521. package/test/cli/tui/plugin-toggle.test.ts +264 -0
  522. package/test/cli/tui/thread.test.ts +101 -0
  523. package/test/config/agent-color.test.ts +47 -0
  524. package/test/config/config.test.ts +2047 -0
  525. package/test/config/entry-name.test.ts +57 -0
  526. package/test/config/fixtures/empty-frontmatter.md +4 -0
  527. package/test/config/fixtures/frontmatter.md +28 -0
  528. package/test/config/fixtures/markdown-header.md +11 -0
  529. package/test/config/fixtures/no-frontmatter.md +1 -0
  530. package/test/config/fixtures/weird-model-id.md +13 -0
  531. package/test/config/lsp.test.ts +69 -0
  532. package/test/config/markdown.test.ts +228 -0
  533. package/test/config/plugin.test.ts +0 -0
  534. package/test/config/tui.test.ts +894 -0
  535. package/test/control-plane/adapters.test.ts +71 -0
  536. package/test/control-plane/workspace.test.ts +1717 -0
  537. package/test/effect/app-runtime-logger.test.ts +101 -0
  538. package/test/effect/config-service.test.ts +65 -0
  539. package/test/effect/instance-state.test.ts +392 -0
  540. package/test/effect/run-service.test.ts +89 -0
  541. package/test/effect/runner.test.ts +514 -0
  542. package/test/effect/runtime-flags.test.ts +374 -0
  543. package/test/event-manifest.test.ts +24 -0
  544. package/test/fake/account.ts +9 -0
  545. package/test/fake/auth.ts +8 -0
  546. package/test/fake/npm.ts +8 -0
  547. package/test/fake/provider.ts +82 -0
  548. package/test/fake/skill.ts +8 -0
  549. package/test/filesystem/filesystem.test.ts +318 -0
  550. package/test/fixture/agent-plugin.constants.ts +6 -0
  551. package/test/fixture/agent-plugin.ts +12 -0
  552. package/test/fixture/config.ts +23 -0
  553. package/test/fixture/db.ts +11 -0
  554. package/test/fixture/fixture.test.ts +26 -0
  555. package/test/fixture/fixture.ts +228 -0
  556. package/test/fixture/flag.ts +20 -0
  557. package/test/fixture/flock-worker.ts +72 -0
  558. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  559. package/test/fixture/mcp-lifecycle-stdio.ts +26 -0
  560. package/test/fixture/mcp-session-recovery.ts +50 -0
  561. package/test/fixture/plug-worker.ts +93 -0
  562. package/test/fixture/plugin-meta-worker.ts +19 -0
  563. package/test/fixture/plugin.ts +10 -0
  564. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  565. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  566. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  567. package/test/fixture/skills/index.json +6 -0
  568. package/test/fixture/tui-environment.tsx +32 -0
  569. package/test/fixture/tui-plugin.ts +355 -0
  570. package/test/fixture/tui-runtime.ts +56 -0
  571. package/test/fixture/tui-sdk.ts +82 -0
  572. package/test/fixture/workspace.ts +34 -0
  573. package/test/fixtures/recordings/session/native-anthropic-tool-loop.json +49 -0
  574. package/test/fixtures/recordings/session/native-openai-oauth-tool-loop.json +45 -0
  575. package/test/fixtures/recordings/session/native-zen-tool-loop.json +49 -0
  576. package/test/format/format.test.ts +235 -0
  577. package/test/git/git.test.ts +179 -0
  578. package/test/ide/ide.test.ts +82 -0
  579. package/test/image/fixtures/picture-5mb-base64.png +0 -0
  580. package/test/image/image.test.ts +121 -0
  581. package/test/installation/installation.test.ts +240 -0
  582. package/test/lib/cli-process.ts +535 -0
  583. package/test/lib/effect.ts +177 -0
  584. package/test/lib/filesystem.ts +10 -0
  585. package/test/lib/llm-server.ts +779 -0
  586. package/test/lib/snapshot.ts +73 -0
  587. package/test/lib/test-provider.ts +37 -0
  588. package/test/lib/websocket.ts +46 -0
  589. package/test/lsp/client.test.ts +488 -0
  590. package/test/lsp/index.test.ts +231 -0
  591. package/test/lsp/jdtls-root.test.ts +459 -0
  592. package/test/lsp/launch.test.ts +22 -0
  593. package/test/lsp/lifecycle.test.ts +160 -0
  594. package/test/mcp/auth.test.ts +78 -0
  595. package/test/mcp/catalog.test.ts +107 -0
  596. package/test/mcp/headers.test.ts +102 -0
  597. package/test/mcp/lifecycle.test.ts +560 -0
  598. package/test/mcp/oauth-auto-connect.test.ts +300 -0
  599. package/test/mcp/oauth-browser.test.ts +225 -0
  600. package/test/mcp/oauth-callback.test.ts +114 -0
  601. package/test/mcp/oauth-provider.test.ts +102 -0
  602. package/test/mcp/session-recovery.test.ts +27 -0
  603. package/test/mcp/transport.test.ts +38 -0
  604. package/test/patch/patch.test.ts +384 -0
  605. package/test/permission/arity.test.ts +33 -0
  606. package/test/permission/next.test.ts +1174 -0
  607. package/test/permission-task.test.ts +319 -0
  608. package/test/plugin/auth-override.test.ts +101 -0
  609. package/test/plugin/cerebras.test.ts +47 -0
  610. package/test/plugin/cloudflare.test.ts +25 -0
  611. package/test/plugin/codex.test.ts +463 -0
  612. package/test/plugin/github-copilot-models.test.ts +492 -0
  613. package/test/plugin/install-concurrency.test.ts +140 -0
  614. package/test/plugin/install.test.ts +570 -0
  615. package/test/plugin/loader-shared.test.ts +1306 -0
  616. package/test/plugin/meta.test.ts +137 -0
  617. package/test/plugin/modal-models.test.ts +198 -0
  618. package/test/plugin/openai-rollout.test.ts +17 -0
  619. package/test/plugin/openai-ws.test.ts +909 -0
  620. package/test/plugin/shared.test.ts +88 -0
  621. package/test/plugin/snowflake-cortex.test.ts +278 -0
  622. package/test/plugin/trigger.test.ts +108 -0
  623. package/test/plugin/workspace-adapter.test.ts +111 -0
  624. package/test/plugin/xai.test.ts +585 -0
  625. package/test/preload.ts +92 -0
  626. package/test/project/instance-bootstrap.test.ts +115 -0
  627. package/test/project/instance.test.ts +248 -0
  628. package/test/project/migrate-global.test.ts +168 -0
  629. package/test/project/project-directory.test.ts +202 -0
  630. package/test/project/project.test.ts +808 -0
  631. package/test/project/vcs.test.ts +335 -0
  632. package/test/project/worktree-remove.test.ts +128 -0
  633. package/test/project/worktree.test.ts +324 -0
  634. package/test/provider/amazon-bedrock.test.ts +361 -0
  635. package/test/provider/cf-ai-gateway-e2e.test.ts +308 -0
  636. package/test/provider/digitalocean.test.ts +124 -0
  637. package/test/provider/error.test.ts +24 -0
  638. package/test/provider/gitlab-duo.test.ts +412 -0
  639. package/test/provider/header-timeout.test.ts +234 -0
  640. package/test/provider/model-status.test.ts +61 -0
  641. package/test/provider/provider.test.ts +2118 -0
  642. package/test/provider/transform.test.ts +5682 -0
  643. package/test/question/question.test.ts +459 -0
  644. package/test/server/AGENTS.md +15 -0
  645. package/test/server/auth.test.ts +59 -0
  646. package/test/server/global-bus.ts +31 -0
  647. package/test/server/global-session-list.test.ts +108 -0
  648. package/test/server/httpapi-authorization.test.ts +174 -0
  649. package/test/server/httpapi-compression.test.ts +151 -0
  650. package/test/server/httpapi-config.test.ts +110 -0
  651. package/test/server/httpapi-control-plane.test.ts +63 -0
  652. package/test/server/httpapi-cors-vary.test.ts +63 -0
  653. package/test/server/httpapi-cors.test.ts +122 -0
  654. package/test/server/httpapi-error-middleware.test.ts +101 -0
  655. package/test/server/httpapi-event.test.ts +94 -0
  656. package/test/server/httpapi-exercise/assertions.ts +64 -0
  657. package/test/server/httpapi-exercise/backend.ts +144 -0
  658. package/test/server/httpapi-exercise/dsl.ts +210 -0
  659. package/test/server/httpapi-exercise/environment.ts +40 -0
  660. package/test/server/httpapi-exercise/index.ts +1802 -0
  661. package/test/server/httpapi-exercise/report.ts +66 -0
  662. package/test/server/httpapi-exercise/routing.ts +96 -0
  663. package/test/server/httpapi-exercise/runner.ts +267 -0
  664. package/test/server/httpapi-exercise/runtime.ts +52 -0
  665. package/test/server/httpapi-exercise/types.ts +127 -0
  666. package/test/server/httpapi-experimental.test.ts +298 -0
  667. package/test/server/httpapi-file.test.ts +83 -0
  668. package/test/server/httpapi-global.test.ts +66 -0
  669. package/test/server/httpapi-instance-context.test.ts +352 -0
  670. package/test/server/httpapi-instance-route-auth.test.ts +81 -0
  671. package/test/server/httpapi-instance.test.ts +265 -0
  672. package/test/server/httpapi-layer.ts +33 -0
  673. package/test/server/httpapi-listen.test.ts +465 -0
  674. package/test/server/httpapi-mcp-oauth.test.ts +73 -0
  675. package/test/server/httpapi-mcp.test.ts +223 -0
  676. package/test/server/httpapi-mdns.test.ts +79 -0
  677. package/test/server/httpapi-promptasync-context.test.ts +212 -0
  678. package/test/server/httpapi-provider.test.ts +401 -0
  679. package/test/server/httpapi-pty.test.ts +299 -0
  680. package/test/server/httpapi-public-openapi.test.ts +350 -0
  681. package/test/server/httpapi-query-schema-drift.test.ts +330 -0
  682. package/test/server/httpapi-reference.test.ts +63 -0
  683. package/test/server/httpapi-schema-error-body.test.ts +166 -0
  684. package/test/server/httpapi-sdk.test.ts +913 -0
  685. package/test/server/httpapi-session.test.ts +1090 -0
  686. package/test/server/httpapi-sync.test.ts +149 -0
  687. package/test/server/httpapi-ui.test.ts +457 -0
  688. package/test/server/httpapi-v2-location.test.ts +126 -0
  689. package/test/server/httpapi-v2-pty.test.ts +250 -0
  690. package/test/server/httpapi-workspace-routing.test.ts +552 -0
  691. package/test/server/httpapi-workspace.test.ts +506 -0
  692. package/test/server/negative-tokens-regression.test.ts +84 -0
  693. package/test/server/project-copy.test.ts +127 -0
  694. package/test/server/project-init-git.test.ts +118 -0
  695. package/test/server/proxy-util.test.ts +113 -0
  696. package/test/server/sdk-error-shape.test.ts +81 -0
  697. package/test/server/sdk-v1-smoke.test.ts +57 -0
  698. package/test/server/session-actions.test.ts +110 -0
  699. package/test/server/session-diff-missing-patch.test.ts +97 -0
  700. package/test/server/session-list.test.ts +302 -0
  701. package/test/server/session-messages.test.ts +180 -0
  702. package/test/server/session-select.test.ts +67 -0
  703. package/test/server/workspace-proxy.test.ts +181 -0
  704. package/test/server/workspace-routing.test.ts +101 -0
  705. package/test/server/worktree-endpoint-repro.test.ts +307 -0
  706. package/test/session/compaction.test.ts +1975 -0
  707. package/test/session/instruction.test.ts +264 -0
  708. package/test/session/llm-native-recorded.test.ts +413 -0
  709. package/test/session/llm-native.test.ts +761 -0
  710. package/test/session/llm.test.ts +2180 -0
  711. package/test/session/message-v2.test.ts +1729 -0
  712. package/test/session/messages-pagination.test.ts +1057 -0
  713. package/test/session/processor-effect.test.ts +1171 -0
  714. package/test/session/prompt.test.ts +2470 -0
  715. package/test/session/retry.test.ts +521 -0
  716. package/test/session/revert-compact.test.ts +683 -0
  717. package/test/session/schema-decoding.test.ts +313 -0
  718. package/test/session/session-schema.test.ts +78 -0
  719. package/test/session/session.test.ts +285 -0
  720. package/test/session/snapshot-tool-race.test.ts +189 -0
  721. package/test/session/structured-output-integration.test.ts +235 -0
  722. package/test/session/structured-output.test.ts +387 -0
  723. package/test/session/system.test.ts +168 -0
  724. package/test/share/share-next.test.ts +324 -0
  725. package/test/skill/discovery.test.ts +186 -0
  726. package/test/skill/skill.test.ts +585 -0
  727. package/test/snapshot/snapshot.test.ts +1218 -0
  728. package/test/storage/storage.test.ts +297 -0
  729. package/test/tool/__snapshots__/parameters.test.ts.snap +466 -0
  730. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  731. package/test/tool/apply_patch.test.ts +529 -0
  732. package/test/tool/code-mode-integration.test.ts +331 -0
  733. package/test/tool/code-mode.test.ts +730 -0
  734. package/test/tool/edit.test.ts +574 -0
  735. package/test/tool/external-directory.test.ts +156 -0
  736. package/test/tool/fixtures/large-image.png +0 -0
  737. package/test/tool/fixtures/models-api.json +172692 -0
  738. package/test/tool/glob.test.ts +132 -0
  739. package/test/tool/grep.test.ts +223 -0
  740. package/test/tool/lsp.test.ts +184 -0
  741. package/test/tool/parameters.test.ts +290 -0
  742. package/test/tool/question.test.ts +133 -0
  743. package/test/tool/read.test.ts +608 -0
  744. package/test/tool/registry.test.ts +572 -0
  745. package/test/tool/shell.test.ts +1199 -0
  746. package/test/tool/skill.test.ts +134 -0
  747. package/test/tool/task.test.ts +1101 -0
  748. package/test/tool/tool-define.test.ts +154 -0
  749. package/test/tool/truncation.test.ts +266 -0
  750. package/test/tool/webfetch.test.ts +119 -0
  751. package/test/tool/websearch.test.ts +100 -0
  752. package/test/tool/write.test.ts +279 -0
  753. package/test/util/data-url.test.ts +14 -0
  754. package/test/util/error.test.ts +16 -0
  755. package/test/util/filesystem.test.ts +656 -0
  756. package/test/util/glob.test.ts +164 -0
  757. package/test/util/html.test.ts +15 -0
  758. package/test/util/iife.test.ts +36 -0
  759. package/test/util/lazy.test.ts +50 -0
  760. package/test/util/module.test.ts +59 -0
  761. package/test/util/process.test.ts +128 -0
  762. package/test/util/repository.test.ts +93 -0
  763. package/test/util/timeout.test.ts +21 -0
  764. package/test/util/wildcard.test.ts +90 -0
  765. package/test/v2/session-message-updater.test.ts +269 -0
  766. package/tsconfig.json +16 -0
@@ -0,0 +1,2470 @@
1
+ import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
2
+ import { SessionV1 } from "@opencode-ai/core/v1/session"
3
+ import { Database } from "@opencode-ai/core/database/database"
4
+ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
5
+ import { SessionProjector } from "@opencode-ai/core/session/projector"
6
+ import { eq } from "drizzle-orm"
7
+ import { EventV2Bridge } from "@/event-v2-bridge"
8
+ import { expect } from "bun:test"
9
+ import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
10
+ import path from "path"
11
+ import { fileURLToPath } from "url"
12
+ import { NamedError } from "@opencode-ai/core/util/error"
13
+ import { Agent as AgentSvc } from "../../src/agent/agent"
14
+ import { BackgroundJob } from "@/background/job"
15
+ import { Command } from "../../src/command"
16
+ import { Config } from "@/config/config"
17
+ import { LSP } from "@/lsp/lsp"
18
+ import { MCP } from "../../src/mcp"
19
+ import { Permission } from "../../src/permission"
20
+ import { Plugin } from "../../src/plugin"
21
+ import { Provider as ProviderSvc } from "@/provider/provider"
22
+ import { Env } from "../../src/env"
23
+ import { Git } from "../../src/git"
24
+ import { Image } from "../../src/image/image"
25
+
26
+ import { Question } from "../../src/question"
27
+ import { Todo } from "../../src/session/todo"
28
+ import { Session } from "@/session/session"
29
+ import { SessionMessageTable } from "@opencode-ai/core/session/sql"
30
+ import { LLM } from "../../src/session/llm"
31
+ import { MessageV2 } from "../../src/session/message-v2"
32
+ import { FSUtil } from "@opencode-ai/core/fs-util"
33
+ import { SessionCompaction } from "../../src/session/compaction"
34
+ import { SessionSummary } from "../../src/session/summary"
35
+ import { Instruction } from "../../src/session/instruction"
36
+ import { SessionProcessor } from "../../src/session/processor"
37
+ import { SessionPrompt } from "../../src/session/prompt"
38
+ import { SessionRevert } from "../../src/session/revert"
39
+ import { SessionRunState } from "../../src/session/run-state"
40
+ import { MessageID, PartID, SessionID } from "../../src/session/schema"
41
+ import { SessionStatus } from "../../src/session/status"
42
+ import { SessionV2 } from "@opencode-ai/core/session"
43
+ import { SessionExecution } from "@opencode-ai/core/session/execution"
44
+ import { Skill } from "../../src/skill"
45
+ import { SystemPrompt } from "../../src/session/system"
46
+ import { Shell } from "@opencode-ai/core/shell"
47
+ import { Snapshot } from "../../src/snapshot"
48
+ import { ToolRegistry } from "@/tool/registry"
49
+ import { Truncate } from "@/tool/truncate"
50
+ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
51
+ import { Ripgrep } from "@opencode-ai/core/ripgrep"
52
+ import { Format } from "../../src/format"
53
+ import { TestInstance } from "../fixture/fixture"
54
+ import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
55
+ import { reply, TestLLMServer } from "../lib/llm-server"
56
+ import { RuntimeFlags } from "@/effect/runtime-flags"
57
+ import { ProviderV2 } from "@opencode-ai/core/provider"
58
+ import { ModelV2 } from "@opencode-ai/core/model"
59
+ import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
60
+
61
+ const summary = Layer.succeed(
62
+ SessionSummary.Service,
63
+ SessionSummary.Service.of({
64
+ summarize: () => Effect.void,
65
+ diff: () => Effect.succeed([]),
66
+ computeDiff: () => Effect.succeed([]),
67
+ }),
68
+ )
69
+
70
+ const ref = {
71
+ providerID: ProviderV2.ID.make("test"),
72
+ modelID: ModelV2.ID.make("test-model"),
73
+ }
74
+
75
+ function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
76
+ return Effect.acquireUseRelease(
77
+ Effect.sync(() => {
78
+ const prev = process.env.SHELL
79
+ process.env.SHELL = "/bin/sh"
80
+ Shell.preferred.reset()
81
+ return prev
82
+ }),
83
+ () => fx(),
84
+ (prev) =>
85
+ Effect.sync(() => {
86
+ if (prev === undefined) delete process.env.SHELL
87
+ else process.env.SHELL = prev
88
+ Shell.preferred.reset()
89
+ }),
90
+ )
91
+ }
92
+
93
+ function toolPart(parts: SessionV1.Part[]) {
94
+ return parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
95
+ }
96
+
97
+ type CompletedToolPart = SessionV1.ToolPart & { state: SessionV1.ToolStateCompleted }
98
+ type ErrorToolPart = SessionV1.ToolPart & { state: SessionV1.ToolStateError }
99
+
100
+ function completedTool(parts: SessionV1.Part[]) {
101
+ const part = toolPart(parts)
102
+ expect(part?.state.status).toBe("completed")
103
+ return part?.state.status === "completed" ? (part as CompletedToolPart) : undefined
104
+ }
105
+
106
+ function errorTool(parts: SessionV1.Part[]) {
107
+ const part = toolPart(parts)
108
+ expect(part?.state.status).toBe("error")
109
+ return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
110
+ }
111
+
112
+ function makeMcp(instructions: MCP.ServerInstructions[] = []) {
113
+ return Layer.succeed(
114
+ MCP.Service,
115
+ MCP.Service.of({
116
+ status: () => Effect.succeed({}),
117
+ clients: () => Effect.succeed({}),
118
+ instructions: () => Effect.succeed(instructions),
119
+ tools: () => Effect.succeed({}),
120
+ prompts: () => Effect.succeed({}),
121
+ resources: () => Effect.succeed({}),
122
+ resourceTemplates: () => Effect.succeed({}),
123
+ add: () => Effect.succeed({ status: { status: "disabled" as const } }),
124
+ connect: () => Effect.void,
125
+ disconnect: () => Effect.void,
126
+ getPrompt: () => Effect.succeed(undefined),
127
+ readResource: () => Effect.succeed(undefined),
128
+ startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
129
+ authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
130
+ finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
131
+ removeAuth: () => Effect.void,
132
+ supportsOAuth: () => Effect.succeed(false),
133
+ hasStoredTokens: () => Effect.succeed(false),
134
+ getAuthStatus: () => Effect.succeed("not_authenticated" as const),
135
+ }),
136
+ )
137
+ }
138
+
139
+ const lsp = Layer.succeed(
140
+ LSP.Service,
141
+ LSP.Service.of({
142
+ init: () => Effect.void,
143
+ status: () => Effect.succeed([]),
144
+ hasClients: () => Effect.succeed(false),
145
+ touchFile: () => Effect.void,
146
+ diagnostics: () => Effect.succeed({}),
147
+ hover: () => Effect.succeed(undefined),
148
+ definition: () => Effect.succeed([]),
149
+ references: () => Effect.succeed([]),
150
+ implementation: () => Effect.succeed([]),
151
+ documentSymbol: () => Effect.succeed([]),
152
+ workspaceSymbol: () => Effect.succeed([]),
153
+ prepareCallHierarchy: () => Effect.succeed([]),
154
+ incomingCalls: () => Effect.succeed([]),
155
+ outgoingCalls: () => Effect.succeed([]),
156
+ }),
157
+ )
158
+
159
+ const processorCreateStarted: Array<() => void> = []
160
+ const blockingProcessor = Layer.succeed(
161
+ SessionProcessor.Service,
162
+ SessionProcessor.Service.of({
163
+ create: () => Effect.sync(() => processorCreateStarted.shift()?.()).pipe(Effect.andThen(Effect.never)),
164
+ }),
165
+ )
166
+
167
+ const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })
168
+
169
+ const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
170
+
171
+ const promptRoot = LayerNode.group([
172
+ SessionPrompt.node,
173
+ Session.node,
174
+ SessionProjector.node,
175
+ MessageV2.node,
176
+ Snapshot.node,
177
+ LLM.node,
178
+ Env.node,
179
+ AgentSvc.node,
180
+ Command.node,
181
+ Permission.node,
182
+ Plugin.node,
183
+ Config.node,
184
+ ProviderSvc.node,
185
+ LSP.node,
186
+ MCP.node,
187
+ FSUtil.node,
188
+ BackgroundJob.node,
189
+ SessionStatus.node,
190
+ SessionRunState.node,
191
+ Database.node,
192
+ EventV2Bridge.node,
193
+ Question.node,
194
+ Todo.node,
195
+ ToolRegistry.node,
196
+ Skill.node,
197
+ Git.node,
198
+ Ripgrep.node,
199
+ Format.node,
200
+ Truncate.node,
201
+ SessionProcessor.node,
202
+ Image.node,
203
+ SessionCompaction.node,
204
+ SessionRevert.node,
205
+ Instruction.node,
206
+ SystemPrompt.node,
207
+ CrossSpawnSpawner.node,
208
+ RuntimeFlags.node,
209
+ ])
210
+
211
+ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
212
+ const replacements = [
213
+ [SessionSummary.node, summary],
214
+ [LSP.node, lsp],
215
+ [MCP.node, makeMcp(input?.mcpInstructions)],
216
+ [RuntimeFlags.node, runtimeFlags],
217
+ ] as const
218
+ if (input?.processor === "blocking") {
219
+ return LayerNode.compile(promptRoot, [...replacements, [SessionProcessor.node, blockingProcessor]])
220
+ }
221
+ return LayerNode.compile(promptRoot, replacements)
222
+ }
223
+
224
+ function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
225
+ const root = LayerNode.group([promptRoot, testLLMServerNode])
226
+ const replacements = [
227
+ [SessionSummary.node, summary],
228
+ [LSP.node, lsp],
229
+ [MCP.node, makeMcp(input?.mcpInstructions)],
230
+ [RuntimeFlags.node, runtimeFlags],
231
+ ] as const
232
+ if (input?.processor === "blocking") {
233
+ return LayerNode.compile(root, [...replacements, [SessionProcessor.node, blockingProcessor]])
234
+ }
235
+ return LayerNode.compile(root, replacements)
236
+ }
237
+
238
+ function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
239
+ return makePrompt(input)
240
+ }
241
+
242
+ const it = testEffect(makeHttp())
243
+ const noLLMServer = testEffect(makeHttpNoLLMServer())
244
+ const raceNoLLMServer = testEffect(makeHttpNoLLMServer({ processor: "blocking" }))
245
+ const withMcpInstructions = testEffect(
246
+ makeHttp({
247
+ mcpInstructions: [
248
+ {
249
+ name: "guide-server",
250
+ instructions: "Use lookup before mutate.",
251
+ tools: ["guide-server_lookup"],
252
+ },
253
+ ],
254
+ }),
255
+ )
256
+ const unix = process.platform !== "win32" ? it.instance : it.instance.skip
257
+ const unixNoLLMServer = process.platform !== "win32" ? noLLMServer.instance : noLLMServer.instance.skip
258
+
259
+ // Config that registers a custom "test" provider with a "test-model" model
260
+ // so provider model lookup succeeds inside the loop.
261
+ const cfg = {
262
+ provider: {
263
+ test: {
264
+ name: "Test",
265
+ id: "test",
266
+ env: [],
267
+ npm: "@ai-sdk/openai-compatible",
268
+ models: {
269
+ "test-model": {
270
+ id: "test-model",
271
+ name: "Test Model",
272
+ attachment: false,
273
+ reasoning: false,
274
+ temperature: false,
275
+ tool_call: true,
276
+ release_date: "2025-01-01",
277
+ limit: { context: 100000, output: 10000 },
278
+ cost: { input: 0, output: 0 },
279
+ options: {},
280
+ },
281
+ },
282
+ options: {
283
+ apiKey: "test-key",
284
+ baseURL: "http://localhost:1/v1",
285
+ },
286
+ },
287
+ },
288
+ }
289
+
290
+ function providerCfg(url: string) {
291
+ return {
292
+ ...cfg,
293
+ provider: {
294
+ ...cfg.provider,
295
+ test: {
296
+ ...cfg.provider.test,
297
+ options: {
298
+ ...cfg.provider.test.options,
299
+ baseURL: url,
300
+ },
301
+ },
302
+ },
303
+ }
304
+ }
305
+
306
+ const writeText = Effect.fn("test.writeText")(function* (file: string, text: string) {
307
+ const fs = yield* FSUtil.Service
308
+ yield* fs.writeWithDirs(file, text)
309
+ })
310
+
311
+ const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial<ConfigV1.Info>) {
312
+ yield* writeText(
313
+ path.join(dir, "opencode.json"),
314
+ JSON.stringify({ $schema: "https://opencode.ai/config.json", ...config }),
315
+ )
316
+ })
317
+
318
+ const useServerConfig = Effect.fn("test.useServerConfig")(function* (config: (url: string) => Partial<ConfigV1.Info>) {
319
+ const { directory: dir } = yield* TestInstance
320
+ const llm = yield* TestLLMServer
321
+ yield* writeConfig(dir, config(llm.url))
322
+ return { dir, llm }
323
+ })
324
+
325
+ // Wait for a session's runner to enter a busy state. SessionStatus is flipped
326
+ // inside Runner.startShell's serialized transition, so cancel can't no-op once
327
+ // we observe it.
328
+ const waitForBusy = (sessionID: SessionID, duration: Duration.Input = "2 seconds") =>
329
+ pollWithTimeout(
330
+ Effect.gen(function* () {
331
+ const status = yield* SessionStatus.Service
332
+ const s = yield* status.get(sessionID)
333
+ return s.type === "busy" ? (true as const) : undefined
334
+ }),
335
+ `session ${sessionID} never became busy`,
336
+ duration,
337
+ )
338
+
339
+ const hasBash = Effect.sync(() => Bun.which("bash") !== null)
340
+
341
+ const deferredAsPromise = <A>(deferred: Deferred.Deferred<A>): PromiseLike<A> => ({
342
+ then: (onfulfilled, onrejected) => {
343
+ Effect.runFork(
344
+ Deferred.await(deferred).pipe(
345
+ Effect.match({
346
+ onFailure: (error) => {
347
+ onrejected?.(error)
348
+ },
349
+ onSuccess: (value) => {
350
+ onfulfilled?.(value)
351
+ },
352
+ }),
353
+ ),
354
+ )
355
+ return deferredAsPromise(deferred) as PromiseLike<never>
356
+ },
357
+ })
358
+
359
+ function defer<T>() {
360
+ let resolve!: (value: T | PromiseLike<T>) => void
361
+ const promise = new Promise<T>((done) => {
362
+ resolve = done
363
+ })
364
+ return { promise, resolve }
365
+ }
366
+
367
+ const succeedVoid = (deferred: Deferred.Deferred<void>) => {
368
+ Effect.runSync(Deferred.succeed(deferred, void 0).pipe(Effect.ignore))
369
+ }
370
+
371
+ const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string) {
372
+ const session = yield* Session.Service
373
+ const msg = yield* session.updateMessage({
374
+ id: MessageID.ascending(),
375
+ role: "user",
376
+ sessionID,
377
+ agent: "build",
378
+ model: ref,
379
+ time: { created: Date.now() },
380
+ })
381
+ yield* session.updatePart({
382
+ id: PartID.ascending(),
383
+ messageID: msg.id,
384
+ sessionID,
385
+ type: "text",
386
+ text,
387
+ })
388
+ return msg
389
+ })
390
+
391
+ const seed = Effect.fn("test.seed")(function* (sessionID: SessionID, opts?: { finish?: string }) {
392
+ const session = yield* Session.Service
393
+ const msg = yield* user(sessionID, "hello")
394
+ const assistant: SessionV1.Assistant = {
395
+ id: MessageID.ascending(),
396
+ role: "assistant",
397
+ parentID: msg.id,
398
+ sessionID,
399
+ mode: "build",
400
+ agent: "build",
401
+ cost: 0,
402
+ path: { cwd: "/tmp", root: "/tmp" },
403
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
404
+ modelID: ref.modelID,
405
+ providerID: ref.providerID,
406
+ time: { created: Date.now() },
407
+ ...(opts?.finish ? { finish: opts.finish } : {}),
408
+ }
409
+ yield* session.updateMessage(assistant)
410
+ yield* session.updatePart({
411
+ id: PartID.ascending(),
412
+ messageID: assistant.id,
413
+ sessionID,
414
+ type: "text",
415
+ text: "hi there",
416
+ })
417
+ return { user: msg, assistant }
418
+ })
419
+
420
+ const addSubtask = (sessionID: SessionID, messageID: MessageID, model = ref) =>
421
+ Effect.gen(function* () {
422
+ const session = yield* Session.Service
423
+ yield* session.updatePart({
424
+ id: PartID.ascending(),
425
+ messageID,
426
+ sessionID,
427
+ type: "subtask",
428
+ prompt: "look into the cache key path",
429
+ description: "inspect bug",
430
+ agent: "general",
431
+ model,
432
+ })
433
+ })
434
+
435
+ const boot = Effect.fn("test.boot")(function* (input?: { title?: string }) {
436
+ const config = yield* Config.Service
437
+ const prompt = yield* SessionPrompt.Service
438
+ const run = yield* SessionRunState.Service
439
+ const sessions = yield* Session.Service
440
+ yield* config.get()
441
+ const chat = yield* sessions.create(input ?? { title: "Pinned" })
442
+ return { prompt, run, sessions, chat }
443
+ })
444
+
445
+ // Loop semantics
446
+
447
+ noLLMServer.instance(
448
+ "loop exits immediately when last assistant has stop finish",
449
+ () =>
450
+ Effect.gen(function* () {
451
+ const prompt = yield* SessionPrompt.Service
452
+ const sessions = yield* Session.Service
453
+ const chat = yield* sessions.create({ title: "Pinned" })
454
+ yield* seed(chat.id, { finish: "stop" })
455
+
456
+ const result = yield* prompt.loop({ sessionID: chat.id })
457
+ expect(result.info.role).toBe("assistant")
458
+ if (result.info.role === "assistant") expect(result.info.finish).toBe("stop")
459
+ }),
460
+ { config: cfg },
461
+ )
462
+
463
+ noLLMServer.instance(
464
+ "loop exits for a completed parent turn with nonmonotonic message IDs",
465
+ () =>
466
+ Effect.gen(function* () {
467
+ const prompt = yield* SessionPrompt.Service
468
+ const sessions = yield* Session.Service
469
+ const chat = yield* sessions.create({ title: "Pinned" })
470
+ const userID = MessageID.make("msg_z_user")
471
+ const assistantID = MessageID.make("msg_a_assistant")
472
+ yield* sessions.updateMessage({
473
+ id: userID,
474
+ role: "user",
475
+ sessionID: chat.id,
476
+ agent: "build",
477
+ model: ref,
478
+ time: { created: 100 },
479
+ })
480
+ yield* sessions.updateMessage({
481
+ id: assistantID,
482
+ role: "assistant",
483
+ parentID: userID,
484
+ sessionID: chat.id,
485
+ mode: "build",
486
+ agent: "build",
487
+ cost: 0,
488
+ path: { cwd: "/tmp", root: "/tmp" },
489
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
490
+ modelID: ref.modelID,
491
+ providerID: ref.providerID,
492
+ time: { created: 200, completed: 201 },
493
+ finish: "stop",
494
+ })
495
+
496
+ const result = yield* prompt.loop({ sessionID: chat.id })
497
+
498
+ expect(result.info.id).toBe(assistantID)
499
+ }),
500
+ { config: cfg },
501
+ )
502
+
503
+ it.instance("loop exits without an LLM request for interrupted orphan tool calls", () =>
504
+ Effect.gen(function* () {
505
+ const { llm } = yield* useServerConfig(providerCfg)
506
+ const prompt = yield* SessionPrompt.Service
507
+ const sessions = yield* Session.Service
508
+ const chat = yield* sessions.create({ title: "Pinned" })
509
+ const seeded = yield* seed(chat.id, { finish: "stop" })
510
+ yield* sessions.updatePart({
511
+ id: PartID.ascending(),
512
+ messageID: seeded.assistant.id,
513
+ sessionID: chat.id,
514
+ type: "tool",
515
+ callID: "interrupted-call",
516
+ tool: "edit",
517
+ state: {
518
+ status: "error",
519
+ input: {},
520
+ error: "Tool execution aborted",
521
+ metadata: { interrupted: true },
522
+ time: { start: 1, end: 2 },
523
+ },
524
+ })
525
+
526
+ const result = yield* prompt.loop({ sessionID: chat.id })
527
+ expect(result.info.id).toBe(seeded.assistant.id)
528
+ expect(yield* llm.hits).toHaveLength(0)
529
+ }),
530
+ )
531
+
532
+ it.instance("loop calls LLM and returns assistant message", () =>
533
+ Effect.gen(function* () {
534
+ const { llm } = yield* useServerConfig(providerCfg)
535
+ const prompt = yield* SessionPrompt.Service
536
+ const sessions = yield* Session.Service
537
+ const chat = yield* sessions.create({
538
+ title: "Pinned",
539
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
540
+ })
541
+ yield* prompt.prompt({
542
+ sessionID: chat.id,
543
+ agent: "build",
544
+ noReply: true,
545
+ parts: [{ type: "text", text: "hello" }],
546
+ })
547
+ yield* llm.text("world")
548
+
549
+ const result = yield* prompt.loop({ sessionID: chat.id })
550
+ expect(result.info.role).toBe("assistant")
551
+ const parts = result.parts.filter((p) => p.type === "text")
552
+ expect(parts.some((p) => p.type === "text" && p.text === "world")).toBe(true)
553
+ expect(yield* llm.hits).toHaveLength(1)
554
+ }),
555
+ )
556
+
557
+ withMcpInstructions.instance(
558
+ "loop includes MCP instructions in model system context",
559
+ () =>
560
+ Effect.gen(function* () {
561
+ const { llm } = yield* useServerConfig(providerCfg)
562
+ const prompt = yield* SessionPrompt.Service
563
+ const sessions = yield* Session.Service
564
+ const chat = yield* sessions.create({
565
+ title: "Pinned",
566
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
567
+ })
568
+ yield* llm.hang
569
+ yield* user(chat.id, "hello")
570
+
571
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
572
+ yield* awaitWithTimeout(llm.wait(1), "timed out waiting for MCP instruction request", "10 seconds")
573
+
574
+ const hits = yield* llm.hits
575
+ const body = JSON.stringify(hits[0]?.body)
576
+ expect(body).toContain('<server name=\\"guide-server\\">')
577
+ expect(body).toContain("Use lookup before mutate.")
578
+ yield* Fiber.interrupt(fiber)
579
+ }),
580
+ 15_000,
581
+ )
582
+
583
+ it.instance("legacy prompt emits message events without session.next events", () =>
584
+ Effect.gen(function* () {
585
+ const events = yield* EventV2Bridge.Service
586
+ const prompt = yield* SessionPrompt.Service
587
+ const sessions = yield* Session.Service
588
+ const chat = yield* sessions.create({
589
+ title: "Pinned",
590
+ agent: "plan",
591
+ model: { providerID: ProviderV2.ID.make("old"), id: ModelV2.ID.make("old-model") },
592
+ })
593
+ const seen: string[] = []
594
+ const off = yield* events.listen((event) => {
595
+ seen.push(event.type)
596
+ return Effect.void
597
+ })
598
+
599
+ const first = yield* prompt.prompt({
600
+ sessionID: chat.id,
601
+ agent: "build",
602
+ model: ref,
603
+ noReply: true,
604
+ parts: [{ type: "text", text: "hello" }],
605
+ })
606
+ const second = yield* prompt.prompt({
607
+ sessionID: chat.id,
608
+ agent: "build",
609
+ noReply: true,
610
+ parts: [{ type: "text", text: "again" }],
611
+ })
612
+ yield* off
613
+
614
+ expect(first.info.role).toBe("user")
615
+ expect(second.info.role).toBe("user")
616
+ if (first.info.role === "user" && second.info.role === "user") {
617
+ expect(first.info.model).toEqual(ref)
618
+ expect(second.info.model).toEqual(ref)
619
+ }
620
+ expect(yield* sessions.get(chat.id)).toMatchObject({
621
+ agent: "build",
622
+ model: { providerID: ref.providerID, id: ref.modelID },
623
+ })
624
+ expect(seen).toContain(Session.Event.Updated.type)
625
+ expect(seen).toContain(MessageV2.Event.Updated.type)
626
+ expect(seen).toContain(MessageV2.Event.PartUpdated.type)
627
+ expect(seen.filter((type) => type.startsWith("session.next."))).toEqual([])
628
+ }),
629
+ )
630
+
631
+ it.instance("loop surfaces content-filter finishes as session errors", () =>
632
+ Effect.gen(function* () {
633
+ const { llm } = yield* useServerConfig(providerCfg)
634
+ const events = yield* EventV2Bridge.Service
635
+ const prompt = yield* SessionPrompt.Service
636
+ const sessions = yield* Session.Service
637
+ const chat = yield* sessions.create({ title: "Pinned" })
638
+ const errors: NonNullable<SessionV1.Assistant["error"]>[] = []
639
+ const expected = {
640
+ name: "ContentFilterError",
641
+ data: { message: "The response was blocked by the provider's content filter" },
642
+ } satisfies NonNullable<SessionV1.Assistant["error"]>
643
+ const off = yield* events.listen((event) => {
644
+ if (event.type !== Session.Event.Error.type) return Effect.void
645
+ const data = event.data as typeof Session.Event.Error.data.Type
646
+ if (data.sessionID === chat.id && data.error) errors.push(data.error)
647
+ return Effect.void
648
+ })
649
+
650
+ yield* prompt.prompt({
651
+ sessionID: chat.id,
652
+ agent: "build",
653
+ noReply: true,
654
+ parts: [{ type: "text", text: "hello" }],
655
+ })
656
+ yield* llm.push(reply().text("partial response").contentFilter())
657
+
658
+ const result = yield* prompt.loop({ sessionID: chat.id })
659
+ const stored = yield* MessageV2.get({ sessionID: chat.id, messageID: result.info.id })
660
+ yield* off
661
+
662
+ expect(yield* llm.hits).toHaveLength(1)
663
+ expect(result.info.role).toBe("assistant")
664
+ expect(stored.info.role).toBe("assistant")
665
+ if (result.info.role === "assistant" && stored.info.role === "assistant") {
666
+ expect(result.info.finish).toBe("content-filter")
667
+ expect(result.info.error).toEqual(expected)
668
+ expect(stored.info.error).toEqual(result.info.error)
669
+ expect(errors).toContainEqual(expected)
670
+ }
671
+ expect(result.parts).toEqual(
672
+ expect.arrayContaining([expect.objectContaining({ type: "text", text: "partial response" })]),
673
+ )
674
+ }),
675
+ )
676
+
677
+ it.instance("loop stops provider overflow instead of auto-compacting when disabled", () =>
678
+ Effect.gen(function* () {
679
+ const { llm } = yield* useServerConfig((url) => ({
680
+ ...providerCfg(url),
681
+ compaction: { auto: false },
682
+ }))
683
+ const prompt = yield* SessionPrompt.Service
684
+ const sessions = yield* Session.Service
685
+ const chat = yield* sessions.create({ title: "Pinned" })
686
+
687
+ yield* llm.error(413, { error: { message: "request entity too large" } })
688
+ yield* prompt.prompt({
689
+ sessionID: chat.id,
690
+ agent: "build",
691
+ noReply: true,
692
+ parts: [{ type: "text", text: "hello" }],
693
+ })
694
+
695
+ const result = yield* prompt.loop({ sessionID: chat.id })
696
+ const messages = yield* sessions.messages({ sessionID: chat.id })
697
+
698
+ expect(result.info.role).toBe("assistant")
699
+ if (result.info.role === "assistant") {
700
+ expect(result.info.error?.name).toBe("ContextOverflowError")
701
+ expect(result.info.finish).toBe("error")
702
+ }
703
+ expect(messages.some((message) => message.parts.some((part) => part.type === "compaction"))).toBe(false)
704
+ }),
705
+ )
706
+
707
+ noLLMServer.instance.skip(
708
+ "prompt emits v2 prompted and synthetic events (v2 projector disabled)",
709
+ () =>
710
+ Effect.gen(function* () {
711
+ const prompt = yield* SessionPrompt.Service
712
+ const sessions = yield* Session.Service
713
+ const chat = yield* sessions.create({ title: "Pinned" })
714
+
715
+ yield* prompt.prompt({
716
+ sessionID: chat.id,
717
+ agent: "build",
718
+ noReply: true,
719
+ parts: [
720
+ { type: "text", text: "hello v2" },
721
+ {
722
+ type: "file",
723
+ mime: "text/plain",
724
+ filename: "note.txt",
725
+ url: "data:text/plain;base64,bm90ZSBjb250ZW50",
726
+ },
727
+ ],
728
+ })
729
+
730
+ const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
731
+ Effect.provide(
732
+ LayerNode.compile(SessionV2.node, [
733
+ [SessionExecution.node, SessionExecution.noopLayer],
734
+ [LocationServiceMap.node, locationServiceMapLayer],
735
+ ]),
736
+ ),
737
+ )
738
+ const { db } = yield* Database.Service
739
+ const row = yield* db
740
+ .select()
741
+ .from(SessionMessageTable)
742
+ .where(eq(SessionMessageTable.session_id, chat.id))
743
+ .get()
744
+ .pipe(Effect.orDie)
745
+ expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
746
+ expect(typeof row?.data.time.created).toBe("number")
747
+ expect(messages).toEqual(
748
+ expect.arrayContaining([
749
+ expect.objectContaining({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }),
750
+ expect.objectContaining({ type: "synthetic", text: "note content" }),
751
+ ]),
752
+ )
753
+ }),
754
+ { config: cfg },
755
+ )
756
+
757
+ it.instance("static loop returns assistant text through local provider", () =>
758
+ Effect.gen(function* () {
759
+ const { llm } = yield* useServerConfig(providerCfg)
760
+ const prompt = yield* SessionPrompt.Service
761
+ const sessions = yield* Session.Service
762
+ const session = yield* sessions.create({
763
+ title: "Prompt provider",
764
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
765
+ })
766
+
767
+ yield* prompt.prompt({
768
+ sessionID: session.id,
769
+ agent: "build",
770
+ noReply: true,
771
+ parts: [{ type: "text", text: "hello" }],
772
+ })
773
+
774
+ yield* llm.text("world")
775
+
776
+ const result = yield* prompt.loop({ sessionID: session.id })
777
+ expect(result.info.role).toBe("assistant")
778
+ expect(result.parts.some((part) => part.type === "text" && part.text === "world")).toBe(true)
779
+ expect(yield* llm.hits).toHaveLength(1)
780
+ expect(yield* llm.pending).toBe(0)
781
+ }),
782
+ )
783
+
784
+ it.instance("static loop consumes queued replies across turns", () =>
785
+ Effect.gen(function* () {
786
+ const { llm } = yield* useServerConfig(providerCfg)
787
+ const prompt = yield* SessionPrompt.Service
788
+ const sessions = yield* Session.Service
789
+ const session = yield* sessions.create({
790
+ title: "Prompt provider turns",
791
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
792
+ })
793
+
794
+ yield* prompt.prompt({
795
+ sessionID: session.id,
796
+ agent: "build",
797
+ noReply: true,
798
+ parts: [{ type: "text", text: "hello one" }],
799
+ })
800
+
801
+ yield* llm.text("world one")
802
+
803
+ const first = yield* prompt.loop({ sessionID: session.id })
804
+ expect(first.info.role).toBe("assistant")
805
+ expect(first.parts.some((part) => part.type === "text" && part.text === "world one")).toBe(true)
806
+
807
+ yield* prompt.prompt({
808
+ sessionID: session.id,
809
+ agent: "build",
810
+ noReply: true,
811
+ parts: [{ type: "text", text: "hello two" }],
812
+ })
813
+
814
+ yield* llm.text("world two")
815
+
816
+ const second = yield* prompt.loop({ sessionID: session.id })
817
+ expect(second.info.role).toBe("assistant")
818
+ expect(second.parts.some((part) => part.type === "text" && part.text === "world two")).toBe(true)
819
+
820
+ expect(yield* llm.hits).toHaveLength(2)
821
+ expect(yield* llm.pending).toBe(0)
822
+ }),
823
+ )
824
+
825
+ it.instance("loop continues when finish is tool-calls", () =>
826
+ Effect.gen(function* () {
827
+ const { llm } = yield* useServerConfig(providerCfg)
828
+ const prompt = yield* SessionPrompt.Service
829
+ const sessions = yield* Session.Service
830
+ const session = yield* sessions.create({
831
+ title: "Pinned",
832
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
833
+ })
834
+ yield* prompt.prompt({
835
+ sessionID: session.id,
836
+ agent: "build",
837
+ noReply: true,
838
+ parts: [{ type: "text", text: "hello" }],
839
+ })
840
+ yield* llm.tool("first", { value: "first" })
841
+ yield* llm.text("second")
842
+
843
+ const result = yield* prompt.loop({ sessionID: session.id })
844
+ expect(yield* llm.calls).toBe(2)
845
+ expect(result.info.role).toBe("assistant")
846
+ if (result.info.role === "assistant") {
847
+ expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
848
+ expect(result.info.finish).toBe("stop")
849
+ }
850
+ }),
851
+ )
852
+
853
+ it.instance("loop continues when finish is unknown", () =>
854
+ Effect.gen(function* () {
855
+ const { llm } = yield* useServerConfig(providerCfg)
856
+ const prompt = yield* SessionPrompt.Service
857
+ const sessions = yield* Session.Service
858
+ const session = yield* sessions.create({
859
+ title: "Pinned",
860
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
861
+ })
862
+ yield* prompt.prompt({
863
+ sessionID: session.id,
864
+ agent: "build",
865
+ noReply: true,
866
+ parts: [{ type: "text", text: "hello" }],
867
+ })
868
+ yield* llm.push(reply())
869
+ yield* llm.text("second")
870
+
871
+ const result = yield* prompt.loop({ sessionID: session.id })
872
+ expect(yield* llm.calls).toBe(2)
873
+ expect(result.info.role).toBe("assistant")
874
+ if (result.info.role === "assistant") {
875
+ expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
876
+ expect(result.info.finish).toBe("stop")
877
+ }
878
+ }),
879
+ )
880
+
881
+ it.instance("glob tool keeps instance context during prompt runs", () =>
882
+ Effect.gen(function* () {
883
+ const { dir, llm } = yield* useServerConfig(providerCfg)
884
+ const prompt = yield* SessionPrompt.Service
885
+ const sessions = yield* Session.Service
886
+ const session = yield* sessions.create({
887
+ title: "Glob context",
888
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
889
+ })
890
+ const file = path.join(dir, "probe.txt")
891
+ yield* writeText(file, "probe")
892
+
893
+ yield* prompt.prompt({
894
+ sessionID: session.id,
895
+ agent: "build",
896
+ noReply: true,
897
+ parts: [{ type: "text", text: "find text files" }],
898
+ })
899
+ yield* llm.tool("glob", { pattern: "**/*.txt" })
900
+ yield* llm.text("done")
901
+
902
+ const result = yield* prompt.loop({ sessionID: session.id })
903
+ expect(result.info.role).toBe("assistant")
904
+
905
+ const msgs = yield* MessageV2.filterCompactedEffect(session.id)
906
+ const tool = msgs
907
+ .flatMap((msg) => msg.parts)
908
+ .find(
909
+ (part): part is CompletedToolPart =>
910
+ part.type === "tool" && part.tool === "glob" && part.state.status === "completed",
911
+ )
912
+ if (!tool) return
913
+
914
+ expect(tool.state.output).toContain(file)
915
+ expect(tool.state.output).not.toContain("No context found for instance")
916
+ expect(result.parts.some((part) => part.type === "text" && part.text === "done")).toBe(true)
917
+ }),
918
+ )
919
+
920
+ it.instance("loop continues when finish is stop but assistant has tool parts", () =>
921
+ Effect.gen(function* () {
922
+ const { llm } = yield* useServerConfig(providerCfg)
923
+ const prompt = yield* SessionPrompt.Service
924
+ const sessions = yield* Session.Service
925
+ const session = yield* sessions.create({
926
+ title: "Pinned",
927
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
928
+ })
929
+ yield* prompt.prompt({
930
+ sessionID: session.id,
931
+ agent: "build",
932
+ noReply: true,
933
+ parts: [{ type: "text", text: "hello" }],
934
+ })
935
+ yield* llm.push(reply().tool("first", { value: "first" }).stop())
936
+ yield* llm.text("second")
937
+
938
+ const result = yield* prompt.loop({ sessionID: session.id })
939
+ expect(yield* llm.calls).toBe(2)
940
+ expect(result.info.role).toBe("assistant")
941
+ if (result.info.role === "assistant") {
942
+ expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
943
+ expect(result.info.finish).toBe("stop")
944
+ }
945
+ }),
946
+ )
947
+
948
+ it.instance("failed subtask preserves metadata on error tool state", () =>
949
+ Effect.gen(function* () {
950
+ const { llm } = yield* useServerConfig((url) => ({
951
+ ...providerCfg(url),
952
+ agent: {
953
+ general: {
954
+ model: "test/missing-model",
955
+ },
956
+ },
957
+ }))
958
+ const prompt = yield* SessionPrompt.Service
959
+ const sessions = yield* Session.Service
960
+ const chat = yield* sessions.create({ title: "Pinned" })
961
+ yield* llm.tool("task", {
962
+ description: "inspect bug",
963
+ prompt: "look into the cache key path",
964
+ subagent_type: "general",
965
+ })
966
+ yield* llm.text("done")
967
+ const msg = yield* user(chat.id, "hello")
968
+ yield* addSubtask(chat.id, msg.id)
969
+
970
+ const result = yield* prompt.loop({ sessionID: chat.id })
971
+ expect(result.info.role).toBe("assistant")
972
+ expect(yield* llm.calls).toBe(2)
973
+
974
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
975
+ const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
976
+ expect(taskMsg?.info.role).toBe("assistant")
977
+ if (!taskMsg || taskMsg.info.role !== "assistant") return
978
+
979
+ const tool = errorTool(taskMsg.parts)
980
+ if (!tool) return
981
+
982
+ expect(tool.state.error).toContain("Tool execution failed")
983
+ expect(tool.state.metadata).toBeDefined()
984
+ expect(tool.state.metadata?.sessionId).toBeDefined()
985
+ expect(tool.state.metadata?.model).toEqual({
986
+ providerID: ProviderV2.ID.make("test"),
987
+ modelID: ModelV2.ID.make("missing-model"),
988
+ })
989
+ }),
990
+ )
991
+
992
+ it.instance("subtask child inherits parent session external_directory allow", () =>
993
+ Effect.gen(function* () {
994
+ const { llm } = yield* useServerConfig(providerCfg)
995
+ const prompt = yield* SessionPrompt.Service
996
+ const sessions = yield* Session.Service
997
+ const chat = yield* sessions.create({
998
+ title: "Parent",
999
+ permission: [{ permission: "external_directory", pattern: "/tmp/allowed/*", action: "allow" }],
1000
+ })
1001
+ yield* llm.text("done")
1002
+ const msg = yield* user(chat.id, "hello")
1003
+ yield* addSubtask(chat.id, msg.id)
1004
+
1005
+ yield* prompt.loop({ sessionID: chat.id })
1006
+
1007
+ const kids = yield* sessions.children(chat.id)
1008
+ expect(kids).toHaveLength(1)
1009
+ const child = kids[0]!
1010
+ const rules = child.permission ?? []
1011
+ expect(rules).toEqual(
1012
+ expect.arrayContaining([{ permission: "external_directory", pattern: "/tmp/allowed/*", action: "allow" }]),
1013
+ )
1014
+ expect(Permission.evaluate("external_directory", "/tmp/allowed/file", rules).action).toBe("allow")
1015
+ expect(Permission.evaluate("task", "anything", rules).action).toBe("deny")
1016
+ }),
1017
+ )
1018
+
1019
+ noLLMServer.instance("prompt tools replace previous prompt tool rules", () =>
1020
+ Effect.gen(function* () {
1021
+ const prompt = yield* SessionPrompt.Service
1022
+ const sessions = yield* Session.Service
1023
+ const session = yield* sessions.create({ title: "Prompt tools" })
1024
+
1025
+ yield* prompt.prompt({
1026
+ sessionID: session.id,
1027
+ agent: "build",
1028
+ noReply: true,
1029
+ tools: { bash: false },
1030
+ parts: [{ type: "text", text: "first" }],
1031
+ })
1032
+ yield* prompt.prompt({
1033
+ sessionID: session.id,
1034
+ agent: "build",
1035
+ noReply: true,
1036
+ tools: { read: true },
1037
+ parts: [{ type: "text", text: "second" }],
1038
+ })
1039
+
1040
+ const reloaded = yield* sessions.get(session.id)
1041
+ expect(reloaded.permission).toEqual([{ permission: "read", pattern: "*", action: "allow" }])
1042
+ expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("ask")
1043
+ }),
1044
+ )
1045
+
1046
+ it.instance(
1047
+ "running subtask preserves metadata after tool-call transition",
1048
+ () =>
1049
+ Effect.gen(function* () {
1050
+ const { llm } = yield* useServerConfig(providerCfg)
1051
+ const prompt = yield* SessionPrompt.Service
1052
+ const sessions = yield* Session.Service
1053
+ const chat = yield* sessions.create({ title: "Pinned" })
1054
+ yield* llm.hang
1055
+ const msg = yield* user(chat.id, "hello")
1056
+ yield* addSubtask(chat.id, msg.id)
1057
+
1058
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1059
+
1060
+ const tool = yield* pollWithTimeout(
1061
+ Effect.gen(function* () {
1062
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1063
+ const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
1064
+ const tool = taskMsg?.parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
1065
+ if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
1066
+ }),
1067
+ "timed out waiting for running subtask metadata",
1068
+ )
1069
+
1070
+ if (tool.state.status !== "running") return
1071
+ expect(typeof tool.state.metadata?.sessionId).toBe("string")
1072
+ expect(tool.state.title).toBeDefined()
1073
+ expect(tool.state.metadata?.model).toBeDefined()
1074
+
1075
+ yield* prompt.cancel(chat.id)
1076
+ yield* Fiber.await(fiber)
1077
+ }),
1078
+ 5_000,
1079
+ )
1080
+
1081
+ it.instance(
1082
+ "running task tool preserves metadata after tool-call transition",
1083
+ () =>
1084
+ Effect.gen(function* () {
1085
+ const { llm } = yield* useServerConfig(providerCfg)
1086
+ const prompt = yield* SessionPrompt.Service
1087
+ const sessions = yield* Session.Service
1088
+ const chat = yield* sessions.create({
1089
+ title: "Pinned",
1090
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1091
+ })
1092
+ yield* llm.tool("task", {
1093
+ description: "inspect bug",
1094
+ prompt: "look into the cache key path",
1095
+ subagent_type: "general",
1096
+ })
1097
+ yield* llm.hang
1098
+ yield* user(chat.id, "hello")
1099
+
1100
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1101
+
1102
+ const tool = yield* pollWithTimeout(
1103
+ Effect.gen(function* () {
1104
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1105
+ const assistant = msgs.findLast((item) => item.info.role === "assistant" && item.info.agent === "build")
1106
+ const tool = assistant?.parts.find(
1107
+ (part): part is SessionV1.ToolPart => part.type === "tool" && part.tool === "task",
1108
+ )
1109
+ if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
1110
+ }),
1111
+ "timed out waiting for running task metadata",
1112
+ )
1113
+
1114
+ if (tool.state.status !== "running") return
1115
+ expect(typeof tool.state.metadata?.sessionId).toBe("string")
1116
+ expect(tool.state.title).toBe("inspect bug")
1117
+ expect(tool.state.metadata?.model).toBeDefined()
1118
+
1119
+ yield* prompt.cancel(chat.id)
1120
+ yield* Fiber.await(fiber)
1121
+ }),
1122
+ 10_000,
1123
+ )
1124
+
1125
+ it.instance(
1126
+ "loop sets status to busy then idle",
1127
+ () =>
1128
+ Effect.gen(function* () {
1129
+ const { llm } = yield* useServerConfig(providerCfg)
1130
+ const prompt = yield* SessionPrompt.Service
1131
+ const sessions = yield* Session.Service
1132
+ const status = yield* SessionStatus.Service
1133
+
1134
+ yield* llm.hang
1135
+
1136
+ const chat = yield* sessions.create({})
1137
+ yield* user(chat.id, "hi")
1138
+
1139
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1140
+ yield* llm.wait(1)
1141
+ expect((yield* status.get(chat.id)).type).toBe("busy")
1142
+ yield* prompt.cancel(chat.id)
1143
+ yield* Fiber.await(fiber)
1144
+ expect((yield* status.get(chat.id)).type).toBe("idle")
1145
+ }),
1146
+ 3_000,
1147
+ )
1148
+
1149
+ // Cancel semantics
1150
+
1151
+ it.instance("cancel interrupts loop and resolves with an assistant message", () =>
1152
+ Effect.gen(function* () {
1153
+ const { llm } = yield* useServerConfig(providerCfg)
1154
+ const prompt = yield* SessionPrompt.Service
1155
+ const sessions = yield* Session.Service
1156
+ const chat = yield* sessions.create({ title: "Pinned" })
1157
+ yield* seed(chat.id)
1158
+
1159
+ yield* llm.hang
1160
+
1161
+ yield* user(chat.id, "more")
1162
+
1163
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1164
+ yield* llm.wait(1)
1165
+ yield* waitForBusy(chat.id)
1166
+ yield* prompt.cancel(chat.id)
1167
+ const exit = yield* Fiber.await(fiber)
1168
+ expect(Exit.isSuccess(exit)).toBe(true)
1169
+ if (Exit.isSuccess(exit)) {
1170
+ expect(exit.value.info.role).toBe("assistant")
1171
+ }
1172
+ }),
1173
+ )
1174
+
1175
+ it.instance("cancel records MessageAbortedError on interrupted process", () =>
1176
+ Effect.gen(function* () {
1177
+ const { llm } = yield* useServerConfig(providerCfg)
1178
+ const prompt = yield* SessionPrompt.Service
1179
+ const sessions = yield* Session.Service
1180
+ const chat = yield* sessions.create({ title: "Pinned" })
1181
+ yield* llm.hang
1182
+ yield* user(chat.id, "hello")
1183
+
1184
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1185
+ yield* llm.wait(1)
1186
+ yield* waitForBusy(chat.id)
1187
+ yield* prompt.cancel(chat.id)
1188
+ const exit = yield* Fiber.await(fiber)
1189
+ expect(Exit.isSuccess(exit)).toBe(true)
1190
+ if (Exit.isSuccess(exit)) {
1191
+ const info = exit.value.info
1192
+ if (info.role === "assistant") {
1193
+ expect(info.error?.name).toBe("MessageAbortedError")
1194
+ }
1195
+ }
1196
+ }),
1197
+ )
1198
+
1199
+ raceNoLLMServer.instance(
1200
+ "finalizes assistant when cancelled before processor creation completes",
1201
+ () =>
1202
+ Effect.gen(function* () {
1203
+ processorCreateStarted.length = 0
1204
+ yield* Effect.addFinalizer(() =>
1205
+ Effect.sync(() => {
1206
+ processorCreateStarted.length = 0
1207
+ }),
1208
+ )
1209
+
1210
+ const prompt = yield* SessionPrompt.Service
1211
+ const sessions = yield* Session.Service
1212
+ const chat = yield* sessions.create({ title: "Processor creation race" })
1213
+
1214
+ yield* prompt.prompt({
1215
+ sessionID: chat.id,
1216
+ agent: "build",
1217
+ noReply: true,
1218
+ parts: [{ type: "text", text: "first" }],
1219
+ })
1220
+
1221
+ const firstCreate = defer<void>()
1222
+ processorCreateStarted.push(firstCreate.resolve)
1223
+ const first = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1224
+ yield* Effect.promise(() => firstCreate.promise)
1225
+
1226
+ yield* prompt.cancel(chat.id)
1227
+ const firstExit = yield* Fiber.await(first)
1228
+ expect(Exit.isSuccess(firstExit)).toBe(true)
1229
+
1230
+ let messages = yield* sessions.messages({ sessionID: chat.id })
1231
+ const firstInterrupted = messages.at(-1)
1232
+ expect(firstInterrupted?.info.role).toBe("assistant")
1233
+ expect(firstInterrupted?.parts).toHaveLength(0)
1234
+ if (firstInterrupted?.info.role === "assistant") {
1235
+ expect(firstInterrupted.info.finish).toBeUndefined()
1236
+ expect(firstInterrupted.info.time.completed).toBeNumber()
1237
+ expect(firstInterrupted.info.error?.name).toBe("MessageAbortedError")
1238
+ }
1239
+
1240
+ yield* prompt.prompt({
1241
+ sessionID: chat.id,
1242
+ agent: "build",
1243
+ noReply: true,
1244
+ parts: [{ type: "text", text: "second" }],
1245
+ })
1246
+
1247
+ const secondCreate = defer<void>()
1248
+ processorCreateStarted.push(secondCreate.resolve)
1249
+ const second = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1250
+ yield* Effect.promise(() => secondCreate.promise)
1251
+
1252
+ yield* prompt.cancel(chat.id)
1253
+ const secondExit = yield* Fiber.await(second)
1254
+ expect(Exit.isSuccess(secondExit)).toBe(true)
1255
+
1256
+ messages = yield* sessions.messages({ sessionID: chat.id })
1257
+ const poisonMessages = messages.filter(
1258
+ (message) =>
1259
+ message.info.role === "assistant" &&
1260
+ message.parts.length === 0 &&
1261
+ !message.info.finish &&
1262
+ !message.info.time.completed &&
1263
+ !message.info.error,
1264
+ )
1265
+ expect(poisonMessages).toHaveLength(0)
1266
+
1267
+ const interruptedMessages = messages.filter(
1268
+ (message) =>
1269
+ message.info.role === "assistant" &&
1270
+ message.parts.length === 0 &&
1271
+ message.info.time.completed &&
1272
+ message.info.error?.name === "MessageAbortedError",
1273
+ )
1274
+ expect(interruptedMessages).toHaveLength(2)
1275
+
1276
+ const lastUser = messages.at(-2)
1277
+ const lastAssistant = messages.at(-1)
1278
+ expect(lastUser?.info.role).toBe("user")
1279
+ expect(lastAssistant?.info.role).toBe("assistant")
1280
+ if (lastUser?.info.role === "user" && lastAssistant?.info.role === "assistant") {
1281
+ expect(lastAssistant.info.parentID).toBe(lastUser?.info.id)
1282
+ }
1283
+ }),
1284
+ { config: cfg },
1285
+ 3_000,
1286
+ )
1287
+
1288
+ noLLMServer.instance(
1289
+ "cancel finalizes subtask tool state",
1290
+ () =>
1291
+ Effect.gen(function* () {
1292
+ const ready = yield* Deferred.make<void>()
1293
+ const aborted = yield* Deferred.make<void>()
1294
+ const registry = yield* ToolRegistry.Service
1295
+ const { task } = yield* registry.named()
1296
+ const original = task.execute
1297
+ task.execute = (_args, ctx) =>
1298
+ Effect.callback<never>((_resume) => {
1299
+ ctx.abort.addEventListener("abort", () => succeedVoid(aborted), { once: true })
1300
+ if (ctx.abort.aborted) succeedVoid(aborted)
1301
+ succeedVoid(ready)
1302
+ return Effect.sync(() => succeedVoid(aborted))
1303
+ })
1304
+ yield* Effect.addFinalizer(() => Effect.sync(() => void (task.execute = original)))
1305
+
1306
+ const { prompt, chat } = yield* boot()
1307
+ const msg = yield* user(chat.id, "hello")
1308
+ yield* addSubtask(chat.id, msg.id)
1309
+
1310
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1311
+ yield* awaitWithTimeout(Deferred.await(ready), "timed out waiting for task tool to start", "10 seconds")
1312
+ yield* prompt.cancel(chat.id)
1313
+
1314
+ const exit = yield* Fiber.await(fiber)
1315
+ expect(Exit.isSuccess(exit)).toBe(true)
1316
+ yield* awaitWithTimeout(Deferred.await(aborted), "timed out waiting for task tool abort", "10 seconds")
1317
+
1318
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1319
+ const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
1320
+ expect(taskMsg?.info.role).toBe("assistant")
1321
+ if (!taskMsg || taskMsg.info.role !== "assistant") return
1322
+
1323
+ const tool = toolPart(taskMsg.parts)
1324
+ expect(tool?.type).toBe("tool")
1325
+ if (!tool) return
1326
+
1327
+ expect(tool.state.status).not.toBe("running")
1328
+ expect(taskMsg.info.time.completed).toBeDefined()
1329
+ expect(taskMsg.info.finish).toBeDefined()
1330
+ }),
1331
+ { config: cfg },
1332
+ 30_000,
1333
+ )
1334
+
1335
+ it.instance(
1336
+ "cancel propagates from slash command subtask to child session",
1337
+ () =>
1338
+ Effect.gen(function* () {
1339
+ const { llm } = yield* useServerConfig(providerCfg)
1340
+ const prompt = yield* SessionPrompt.Service
1341
+ const sessions = yield* Session.Service
1342
+ const status = yield* SessionStatus.Service
1343
+ const chat = yield* sessions.create({ title: "Pinned" })
1344
+ yield* llm.hang
1345
+ const msg = yield* user(chat.id, "hello")
1346
+ yield* addSubtask(chat.id, msg.id)
1347
+
1348
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1349
+ yield* llm.wait(1)
1350
+
1351
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1352
+ const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
1353
+ const tool = taskMsg ? toolPart(taskMsg.parts) : undefined
1354
+ const sessionID = tool?.state.status === "running" ? tool.state.metadata?.sessionId : undefined
1355
+ expect(typeof sessionID).toBe("string")
1356
+ if (typeof sessionID !== "string") throw new Error("missing child session id")
1357
+ const childID = SessionID.make(sessionID)
1358
+ expect((yield* status.get(childID)).type).toBe("busy")
1359
+
1360
+ yield* prompt.cancel(chat.id)
1361
+ const exit = yield* Fiber.await(fiber)
1362
+ expect(Exit.isSuccess(exit)).toBe(true)
1363
+
1364
+ expect((yield* status.get(chat.id)).type).toBe("idle")
1365
+ expect((yield* status.get(childID)).type).toBe("idle")
1366
+ }),
1367
+ 10_000,
1368
+ )
1369
+
1370
+ it.instance(
1371
+ "cancel with queued callers resolves all cleanly",
1372
+ () =>
1373
+ Effect.gen(function* () {
1374
+ const { llm } = yield* useServerConfig(providerCfg)
1375
+ const prompt = yield* SessionPrompt.Service
1376
+ const sessions = yield* Session.Service
1377
+ const chat = yield* sessions.create({ title: "Pinned" })
1378
+ yield* llm.hang
1379
+ yield* user(chat.id, "hello")
1380
+
1381
+ const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1382
+ yield* llm.wait(1)
1383
+ const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1384
+ yield* Effect.sleep(50)
1385
+
1386
+ yield* prompt.cancel(chat.id)
1387
+ const [exitA, exitB] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
1388
+ expect(Exit.isSuccess(exitA)).toBe(true)
1389
+ expect(Exit.isSuccess(exitB)).toBe(true)
1390
+ if (Exit.isSuccess(exitA) && Exit.isSuccess(exitB)) {
1391
+ expect(exitA.value.info.id).toBe(exitB.value.info.id)
1392
+ }
1393
+ }),
1394
+ { git: true },
1395
+ 10_000,
1396
+ )
1397
+
1398
+ // Queue semantics
1399
+
1400
+ noLLMServer.instance("concurrent loop callers get same result", () =>
1401
+ Effect.gen(function* () {
1402
+ const { prompt, run, chat } = yield* boot()
1403
+ yield* seed(chat.id, { finish: "stop" })
1404
+
1405
+ const [a, b] = yield* Effect.all([prompt.loop({ sessionID: chat.id }), prompt.loop({ sessionID: chat.id })], {
1406
+ concurrency: "unbounded",
1407
+ })
1408
+
1409
+ expect(a.info.id).toBe(b.info.id)
1410
+ expect(a.info.role).toBe("assistant")
1411
+ yield* run.assertNotBusy(chat.id)
1412
+ }),
1413
+ )
1414
+
1415
+ it.instance("concurrent loop callers all receive same error result", () =>
1416
+ Effect.gen(function* () {
1417
+ const { llm } = yield* useServerConfig(providerCfg)
1418
+ const prompt = yield* SessionPrompt.Service
1419
+ const sessions = yield* Session.Service
1420
+ const chat = yield* sessions.create({ title: "Pinned" })
1421
+
1422
+ yield* llm.fail("boom")
1423
+ yield* user(chat.id, "hello")
1424
+
1425
+ const [a, b] = yield* Effect.all([prompt.loop({ sessionID: chat.id }), prompt.loop({ sessionID: chat.id })], {
1426
+ concurrency: "unbounded",
1427
+ })
1428
+ expect(a.info.id).toBe(b.info.id)
1429
+ expect(a.info.role).toBe("assistant")
1430
+ }),
1431
+ )
1432
+
1433
+ it.instance("prompt submitted during an active run is included in the next LLM input", () =>
1434
+ Effect.gen(function* () {
1435
+ const { llm } = yield* useServerConfig(providerCfg)
1436
+ const gate = yield* Deferred.make<void>()
1437
+ const prompt = yield* SessionPrompt.Service
1438
+ const sessions = yield* Session.Service
1439
+ const chat = yield* sessions.create({ title: "Pinned" })
1440
+
1441
+ yield* llm.hold("first", deferredAsPromise(gate))
1442
+ yield* llm.text("second")
1443
+
1444
+ const a = yield* prompt
1445
+ .prompt({
1446
+ sessionID: chat.id,
1447
+ agent: "build",
1448
+ model: ref,
1449
+ parts: [{ type: "text", text: "first" }],
1450
+ })
1451
+ .pipe(Effect.forkChild)
1452
+
1453
+ yield* llm.wait(1)
1454
+ yield* waitForBusy(chat.id)
1455
+
1456
+ const id = MessageID.ascending()
1457
+ const b = yield* prompt
1458
+ .prompt({
1459
+ sessionID: chat.id,
1460
+ messageID: id,
1461
+ agent: "build",
1462
+ model: ref,
1463
+ parts: [{ type: "text", text: "second" }],
1464
+ })
1465
+ .pipe(Effect.forkChild)
1466
+
1467
+ yield* pollWithTimeout(
1468
+ sessions
1469
+ .messages({ sessionID: chat.id })
1470
+ .pipe(
1471
+ Effect.map((msgs) => (msgs.some((msg) => msg.info.role === "user" && msg.info.id === id) ? true : undefined)),
1472
+ ),
1473
+ "timed out waiting for second prompt to save",
1474
+ )
1475
+
1476
+ yield* Deferred.succeed(gate, void 0)
1477
+
1478
+ const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
1479
+ expect(Exit.isSuccess(ea)).toBe(true)
1480
+ expect(Exit.isSuccess(eb)).toBe(true)
1481
+ expect(yield* llm.calls).toBe(2)
1482
+
1483
+ const msgs = yield* sessions.messages({ sessionID: chat.id })
1484
+ const assistants = msgs.filter((msg) => msg.info.role === "assistant")
1485
+ expect(assistants).toHaveLength(2)
1486
+ const last = assistants.at(-1)
1487
+ if (!last || last.info.role !== "assistant") throw new Error("expected second assistant")
1488
+ expect(last.info.parentID).toBe(id)
1489
+ expect(last.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
1490
+
1491
+ const inputs = yield* llm.inputs
1492
+ expect(inputs).toHaveLength(2)
1493
+ const messages = inputs.at(-1)?.messages
1494
+ if (!Array.isArray(messages)) throw new Error("expected LLM messages")
1495
+ expect(messages.at(-1)).toEqual({ role: "user", content: "second" })
1496
+ }),
1497
+ )
1498
+
1499
+ it.instance("assertNotBusy fails with BusyError when loop running", () =>
1500
+ Effect.gen(function* () {
1501
+ const { llm } = yield* useServerConfig(providerCfg)
1502
+ const prompt = yield* SessionPrompt.Service
1503
+ const run = yield* SessionRunState.Service
1504
+ const sessions = yield* Session.Service
1505
+ yield* llm.hang
1506
+
1507
+ const chat = yield* sessions.create({})
1508
+ yield* user(chat.id, "hi")
1509
+
1510
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1511
+ yield* llm.wait(1)
1512
+ yield* waitForBusy(chat.id)
1513
+
1514
+ const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit)
1515
+ expect(Exit.isFailure(exit)).toBe(true)
1516
+ if (Exit.isFailure(exit)) {
1517
+ expect(Cause.squash(exit.cause)).toBeInstanceOf(Session.BusyError)
1518
+ expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "SessionBusyError", sessionID: chat.id })
1519
+ }
1520
+
1521
+ yield* prompt.cancel(chat.id)
1522
+ yield* Fiber.await(fiber)
1523
+ }),
1524
+ )
1525
+
1526
+ noLLMServer.instance("assertNotBusy succeeds when idle", () =>
1527
+ Effect.gen(function* () {
1528
+ const run = yield* SessionRunState.Service
1529
+ const sessions = yield* Session.Service
1530
+
1531
+ const chat = yield* sessions.create({})
1532
+ const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit)
1533
+ expect(Exit.isSuccess(exit)).toBe(true)
1534
+ }),
1535
+ )
1536
+
1537
+ // Shell semantics
1538
+
1539
+ it.instance("shell rejects with BusyError when loop running", () =>
1540
+ Effect.gen(function* () {
1541
+ const { llm } = yield* useServerConfig(providerCfg)
1542
+ const prompt = yield* SessionPrompt.Service
1543
+ const sessions = yield* Session.Service
1544
+ const chat = yield* sessions.create({ title: "Pinned" })
1545
+ yield* llm.hang
1546
+ yield* user(chat.id, "hi")
1547
+
1548
+ const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1549
+ yield* llm.wait(1)
1550
+ yield* waitForBusy(chat.id)
1551
+
1552
+ const exit = yield* prompt.shell({ sessionID: chat.id, agent: "build", command: "echo hi" }).pipe(Effect.exit)
1553
+ expect(Exit.isFailure(exit)).toBe(true)
1554
+ if (Exit.isFailure(exit)) {
1555
+ expect(Cause.squash(exit.cause)).toBeInstanceOf(Session.BusyError)
1556
+ expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "SessionBusyError", sessionID: chat.id })
1557
+ }
1558
+
1559
+ yield* prompt.cancel(chat.id)
1560
+ yield* Fiber.await(fiber)
1561
+ }),
1562
+ )
1563
+
1564
+ unixNoLLMServer(
1565
+ "shell captures stdout and stderr in completed tool output",
1566
+ () =>
1567
+ Effect.gen(function* () {
1568
+ const { prompt, run, chat } = yield* boot()
1569
+ const result = yield* prompt.shell({
1570
+ sessionID: chat.id,
1571
+ agent: "build",
1572
+ command: "printf out && printf err >&2",
1573
+ })
1574
+
1575
+ expect(result.info.role).toBe("assistant")
1576
+ const tool = completedTool(result.parts)
1577
+ if (!tool) return
1578
+
1579
+ expect(tool.state.output).toContain("out")
1580
+ expect(tool.state.output).toContain("err")
1581
+ expect(tool.state.metadata.output).toContain("out")
1582
+ expect(tool.state.metadata.output).toContain("err")
1583
+ yield* run.assertNotBusy(chat.id)
1584
+ }),
1585
+ { config: cfg },
1586
+ )
1587
+
1588
+ unixNoLLMServer(
1589
+ "shell completes a fast command on the preferred shell",
1590
+ () =>
1591
+ Effect.gen(function* () {
1592
+ const { directory: dir } = yield* TestInstance
1593
+ const { prompt, run, chat } = yield* boot()
1594
+ const result = yield* prompt.shell({
1595
+ sessionID: chat.id,
1596
+ agent: "build",
1597
+ command: "pwd",
1598
+ })
1599
+
1600
+ expect(result.info.role).toBe("assistant")
1601
+ const tool = completedTool(result.parts)
1602
+ if (!tool) return
1603
+
1604
+ expect(tool.state.input.command).toBe("pwd")
1605
+ expect(tool.state.output).toContain(dir)
1606
+ expect(tool.state.metadata.output).toContain(dir)
1607
+ yield* run.assertNotBusy(chat.id)
1608
+ }),
1609
+ { config: cfg },
1610
+ )
1611
+
1612
+ unixNoLLMServer(
1613
+ "shell uses configured shell over env shell",
1614
+ () =>
1615
+ withSh(() =>
1616
+ Effect.gen(function* () {
1617
+ if (!(yield* hasBash)) return
1618
+
1619
+ const { prompt, chat } = yield* boot()
1620
+ const result = yield* prompt.shell({
1621
+ sessionID: chat.id,
1622
+ agent: "build",
1623
+ command: "[[ 1 -eq 1 ]] && printf configured",
1624
+ })
1625
+
1626
+ const tool = completedTool(result.parts)
1627
+ if (!tool) return
1628
+ expect(tool.state.output).toContain("configured")
1629
+ }),
1630
+ ),
1631
+ { config: { ...cfg, shell: "bash" } },
1632
+ 30_000,
1633
+ )
1634
+
1635
+ unixNoLLMServer(
1636
+ "shell commands can change directory after startup",
1637
+ () =>
1638
+ withSh(() =>
1639
+ Effect.gen(function* () {
1640
+ const { directory: dir } = yield* TestInstance
1641
+ const { prompt, run, chat } = yield* boot()
1642
+ const parent = path.dirname(dir)
1643
+ const result = yield* prompt.shell({
1644
+ sessionID: chat.id,
1645
+ agent: "build",
1646
+ command: "cd .. && pwd",
1647
+ })
1648
+
1649
+ expect(result.info.role).toBe("assistant")
1650
+ const tool = completedTool(result.parts)
1651
+ if (!tool) return
1652
+
1653
+ expect(tool.state.output).toContain(parent)
1654
+ expect(tool.state.metadata.output).toContain(parent)
1655
+ yield* run.assertNotBusy(chat.id)
1656
+ }),
1657
+ ),
1658
+ { config: cfg },
1659
+ )
1660
+
1661
+ unixNoLLMServer(
1662
+ "shell lists files from the project directory",
1663
+ () =>
1664
+ Effect.gen(function* () {
1665
+ const { directory: dir } = yield* TestInstance
1666
+ const { prompt, run, chat } = yield* boot()
1667
+ yield* writeText(path.join(dir, "README.md"), "# e2e\n")
1668
+
1669
+ const result = yield* prompt.shell({
1670
+ sessionID: chat.id,
1671
+ agent: "build",
1672
+ command: "command ls",
1673
+ })
1674
+
1675
+ expect(result.info.role).toBe("assistant")
1676
+ const tool = completedTool(result.parts)
1677
+ if (!tool) return
1678
+
1679
+ expect(tool.state.input.command).toBe("command ls")
1680
+ expect(tool.state.output).toContain("README.md")
1681
+ expect(tool.state.metadata.output).toContain("README.md")
1682
+ yield* run.assertNotBusy(chat.id)
1683
+ }),
1684
+ { config: cfg },
1685
+ )
1686
+
1687
+ unixNoLLMServer(
1688
+ "shell captures stderr from a failing command",
1689
+ () =>
1690
+ Effect.gen(function* () {
1691
+ const { prompt, run, chat } = yield* boot()
1692
+ const result = yield* prompt.shell({
1693
+ sessionID: chat.id,
1694
+ agent: "build",
1695
+ command: "command -v __nonexistent_cmd_e2e__ || echo 'not found' >&2; exit 1",
1696
+ })
1697
+
1698
+ expect(result.info.role).toBe("assistant")
1699
+ const tool = completedTool(result.parts)
1700
+ if (!tool) return
1701
+
1702
+ expect(tool.state.output).toContain("not found")
1703
+ expect(tool.state.metadata.output).toContain("not found")
1704
+ yield* run.assertNotBusy(chat.id)
1705
+ }),
1706
+ { config: cfg },
1707
+ )
1708
+
1709
+ unixNoLLMServer(
1710
+ "shell updates running metadata before process exit",
1711
+ () =>
1712
+ withSh(() =>
1713
+ Effect.gen(function* () {
1714
+ const { prompt, chat } = yield* boot()
1715
+
1716
+ const fiber = yield* prompt
1717
+ .shell({ sessionID: chat.id, agent: "build", command: "printf first && sleep 0.2 && printf second" })
1718
+ .pipe(Effect.forkChild)
1719
+
1720
+ yield* pollWithTimeout(
1721
+ Effect.gen(function* () {
1722
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1723
+ const taskMsg = msgs.find((item) => item.info.role === "assistant")
1724
+ const tool = taskMsg ? toolPart(taskMsg.parts) : undefined
1725
+ if (tool?.state.status === "running" && tool.state.metadata?.output.includes("first")) return true
1726
+ }),
1727
+ "timed out waiting for running shell metadata",
1728
+ )
1729
+
1730
+ const exit = yield* Fiber.await(fiber)
1731
+ expect(Exit.isSuccess(exit)).toBe(true)
1732
+ }),
1733
+ ),
1734
+ { config: cfg },
1735
+ 30_000,
1736
+ )
1737
+
1738
+ it.instance(
1739
+ "loop waits while shell runs and starts after shell exits",
1740
+ () =>
1741
+ Effect.gen(function* () {
1742
+ const { llm } = yield* useServerConfig(providerCfg)
1743
+ const prompt = yield* SessionPrompt.Service
1744
+ const sessions = yield* Session.Service
1745
+ const chat = yield* sessions.create({
1746
+ title: "Pinned",
1747
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1748
+ })
1749
+ yield* llm.text("after-shell")
1750
+
1751
+ const sh = yield* prompt
1752
+ .shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" })
1753
+ .pipe(Effect.forkChild)
1754
+ yield* waitForBusy(chat.id)
1755
+
1756
+ const loop = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1757
+ yield* Effect.sleep(50)
1758
+
1759
+ expect(yield* llm.calls).toBe(0)
1760
+
1761
+ yield* Fiber.await(sh)
1762
+ const exit = yield* Fiber.await(loop)
1763
+
1764
+ expect(Exit.isSuccess(exit)).toBe(true)
1765
+ if (Exit.isSuccess(exit)) {
1766
+ expect(exit.value.info.role).toBe("assistant")
1767
+ expect(exit.value.parts.some((part) => part.type === "text" && part.text === "after-shell")).toBe(true)
1768
+ }
1769
+ expect(yield* llm.calls).toBe(1)
1770
+ }),
1771
+ { git: true },
1772
+ 10_000,
1773
+ )
1774
+
1775
+ it.instance(
1776
+ "shell completion resumes queued loop callers",
1777
+ () =>
1778
+ Effect.gen(function* () {
1779
+ const { llm } = yield* useServerConfig(providerCfg)
1780
+ const prompt = yield* SessionPrompt.Service
1781
+ const sessions = yield* Session.Service
1782
+ const chat = yield* sessions.create({
1783
+ title: "Pinned",
1784
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1785
+ })
1786
+ yield* llm.text("done")
1787
+
1788
+ const sh = yield* prompt
1789
+ .shell({ sessionID: chat.id, agent: "build", command: "sleep 0.2" })
1790
+ .pipe(Effect.forkChild)
1791
+ yield* waitForBusy(chat.id)
1792
+
1793
+ const a = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1794
+ const b = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1795
+ yield* Effect.sleep(50)
1796
+
1797
+ expect(yield* llm.calls).toBe(0)
1798
+
1799
+ yield* Fiber.await(sh)
1800
+ const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
1801
+
1802
+ expect(Exit.isSuccess(ea)).toBe(true)
1803
+ expect(Exit.isSuccess(eb)).toBe(true)
1804
+ if (Exit.isSuccess(ea) && Exit.isSuccess(eb)) {
1805
+ expect(ea.value.info.id).toBe(eb.value.info.id)
1806
+ expect(ea.value.info.role).toBe("assistant")
1807
+ }
1808
+ expect(yield* llm.calls).toBe(1)
1809
+ }),
1810
+ { git: true },
1811
+ 10_000,
1812
+ )
1813
+
1814
+ unix(
1815
+ "command ! expansion uses configured shell over env shell",
1816
+ () =>
1817
+ withSh(() =>
1818
+ Effect.gen(function* () {
1819
+ if (!(yield* hasBash)) return
1820
+ const { llm } = yield* useServerConfig((url) => ({
1821
+ ...providerCfg(url),
1822
+ shell: "bash",
1823
+ command: {
1824
+ probe: {
1825
+ template: "Probe: !`[[ 1 -eq 1 ]] && printf configured`",
1826
+ },
1827
+ },
1828
+ }))
1829
+
1830
+ const { prompt, chat } = yield* boot()
1831
+ yield* llm.text("done")
1832
+
1833
+ const result = yield* prompt.command({
1834
+ sessionID: chat.id,
1835
+ command: "probe",
1836
+ arguments: "",
1837
+ })
1838
+
1839
+ expect(result.info.role).toBe("assistant")
1840
+ const inputs = yield* llm.inputs
1841
+ expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("configured")
1842
+ }),
1843
+ ),
1844
+ 30_000,
1845
+ )
1846
+
1847
+ unixNoLLMServer(
1848
+ "cancel interrupts shell and resolves cleanly",
1849
+ () =>
1850
+ withSh(() =>
1851
+ Effect.gen(function* () {
1852
+ const { prompt, run, chat } = yield* boot()
1853
+ const { directory: dir } = yield* TestInstance
1854
+ const afs = yield* FSUtil.Service
1855
+ const ready = path.join(dir, ".shell-ready")
1856
+
1857
+ const sh = yield* prompt
1858
+ .shell({ sessionID: chat.id, agent: "build", command: ": > '.shell-ready'; sleep 30" })
1859
+ .pipe(Effect.forkChild)
1860
+ yield* pollWithTimeout(
1861
+ afs.existsSafe(ready).pipe(Effect.map((exists) => (exists ? (true as const) : undefined))),
1862
+ "shell never created readiness marker",
1863
+ )
1864
+
1865
+ yield* prompt.cancel(chat.id)
1866
+
1867
+ const status = yield* SessionStatus.Service
1868
+ expect((yield* status.get(chat.id)).type).toBe("idle")
1869
+ const busy = yield* run.assertNotBusy(chat.id).pipe(Effect.exit)
1870
+ expect(Exit.isSuccess(busy)).toBe(true)
1871
+
1872
+ const exit = yield* Fiber.await(sh)
1873
+ expect(Exit.isSuccess(exit)).toBe(true)
1874
+ if (Exit.isSuccess(exit)) {
1875
+ expect(exit.value.info.role).toBe("assistant")
1876
+ const tool = completedTool(exit.value.parts)
1877
+ if (tool) {
1878
+ expect(tool.state.output).toContain("User aborted the command")
1879
+ }
1880
+ }
1881
+ }),
1882
+ ),
1883
+ { git: true, config: cfg },
1884
+ 30_000,
1885
+ )
1886
+
1887
+ unixNoLLMServer(
1888
+ "cancel persists aborted shell result when shell ignores TERM",
1889
+ () =>
1890
+ withSh(() =>
1891
+ Effect.gen(function* () {
1892
+ const { prompt, chat } = yield* boot()
1893
+ const { directory: dir } = yield* TestInstance
1894
+ const afs = yield* FSUtil.Service
1895
+ const ready = path.join(dir, ".trap-ready")
1896
+
1897
+ const sh = yield* prompt
1898
+ .shell({
1899
+ sessionID: chat.id,
1900
+ agent: "build",
1901
+ // Touch marker AFTER trap installs so the test waits for the actual
1902
+ // ignore-TERM state before cancelling; otherwise SIGTERM can arrive
1903
+ // before `trap` runs and the escalation path is never exercised.
1904
+ command: `trap '' TERM; touch "${ready}"; sleep 30`,
1905
+ })
1906
+ .pipe(Effect.forkChild)
1907
+
1908
+ yield* Effect.gen(function* () {
1909
+ while (!(yield* afs.existsSafe(ready))) {
1910
+ yield* Effect.sleep(Duration.millis(10))
1911
+ }
1912
+ }).pipe(Effect.timeout(Duration.seconds(5)))
1913
+
1914
+ yield* prompt.cancel(chat.id)
1915
+
1916
+ const exit = yield* Fiber.await(sh)
1917
+ expect(Exit.isSuccess(exit)).toBe(true)
1918
+ if (Exit.isSuccess(exit)) {
1919
+ expect(exit.value.info.role).toBe("assistant")
1920
+ const tool = completedTool(exit.value.parts)
1921
+ if (tool) {
1922
+ expect(tool.state.output).toContain("User aborted the command")
1923
+ }
1924
+ }
1925
+ }),
1926
+ ),
1927
+ { git: true, config: cfg },
1928
+ 30_000,
1929
+ )
1930
+
1931
+ unix(
1932
+ "cancel finalizes interrupted bash tool output through normal truncation",
1933
+ () =>
1934
+ Effect.gen(function* () {
1935
+ const { dir, llm } = yield* useServerConfig(providerCfg)
1936
+ const prompt = yield* SessionPrompt.Service
1937
+ const sessions = yield* Session.Service
1938
+ const chat = yield* sessions.create({
1939
+ title: "Interrupted bash truncation",
1940
+ permission: [{ permission: "*", pattern: "*", action: "allow" }],
1941
+ })
1942
+
1943
+ yield* prompt.prompt({
1944
+ sessionID: chat.id,
1945
+ agent: "build",
1946
+ noReply: true,
1947
+ parts: [{ type: "text", text: "run bash" }],
1948
+ })
1949
+
1950
+ yield* llm.tool("bash", {
1951
+ command:
1952
+ 'i=0; while [ "$i" -lt 4000 ]; do printf "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx %05d\\n" "$i"; i=$((i + 1)); done; printf truncation-ready; sleep 30',
1953
+ timeout: 30_000,
1954
+ workdir: path.resolve(dir),
1955
+ })
1956
+
1957
+ const run = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1958
+ yield* llm.wait(1)
1959
+ yield* pollWithTimeout(
1960
+ Effect.gen(function* () {
1961
+ const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
1962
+ const assistant = msgs.findLast((item) => item.info.role === "assistant")
1963
+ const tool = assistant ? toolPart(assistant.parts) : undefined
1964
+ if (tool?.state.status === "running" && tool.state.metadata?.output.includes("truncation-ready")) return true
1965
+ }),
1966
+ "timed out waiting for truncated shell output",
1967
+ )
1968
+ yield* prompt.cancel(chat.id)
1969
+
1970
+ const exit = yield* Fiber.await(run)
1971
+ expect(Exit.isSuccess(exit)).toBe(true)
1972
+ if (Exit.isFailure(exit)) return
1973
+
1974
+ const tool = completedTool(exit.value.parts)
1975
+ if (!tool) return
1976
+
1977
+ expect(tool.state.metadata.truncated).toBe(true)
1978
+ expect(typeof tool.state.metadata.outputPath).toBe("string")
1979
+ expect(tool.state.output).toMatch(/\.\.\.output truncated\.\.\./)
1980
+ expect(tool.state.output).toMatch(/Full output saved to:\s+\S+/)
1981
+ expect(tool.state.output).not.toContain("Tool execution aborted")
1982
+ }),
1983
+ { git: true },
1984
+ 30_000,
1985
+ )
1986
+
1987
+ unixNoLLMServer(
1988
+ "cancel interrupts loop queued behind shell",
1989
+ () =>
1990
+ Effect.gen(function* () {
1991
+ const { prompt, chat } = yield* boot()
1992
+
1993
+ const sh = yield* prompt.shell({ sessionID: chat.id, agent: "build", command: "sleep 30" }).pipe(Effect.forkChild)
1994
+ yield* waitForBusy(chat.id)
1995
+
1996
+ const loop = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild)
1997
+ yield* Effect.sleep(50)
1998
+
1999
+ yield* prompt.cancel(chat.id)
2000
+
2001
+ const exit = yield* Fiber.await(loop)
2002
+ expect(Exit.isSuccess(exit)).toBe(true)
2003
+ if (Exit.isSuccess(exit)) {
2004
+ const tool = completedTool(exit.value.parts)
2005
+ expect(tool?.state.output).toContain("User aborted the command")
2006
+ }
2007
+
2008
+ yield* Fiber.await(sh)
2009
+ }),
2010
+ { git: true, config: cfg },
2011
+ 30_000,
2012
+ )
2013
+
2014
+ unixNoLLMServer(
2015
+ "shell rejects when another shell is already running",
2016
+ () =>
2017
+ withSh(() =>
2018
+ Effect.gen(function* () {
2019
+ const { prompt, chat } = yield* boot()
2020
+
2021
+ const a = yield* prompt
2022
+ .shell({ sessionID: chat.id, agent: "build", command: "sleep 30" })
2023
+ .pipe(Effect.forkChild)
2024
+ yield* waitForBusy(chat.id)
2025
+
2026
+ const exit = yield* prompt.shell({ sessionID: chat.id, agent: "build", command: "echo hi" }).pipe(Effect.exit)
2027
+ expect(Exit.isFailure(exit)).toBe(true)
2028
+ if (Exit.isFailure(exit)) {
2029
+ expect(Cause.squash(exit.cause)).toBeInstanceOf(Session.BusyError)
2030
+ }
2031
+
2032
+ yield* prompt.cancel(chat.id)
2033
+ yield* Fiber.await(a)
2034
+ }),
2035
+ ),
2036
+ { git: true, config: cfg },
2037
+ 30_000,
2038
+ )
2039
+
2040
+ // Abort signal propagation tests for inline tool execution
2041
+
2042
+ function hangUntilAborted(tool: { execute: (...args: any[]) => any }) {
2043
+ return Effect.gen(function* () {
2044
+ const ready = yield* Deferred.make<void>()
2045
+ const aborted = yield* Deferred.make<void>()
2046
+ const original = tool.execute
2047
+ tool.execute = (_args: any, ctx: any) => {
2048
+ ctx.abort.addEventListener("abort", () => succeedVoid(aborted), { once: true })
2049
+ if (ctx.abort.aborted) succeedVoid(aborted)
2050
+ succeedVoid(ready)
2051
+ return Effect.callback<never>(() => Effect.sync(() => succeedVoid(aborted)))
2052
+ }
2053
+ const restore = Effect.addFinalizer(() => Effect.sync(() => void (tool.execute = original)))
2054
+ return { ready, aborted, restore }
2055
+ })
2056
+ }
2057
+
2058
+ noLLMServer.instance(
2059
+ "interrupt propagates abort signal to read tool via file part (text/plain)",
2060
+ () =>
2061
+ Effect.gen(function* () {
2062
+ const { directory: dir } = yield* TestInstance
2063
+ const registry = yield* ToolRegistry.Service
2064
+ const { read } = yield* registry.named()
2065
+ const { ready, restore } = yield* hangUntilAborted(read)
2066
+ yield* restore
2067
+
2068
+ const prompt = yield* SessionPrompt.Service
2069
+ const sessions = yield* Session.Service
2070
+ const chat = yield* sessions.create({ title: "Abort Test" })
2071
+
2072
+ const testFile = path.join(dir, "test.txt")
2073
+ yield* writeText(testFile, "hello world")
2074
+
2075
+ const fiber = yield* prompt
2076
+ .prompt({
2077
+ sessionID: chat.id,
2078
+ agent: "build",
2079
+ parts: [
2080
+ { type: "text", text: "read this" },
2081
+ { type: "file", url: `file://${testFile}`, filename: "test.txt", mime: "text/plain" },
2082
+ ],
2083
+ })
2084
+ .pipe(Effect.forkChild)
2085
+
2086
+ yield* awaitWithTimeout(Deferred.await(ready), "timed out waiting for read tool to start", "10 seconds")
2087
+ yield* prompt.cancel(chat.id)
2088
+ yield* Fiber.interrupt(fiber)
2089
+ const exit = yield* Fiber.await(fiber)
2090
+ expect(Exit.isFailure(exit)).toBe(true)
2091
+ }),
2092
+ { config: cfg },
2093
+ 30_000,
2094
+ )
2095
+
2096
+ noLLMServer.instance(
2097
+ "interrupt propagates abort signal to read tool via file part (directory)",
2098
+ () =>
2099
+ Effect.gen(function* () {
2100
+ const { directory: dir } = yield* TestInstance
2101
+ const registry = yield* ToolRegistry.Service
2102
+ const { read } = yield* registry.named()
2103
+ const { ready, restore } = yield* hangUntilAborted(read)
2104
+ yield* restore
2105
+
2106
+ const prompt = yield* SessionPrompt.Service
2107
+ const sessions = yield* Session.Service
2108
+ const chat = yield* sessions.create({ title: "Abort Test" })
2109
+
2110
+ const fiber = yield* prompt
2111
+ .prompt({
2112
+ sessionID: chat.id,
2113
+ agent: "build",
2114
+ parts: [
2115
+ { type: "text", text: "read this" },
2116
+ { type: "file", url: `file://${dir}`, filename: "dir", mime: "application/x-directory" },
2117
+ ],
2118
+ })
2119
+ .pipe(Effect.forkChild)
2120
+
2121
+ yield* awaitWithTimeout(Deferred.await(ready), "timed out waiting for read tool to start", "10 seconds")
2122
+ yield* prompt.cancel(chat.id)
2123
+ yield* Fiber.interrupt(fiber)
2124
+ const exit = yield* Fiber.await(fiber)
2125
+ expect(Exit.isFailure(exit)).toBe(true)
2126
+ }),
2127
+ { config: cfg },
2128
+ 30_000,
2129
+ )
2130
+
2131
+ // Missing file handling
2132
+
2133
+ noLLMServer.instance(
2134
+ "does not fail the prompt when a file part is missing",
2135
+ () =>
2136
+ Effect.gen(function* () {
2137
+ const { directory: dir } = yield* TestInstance
2138
+ const prompt = yield* SessionPrompt.Service
2139
+ const sessions = yield* Session.Service
2140
+ const session = yield* sessions.create({})
2141
+
2142
+ const missing = path.join(dir, "does-not-exist.ts")
2143
+ const msg = yield* prompt.prompt({
2144
+ sessionID: session.id,
2145
+ agent: "build",
2146
+ noReply: true,
2147
+ parts: [
2148
+ { type: "text", text: "please review @does-not-exist.ts" },
2149
+ {
2150
+ type: "file",
2151
+ mime: "text/plain",
2152
+ url: `file://${missing}`,
2153
+ filename: "does-not-exist.ts",
2154
+ },
2155
+ ],
2156
+ })
2157
+
2158
+ if (msg.info.role !== "user") throw new Error("expected user message")
2159
+ const hasFailure = msg.parts.some(
2160
+ (part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"),
2161
+ )
2162
+ expect(hasFailure).toBe(true)
2163
+
2164
+ yield* sessions.remove(session.id)
2165
+ }),
2166
+ { config: cfg },
2167
+ )
2168
+
2169
+ noLLMServer.instance(
2170
+ "keeps stored part order stable when file resolution is async",
2171
+ () =>
2172
+ Effect.gen(function* () {
2173
+ const { directory: dir } = yield* TestInstance
2174
+ const prompt = yield* SessionPrompt.Service
2175
+ const sessions = yield* Session.Service
2176
+ const session = yield* sessions.create({})
2177
+
2178
+ const missing = path.join(dir, "still-missing.ts")
2179
+ const msg = yield* prompt.prompt({
2180
+ sessionID: session.id,
2181
+ agent: "build",
2182
+ noReply: true,
2183
+ parts: [
2184
+ {
2185
+ type: "file",
2186
+ mime: "text/plain",
2187
+ url: `file://${missing}`,
2188
+ filename: "still-missing.ts",
2189
+ },
2190
+ { type: "text", text: "after-file" },
2191
+ ],
2192
+ })
2193
+
2194
+ if (msg.info.role !== "user") throw new Error("expected user message")
2195
+
2196
+ const stored = yield* MessageV2.get({
2197
+ sessionID: session.id,
2198
+ messageID: msg.info.id,
2199
+ })
2200
+ const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text)
2201
+
2202
+ expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true)
2203
+ expect(text[1]?.includes("Read tool failed to read")).toBe(true)
2204
+ expect(text[2]).toBe("after-file")
2205
+
2206
+ yield* sessions.remove(session.id)
2207
+ }),
2208
+ { config: cfg },
2209
+ )
2210
+
2211
+ // Special characters in filenames
2212
+
2213
+ noLLMServer.instance(
2214
+ "handles filenames with # character",
2215
+ () =>
2216
+ Effect.gen(function* () {
2217
+ const { directory: dir } = yield* TestInstance
2218
+ yield* writeText(path.join(dir, "file#name.txt"), "special content\n")
2219
+
2220
+ const prompt = yield* SessionPrompt.Service
2221
+ const sessions = yield* Session.Service
2222
+ const session = yield* sessions.create({})
2223
+ const parts = yield* prompt.resolvePromptParts("Read @file#name.txt")
2224
+ const fileParts = parts.filter((part) => part.type === "file")
2225
+
2226
+ expect(fileParts.length).toBe(1)
2227
+ expect(fileParts[0].filename).toBe("file#name.txt")
2228
+ expect(fileParts[0].url).toContain("%23")
2229
+
2230
+ const decodedPath = fileURLToPath(fileParts[0].url)
2231
+ expect(decodedPath).toBe(path.join(dir, "file#name.txt"))
2232
+
2233
+ const message = yield* prompt.prompt({
2234
+ sessionID: session.id,
2235
+ parts,
2236
+ noReply: true,
2237
+ })
2238
+ const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
2239
+ const textParts = stored.parts.filter((part) => part.type === "text")
2240
+ const hasContent = textParts.some((part) => part.text.includes("special content"))
2241
+ expect(hasContent).toBe(true)
2242
+
2243
+ yield* sessions.remove(session.id)
2244
+ }),
2245
+ { git: true, config: cfg },
2246
+ )
2247
+
2248
+ // Regression: empty assistant turn loop
2249
+
2250
+ it.instance("does not loop empty assistant turns for a simple reply", () =>
2251
+ Effect.gen(function* () {
2252
+ const { llm } = yield* useServerConfig(providerCfg)
2253
+ const prompt = yield* SessionPrompt.Service
2254
+ const sessions = yield* Session.Service
2255
+ const session = yield* sessions.create({ title: "Prompt regression" })
2256
+
2257
+ yield* llm.text("packages/opencode/src/session/processor.ts")
2258
+
2259
+ const result = yield* prompt.prompt({
2260
+ sessionID: session.id,
2261
+ agent: "build",
2262
+ parts: [{ type: "text", text: "Where is SessionProcessor?" }],
2263
+ })
2264
+
2265
+ expect(result.info.role).toBe("assistant")
2266
+ expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
2267
+
2268
+ const msgs = yield* sessions.messages({ sessionID: session.id })
2269
+ expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
2270
+ expect(yield* llm.calls).toBe(1)
2271
+ }),
2272
+ )
2273
+
2274
+ it.instance("records aborted errors when prompt is cancelled mid-stream", () =>
2275
+ Effect.gen(function* () {
2276
+ const { llm } = yield* useServerConfig(providerCfg)
2277
+ const prompt = yield* SessionPrompt.Service
2278
+ const sessions = yield* Session.Service
2279
+ const session = yield* sessions.create({ title: "Prompt cancel regression" })
2280
+
2281
+ yield* llm.hang
2282
+
2283
+ const fiber = yield* prompt
2284
+ .prompt({
2285
+ sessionID: session.id,
2286
+ agent: "build",
2287
+ parts: [{ type: "text", text: "Cancel me" }],
2288
+ })
2289
+ .pipe(Effect.forkChild)
2290
+
2291
+ yield* llm.wait(1)
2292
+ yield* waitForBusy(session.id)
2293
+ yield* prompt.cancel(session.id)
2294
+
2295
+ const exit = yield* Fiber.await(fiber)
2296
+ expect(Exit.isSuccess(exit)).toBe(true)
2297
+ if (Exit.isSuccess(exit)) {
2298
+ expect(exit.value.info.role).toBe("assistant")
2299
+ if (exit.value.info.role === "assistant") {
2300
+ expect(exit.value.info.error?.name).toBe("MessageAbortedError")
2301
+ }
2302
+ }
2303
+
2304
+ const msgs = yield* sessions.messages({ sessionID: session.id })
2305
+ const last = msgs.findLast((msg) => msg.info.role === "assistant")
2306
+ expect(last?.info.role).toBe("assistant")
2307
+ if (last?.info.role === "assistant") {
2308
+ expect(last.info.error?.name).toBe("MessageAbortedError")
2309
+ }
2310
+ }),
2311
+ )
2312
+
2313
+ // Agent variant
2314
+
2315
+ noLLMServer.instance(
2316
+ "applies agent variant only when using agent model",
2317
+ () =>
2318
+ Effect.gen(function* () {
2319
+ const prompt = yield* SessionPrompt.Service
2320
+ const sessions = yield* Session.Service
2321
+ const session = yield* sessions.create({})
2322
+
2323
+ const other = yield* prompt.prompt({
2324
+ sessionID: session.id,
2325
+ agent: "build",
2326
+ model: { providerID: ProviderV2.ID.make("opencode"), modelID: ModelV2.ID.make("kimi-k2.5-free") },
2327
+ noReply: true,
2328
+ parts: [{ type: "text", text: "hello" }],
2329
+ })
2330
+ if (other.info.role !== "user") throw new Error("expected user message")
2331
+ expect(other.info.model.variant).toBeUndefined()
2332
+
2333
+ const match = yield* prompt.prompt({
2334
+ sessionID: session.id,
2335
+ agent: "build",
2336
+ noReply: true,
2337
+ parts: [{ type: "text", text: "hello again" }],
2338
+ })
2339
+ if (match.info.role !== "user") throw new Error("expected user message")
2340
+ expect(match.info.model).toEqual({
2341
+ providerID: ProviderV2.ID.make("test"),
2342
+ modelID: ModelV2.ID.make("test-model"),
2343
+ variant: "xhigh",
2344
+ })
2345
+ expect(match.info.model.variant).toBe("xhigh")
2346
+
2347
+ const override = yield* prompt.prompt({
2348
+ sessionID: session.id,
2349
+ agent: "build",
2350
+ noReply: true,
2351
+ variant: "high",
2352
+ parts: [{ type: "text", text: "hello third" }],
2353
+ })
2354
+ if (override.info.role !== "user") throw new Error("expected user message")
2355
+ expect(override.info.model.variant).toBe("high")
2356
+
2357
+ yield* sessions.remove(session.id)
2358
+ }),
2359
+ {
2360
+ config: {
2361
+ ...cfg,
2362
+ provider: {
2363
+ ...cfg.provider,
2364
+ test: {
2365
+ ...cfg.provider.test,
2366
+ models: {
2367
+ "test-model": {
2368
+ ...cfg.provider.test.models["test-model"],
2369
+ variants: { xhigh: {}, high: {} },
2370
+ },
2371
+ },
2372
+ },
2373
+ },
2374
+ agent: {
2375
+ build: {
2376
+ model: "test/test-model",
2377
+ variant: "xhigh",
2378
+ },
2379
+ },
2380
+ },
2381
+ },
2382
+ )
2383
+
2384
+ // Agent / command resolution errors
2385
+
2386
+ noLLMServer.instance(
2387
+ "unknown agent throws typed error",
2388
+ () =>
2389
+ Effect.gen(function* () {
2390
+ const prompt = yield* SessionPrompt.Service
2391
+ const sessions = yield* Session.Service
2392
+ const session = yield* sessions.create({})
2393
+ const exit = yield* prompt
2394
+ .prompt({
2395
+ sessionID: session.id,
2396
+ agent: "nonexistent-agent-xyz",
2397
+ noReply: true,
2398
+ parts: [{ type: "text", text: "hello" }],
2399
+ })
2400
+ .pipe(Effect.exit)
2401
+
2402
+ expect(Exit.isFailure(exit)).toBe(true)
2403
+ if (Exit.isFailure(exit)) {
2404
+ const err = Cause.squash(exit.cause)
2405
+ expect(err).not.toBeInstanceOf(TypeError)
2406
+ expect(NamedError.Unknown.isInstance(err)).toBe(true)
2407
+ if (NamedError.Unknown.isInstance(err)) {
2408
+ expect(err.data.message).toContain('Agent not found: "nonexistent-agent-xyz"')
2409
+ }
2410
+ }
2411
+ }),
2412
+ 30_000,
2413
+ )
2414
+
2415
+ noLLMServer.instance(
2416
+ "unknown agent error includes available agent names",
2417
+ () =>
2418
+ Effect.gen(function* () {
2419
+ const prompt = yield* SessionPrompt.Service
2420
+ const sessions = yield* Session.Service
2421
+ const session = yield* sessions.create({})
2422
+ const exit = yield* prompt
2423
+ .prompt({
2424
+ sessionID: session.id,
2425
+ agent: "nonexistent-agent-xyz",
2426
+ noReply: true,
2427
+ parts: [{ type: "text", text: "hello" }],
2428
+ })
2429
+ .pipe(Effect.exit)
2430
+
2431
+ expect(Exit.isFailure(exit)).toBe(true)
2432
+ if (Exit.isFailure(exit)) {
2433
+ const err = Cause.squash(exit.cause)
2434
+ expect(NamedError.Unknown.isInstance(err)).toBe(true)
2435
+ if (NamedError.Unknown.isInstance(err)) {
2436
+ expect(err.data.message).toContain("build")
2437
+ }
2438
+ }
2439
+ }),
2440
+ 30_000,
2441
+ )
2442
+
2443
+ noLLMServer.instance(
2444
+ "unknown command throws typed error with available names",
2445
+ () =>
2446
+ Effect.gen(function* () {
2447
+ const prompt = yield* SessionPrompt.Service
2448
+ const sessions = yield* Session.Service
2449
+ const session = yield* sessions.create({})
2450
+ const exit = yield* prompt
2451
+ .command({
2452
+ sessionID: session.id,
2453
+ command: "nonexistent-command-xyz",
2454
+ arguments: "",
2455
+ })
2456
+ .pipe(Effect.exit)
2457
+
2458
+ expect(Exit.isFailure(exit)).toBe(true)
2459
+ if (Exit.isFailure(exit)) {
2460
+ const err = Cause.squash(exit.cause)
2461
+ expect(err).not.toBeInstanceOf(TypeError)
2462
+ expect(NamedError.Unknown.isInstance(err)).toBe(true)
2463
+ if (NamedError.Unknown.isInstance(err)) {
2464
+ expect(err.data.message).toContain('Command not found: "nonexistent-command-xyz"')
2465
+ expect(err.data.message).toContain("init")
2466
+ }
2467
+ }
2468
+ }),
2469
+ 30_000,
2470
+ )