@epoch-ai/cli 2.2.4

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 (710) hide show
  1. package/.artifacts/unit/junit.xml +2823 -0
  2. package/.project-map/backups/20260530_223453/.project-map.json +90101 -0
  3. package/.project-map/backups/20260530_223507/.project-map.json +90101 -0
  4. package/.project-map/backups/20260530_223512/.project-map.json +90101 -0
  5. package/.project-map/backups/20260530_223512/map.toon +666 -0
  6. package/.project-map/backups/20260530_223516/.project-map.json +90101 -0
  7. package/.project-map/backups/20260530_223516/map.toon +666 -0
  8. package/.project-map/backups/20260530_223520/.project-map.json +90101 -0
  9. package/.project-map/backups/20260530_223520/map.toon +666 -0
  10. package/AGENTS.md +47 -0
  11. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  12. package/Dockerfile +18 -0
  13. package/README.md +15 -0
  14. package/bin/epochcli +179 -0
  15. package/bunfig.toml +7 -0
  16. package/drizzle.config.ts +10 -0
  17. package/git +0 -0
  18. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  19. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  20. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  21. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  22. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  23. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  24. package/migration/20260225215848_workspace/migration.sql +7 -0
  25. package/migration/20260225215848_workspace/snapshot.json +959 -0
  26. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  27. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  28. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  29. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  30. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  31. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  32. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  33. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  34. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  35. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  36. package/migration/20260323234822_events/migration.sql +13 -0
  37. package/migration/20260323234822_events/snapshot.json +1271 -0
  38. package/migration/20260418092949_add_yolo_to_session/migration.sql +2 -0
  39. package/migration/20260418092949_add_yolo_to_session/snapshot.json +1199 -0
  40. package/migration/20260419120000_add_intervention_to_session/migration.sql +2 -0
  41. package/package.json +186 -0
  42. package/parsers-config.ts +290 -0
  43. package/script/build-node.ts +71 -0
  44. package/script/build.ts +255 -0
  45. package/script/check-migrations.ts +16 -0
  46. package/script/fix-node-pty.ts +28 -0
  47. package/script/postinstall.mjs +131 -0
  48. package/script/publish.ts +184 -0
  49. package/script/schema.ts +63 -0
  50. package/script/seed-e2e.ts +60 -0
  51. package/script/upgrade-opentui.ts +64 -0
  52. package/specs/effect-migration.md +310 -0
  53. package/specs/tui-plugins.md +436 -0
  54. package/specs/v2.md +14 -0
  55. package/src/account/account.sql.ts +39 -0
  56. package/src/account/index.ts +465 -0
  57. package/src/account/repo.ts +163 -0
  58. package/src/account/schema.ts +91 -0
  59. package/src/acp/README.md +174 -0
  60. package/src/acp/agent.ts +1847 -0
  61. package/src/acp/session.ts +116 -0
  62. package/src/acp/types.ts +24 -0
  63. package/src/agent/agent.ts +445 -0
  64. package/src/agent/generate.txt +75 -0
  65. package/src/agent/prompt/compaction.txt +15 -0
  66. package/src/agent/prompt/explore.txt +9 -0
  67. package/src/agent/prompt/summary.txt +11 -0
  68. package/src/agent/prompt/title.txt +44 -0
  69. package/src/auth/index.ts +110 -0
  70. package/src/bus/bus-event.ts +40 -0
  71. package/src/bus/global.ts +10 -0
  72. package/src/bus/index.ts +232 -0
  73. package/src/cli/bootstrap.ts +17 -0
  74. package/src/cli/cmd/account.ts +257 -0
  75. package/src/cli/cmd/acp.ts +70 -0
  76. package/src/cli/cmd/agent.ts +245 -0
  77. package/src/cli/cmd/cmd.ts +7 -0
  78. package/src/cli/cmd/db.ts +119 -0
  79. package/src/cli/cmd/debug/agent.ts +167 -0
  80. package/src/cli/cmd/debug/config.ts +16 -0
  81. package/src/cli/cmd/debug/file.ts +97 -0
  82. package/src/cli/cmd/debug/index.ts +48 -0
  83. package/src/cli/cmd/debug/lsp.ts +53 -0
  84. package/src/cli/cmd/debug/ripgrep.ts +87 -0
  85. package/src/cli/cmd/debug/scrap.ts +16 -0
  86. package/src/cli/cmd/debug/skill.ts +16 -0
  87. package/src/cli/cmd/debug/snapshot.ts +52 -0
  88. package/src/cli/cmd/export.ts +89 -0
  89. package/src/cli/cmd/generate.ts +38 -0
  90. package/src/cli/cmd/github.ts +1639 -0
  91. package/src/cli/cmd/import.ts +169 -0
  92. package/src/cli/cmd/mcp.ts +754 -0
  93. package/src/cli/cmd/models.ts +78 -0
  94. package/src/cli/cmd/plug.ts +233 -0
  95. package/src/cli/cmd/pr.ts +127 -0
  96. package/src/cli/cmd/providers.ts +478 -0
  97. package/src/cli/cmd/run.ts +681 -0
  98. package/src/cli/cmd/serve.ts +24 -0
  99. package/src/cli/cmd/session.ts +159 -0
  100. package/src/cli/cmd/stats.ts +410 -0
  101. package/src/cli/cmd/tui/app.tsx +945 -0
  102. package/src/cli/cmd/tui/attach.ts +88 -0
  103. package/src/cli/cmd/tui/component/border.tsx +21 -0
  104. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  105. package/src/cli/cmd/tui/component/dialog-command.tsx +171 -0
  106. package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
  107. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  108. package/src/cli/cmd/tui/component/dialog-model.tsx +190 -0
  109. package/src/cli/cmd/tui/component/dialog-provider.tsx +364 -0
  110. package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
  111. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  112. package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
  113. package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
  114. package/src/cli/cmd/tui/component/dialog-status.tsx +168 -0
  115. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  116. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  117. package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
  118. package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +320 -0
  119. package/src/cli/cmd/tui/component/error-component.tsx +92 -0
  120. package/src/cli/cmd/tui/component/logo.tsx +85 -0
  121. package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
  122. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +672 -0
  123. package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
  124. package/src/cli/cmd/tui/component/prompt/history.tsx +109 -0
  125. package/src/cli/cmd/tui/component/prompt/index.tsx +1336 -0
  126. package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
  127. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  128. package/src/cli/cmd/tui/component/spinner.tsx +24 -0
  129. package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
  130. package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
  131. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  132. package/src/cli/cmd/tui/component/workspace/dialog-session-list.tsx +151 -0
  133. package/src/cli/cmd/tui/context/args.tsx +15 -0
  134. package/src/cli/cmd/tui/context/directory.ts +13 -0
  135. package/src/cli/cmd/tui/context/exit.tsx +60 -0
  136. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  137. package/src/cli/cmd/tui/context/keybind.tsx +105 -0
  138. package/src/cli/cmd/tui/context/kv.tsx +52 -0
  139. package/src/cli/cmd/tui/context/local.tsx +456 -0
  140. package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
  141. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  142. package/src/cli/cmd/tui/context/route.tsx +52 -0
  143. package/src/cli/cmd/tui/context/sdk.tsx +115 -0
  144. package/src/cli/cmd/tui/context/sync.tsx +516 -0
  145. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  146. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  147. package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
  148. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  149. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  150. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  151. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  152. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  153. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  154. package/src/cli/cmd/tui/context/theme/epochcli.json +245 -0
  155. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  156. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  157. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  158. package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
  159. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  160. package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
  161. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  162. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  163. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  164. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  165. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  166. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  167. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  168. package/src/cli/cmd/tui/context/theme/orng.json +249 -0
  169. package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
  170. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  171. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  172. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  173. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  174. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  175. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  176. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  177. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  178. package/src/cli/cmd/tui/context/theme.tsx +1236 -0
  179. package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
  180. package/src/cli/cmd/tui/event.ts +48 -0
  181. package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
  182. package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +145 -0
  183. package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
  184. package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -0
  185. package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
  186. package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
  187. package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
  188. package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
  189. package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
  190. package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +270 -0
  191. package/src/cli/cmd/tui/plugin/api.tsx +397 -0
  192. package/src/cli/cmd/tui/plugin/index.ts +3 -0
  193. package/src/cli/cmd/tui/plugin/internal.ts +27 -0
  194. package/src/cli/cmd/tui/plugin/runtime.ts +1031 -0
  195. package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
  196. package/src/cli/cmd/tui/routes/home.tsx +84 -0
  197. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
  198. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
  199. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  200. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  201. package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
  202. package/src/cli/cmd/tui/routes/session/index.tsx +2161 -0
  203. package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
  204. package/src/cli/cmd/tui/routes/session/question.tsx +468 -0
  205. package/src/cli/cmd/tui/routes/session/sidebar.tsx +70 -0
  206. package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +131 -0
  207. package/src/cli/cmd/tui/thread.ts +241 -0
  208. package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
  209. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +89 -0
  210. package/src/cli/cmd/tui/ui/dialog-export-options.tsx +211 -0
  211. package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
  212. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +115 -0
  213. package/src/cli/cmd/tui/ui/dialog-select.tsx +417 -0
  214. package/src/cli/cmd/tui/ui/dialog.tsx +192 -0
  215. package/src/cli/cmd/tui/ui/link.tsx +28 -0
  216. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  217. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  218. package/src/cli/cmd/tui/util/clipboard.ts +192 -0
  219. package/src/cli/cmd/tui/util/editor.ts +37 -0
  220. package/src/cli/cmd/tui/util/model.ts +23 -0
  221. package/src/cli/cmd/tui/util/provider-origin.ts +20 -0
  222. package/src/cli/cmd/tui/util/scroll.ts +23 -0
  223. package/src/cli/cmd/tui/util/selection.ts +25 -0
  224. package/src/cli/cmd/tui/util/signal.ts +7 -0
  225. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  226. package/src/cli/cmd/tui/util/transcript.ts +112 -0
  227. package/src/cli/cmd/tui/win32.ts +129 -0
  228. package/src/cli/cmd/tui/worker.ts +195 -0
  229. package/src/cli/cmd/uninstall.ts +353 -0
  230. package/src/cli/cmd/upgrade.ts +73 -0
  231. package/src/cli/cmd/web.ts +81 -0
  232. package/src/cli/effect/prompt.ts +25 -0
  233. package/src/cli/error.ts +46 -0
  234. package/src/cli/heap.ts +59 -0
  235. package/src/cli/logo.ts +6 -0
  236. package/src/cli/network.ts +60 -0
  237. package/src/cli/ui.ts +133 -0
  238. package/src/cli/upgrade.ts +31 -0
  239. package/src/command/index.ts +197 -0
  240. package/src/command/template/initialize.txt +66 -0
  241. package/src/command/template/review.txt +101 -0
  242. package/src/config/config.ts +1610 -0
  243. package/src/config/console-state.ts +15 -0
  244. package/src/config/markdown.ts +99 -0
  245. package/src/config/paths.ts +167 -0
  246. package/src/config/tui-migrate.ts +155 -0
  247. package/src/config/tui-schema.ts +37 -0
  248. package/src/config/tui.ts +179 -0
  249. package/src/config/validator.ts +52 -0
  250. package/src/control-plane/adaptors/index.ts +20 -0
  251. package/src/control-plane/adaptors/worktree.ts +42 -0
  252. package/src/control-plane/schema.ts +17 -0
  253. package/src/control-plane/sse.ts +66 -0
  254. package/src/control-plane/types.ts +32 -0
  255. package/src/control-plane/workspace.sql.ts +17 -0
  256. package/src/control-plane/workspace.ts +168 -0
  257. package/src/effect/cross-spawn-spawner.ts +502 -0
  258. package/src/effect/instance-ref.ts +6 -0
  259. package/src/effect/instance-registry.ts +12 -0
  260. package/src/effect/instance-state.ts +82 -0
  261. package/src/effect/run-service.ts +33 -0
  262. package/src/effect/runner.ts +216 -0
  263. package/src/env/index.ts +28 -0
  264. package/src/file/ignore.ts +82 -0
  265. package/src/file/index.ts +686 -0
  266. package/src/file/protected.ts +59 -0
  267. package/src/file/ripgrep.ts +376 -0
  268. package/src/file/time.ts +133 -0
  269. package/src/file/watcher.ts +172 -0
  270. package/src/filesystem/index.ts +236 -0
  271. package/src/flag/flag.ts +157 -0
  272. package/src/format/formatter.ts +413 -0
  273. package/src/format/index.ts +203 -0
  274. package/src/git/index.ts +303 -0
  275. package/src/global/index.ts +54 -0
  276. package/src/id/id.ts +85 -0
  277. package/src/ide/index.ts +74 -0
  278. package/src/index.ts +253 -0
  279. package/src/installation/index.ts +355 -0
  280. package/src/installation/meta.ts +7 -0
  281. package/src/lsp/client.ts +256 -0
  282. package/src/lsp/index.ts +558 -0
  283. package/src/lsp/language.ts +120 -0
  284. package/src/lsp/launch.ts +21 -0
  285. package/src/lsp/server.ts +1968 -0
  286. package/src/mcp/auth.ts +173 -0
  287. package/src/mcp/index.ts +1250 -0
  288. package/src/mcp/oauth-callback.ts +216 -0
  289. package/src/mcp/oauth-provider.ts +185 -0
  290. package/src/mcp/schema-loader.ts +82 -0
  291. package/src/node.ts +1 -0
  292. package/src/npm/index.ts +188 -0
  293. package/src/patch/index.ts +680 -0
  294. package/src/permission/arity.ts +163 -0
  295. package/src/permission/evaluate.ts +15 -0
  296. package/src/permission/index.ts +323 -0
  297. package/src/permission/schema.ts +17 -0
  298. package/src/plugin/cloudflare.ts +67 -0
  299. package/src/plugin/codex.ts +608 -0
  300. package/src/plugin/github-copilot/copilot.ts +361 -0
  301. package/src/plugin/github-copilot/models.ts +144 -0
  302. package/src/plugin/index.ts +288 -0
  303. package/src/plugin/install.ts +439 -0
  304. package/src/plugin/loader.ts +174 -0
  305. package/src/plugin/meta.ts +188 -0
  306. package/src/plugin/shared.ts +323 -0
  307. package/src/project/bootstrap.ts +29 -0
  308. package/src/project/instance.ts +175 -0
  309. package/src/project/project.sql.ts +16 -0
  310. package/src/project/project.ts +519 -0
  311. package/src/project/schema.ts +16 -0
  312. package/src/project/state.ts +70 -0
  313. package/src/project/vcs.ts +240 -0
  314. package/src/provider/auth.ts +253 -0
  315. package/src/provider/error.ts +297 -0
  316. package/src/provider/models.ts +162 -0
  317. package/src/provider/provider.ts +1776 -0
  318. package/src/provider/schema.ts +38 -0
  319. package/src/provider/sdk/copilot/README.md +5 -0
  320. package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
  321. package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
  322. package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
  323. package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
  324. package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +814 -0
  325. package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
  326. package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
  327. package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
  328. package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
  329. package/src/provider/sdk/copilot/index.ts +2 -0
  330. package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
  331. package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
  332. package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
  333. package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
  334. package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
  335. package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
  336. package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1769 -0
  337. package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
  338. package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
  339. package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
  340. package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
  341. package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
  342. package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
  343. package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
  344. package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
  345. package/src/provider/transform.ts +1124 -0
  346. package/src/pty/index.ts +397 -0
  347. package/src/pty/pty.bun.ts +26 -0
  348. package/src/pty/pty.node.ts +27 -0
  349. package/src/pty/pty.ts +25 -0
  350. package/src/pty/schema.ts +17 -0
  351. package/src/question/index.ts +224 -0
  352. package/src/question/schema.ts +17 -0
  353. package/src/server/error.ts +36 -0
  354. package/src/server/event.ts +7 -0
  355. package/src/server/instance.ts +315 -0
  356. package/src/server/mdns.ts +60 -0
  357. package/src/server/middleware.ts +33 -0
  358. package/src/server/projectors.ts +28 -0
  359. package/src/server/proxy.ts +130 -0
  360. package/src/server/router.ts +105 -0
  361. package/src/server/routes/config.ts +92 -0
  362. package/src/server/routes/event.ts +83 -0
  363. package/src/server/routes/experimental.ts +374 -0
  364. package/src/server/routes/file.ts +197 -0
  365. package/src/server/routes/global.ts +312 -0
  366. package/src/server/routes/mcp.ts +225 -0
  367. package/src/server/routes/permission.ts +69 -0
  368. package/src/server/routes/project.ts +118 -0
  369. package/src/server/routes/provider.ts +171 -0
  370. package/src/server/routes/pty.ts +210 -0
  371. package/src/server/routes/question.ts +99 -0
  372. package/src/server/routes/session.ts +984 -0
  373. package/src/server/routes/tui.ts +378 -0
  374. package/src/server/routes/workspace.ts +94 -0
  375. package/src/server/server.ts +353 -0
  376. package/src/session/compaction.ts +86 -0
  377. package/src/session/index.ts +904 -0
  378. package/src/session/instruction.ts +261 -0
  379. package/src/session/llm/monitor.ts +87 -0
  380. package/src/session/llm.ts +1676 -0
  381. package/src/session/message-v2.ts +1082 -0
  382. package/src/session/message.ts +191 -0
  383. package/src/session/overflow.ts +35 -0
  384. package/src/session/processor.ts +635 -0
  385. package/src/session/projectors.ts +136 -0
  386. package/src/session/prompt/build-switch.txt +5 -0
  387. package/src/session/prompt/builder.ts +135 -0
  388. package/src/session/prompt/default.txt +11 -0
  389. package/src/session/prompt/engine.ts +1072 -0
  390. package/src/session/prompt/gemma4.txt +1 -0
  391. package/src/session/prompt/max-steps.txt +16 -0
  392. package/src/session/prompt/orchestrator.ts +426 -0
  393. package/src/session/prompt/plan.txt +28 -0
  394. package/src/session/prompt/qwen.txt +19 -0
  395. package/src/session/prompt/resolver.ts +670 -0
  396. package/src/session/prompt/router.ts +197 -0
  397. package/src/session/prompt/state.ts +96 -0
  398. package/src/session/prompt/types.ts +115 -0
  399. package/src/session/prompt/utils.ts +15 -0
  400. package/src/session/prompt.ts +362 -0
  401. package/src/session/retry.ts +106 -0
  402. package/src/session/revert.ts +176 -0
  403. package/src/session/sanitizer.ts +125 -0
  404. package/src/session/schema.ts +38 -0
  405. package/src/session/session.sql.ts +106 -0
  406. package/src/session/status.ts +102 -0
  407. package/src/session/summary.ts +183 -0
  408. package/src/session/system.ts +79 -0
  409. package/src/session/todo.ts +166 -0
  410. package/src/session/worker.ts +382 -0
  411. package/src/shell/shell.ts +110 -0
  412. package/src/skill/discovery.ts +116 -0
  413. package/src/skill/index.ts +287 -0
  414. package/src/snapshot/index.ts +726 -0
  415. package/src/sql.d.ts +4 -0
  416. package/src/storage/db.bun.ts +8 -0
  417. package/src/storage/db.node.ts +8 -0
  418. package/src/storage/db.ts +174 -0
  419. package/src/storage/json-migration.ts +387 -0
  420. package/src/storage/schema.sql.ts +10 -0
  421. package/src/storage/schema.ts +4 -0
  422. package/src/storage/storage.ts +353 -0
  423. package/src/sync/README.md +179 -0
  424. package/src/sync/event.sql.ts +16 -0
  425. package/src/sync/index.ts +263 -0
  426. package/src/sync/schema.ts +14 -0
  427. package/src/tool/apply_patch.ts +281 -0
  428. package/src/tool/apply_patch.txt +1 -0
  429. package/src/tool/arbitration.txt +5 -0
  430. package/src/tool/bash.ts +494 -0
  431. package/src/tool/bash.txt +2 -0
  432. package/src/tool/batch.ts +183 -0
  433. package/src/tool/batch.txt +1 -0
  434. package/src/tool/codesearch.ts +132 -0
  435. package/src/tool/codesearch.txt +1 -0
  436. package/src/tool/edit.ts +734 -0
  437. package/src/tool/edit.txt +1 -0
  438. package/src/tool/external-directory.ts +46 -0
  439. package/src/tool/glob.ts +73 -0
  440. package/src/tool/glob.txt +2 -0
  441. package/src/tool/grep.ts +156 -0
  442. package/src/tool/grep.txt +2 -0
  443. package/src/tool/invalid.ts +20 -0
  444. package/src/tool/ls.ts +121 -0
  445. package/src/tool/ls.txt +1 -0
  446. package/src/tool/lsp.ts +97 -0
  447. package/src/tool/lsp.txt +1 -0
  448. package/src/tool/multiedit.ts +46 -0
  449. package/src/tool/multiedit.txt +1 -0
  450. package/src/tool/plan-enter.txt +14 -0
  451. package/src/tool/plan-exit.txt +13 -0
  452. package/src/tool/plan.ts +131 -0
  453. package/src/tool/question.ts +46 -0
  454. package/src/tool/question.txt +10 -0
  455. package/src/tool/read.ts +332 -0
  456. package/src/tool/read.txt +1 -0
  457. package/src/tool/registry.ts +288 -0
  458. package/src/tool/revert.ts +37 -0
  459. package/src/tool/schema.ts +17 -0
  460. package/src/tool/skill.ts +105 -0
  461. package/src/tool/task.ts +150 -0
  462. package/src/tool/task.txt +3 -0
  463. package/src/tool/task_complete.ts +21 -0
  464. package/src/tool/tool.ts +112 -0
  465. package/src/tool/truncate.ts +144 -0
  466. package/src/tool/truncation-dir.ts +4 -0
  467. package/src/tool/webfetch.ts +206 -0
  468. package/src/tool/webfetch.txt +1 -0
  469. package/src/tool/websearch.ts +150 -0
  470. package/src/tool/websearch.txt +1 -0
  471. package/src/tool/write.ts +101 -0
  472. package/src/tool/write.txt +1 -0
  473. package/src/util/abort.ts +35 -0
  474. package/src/util/ai-sdk.ts +59 -0
  475. package/src/util/archive.ts +17 -0
  476. package/src/util/color.ts +19 -0
  477. package/src/util/context.ts +25 -0
  478. package/src/util/data-url.ts +9 -0
  479. package/src/util/defer.ts +12 -0
  480. package/src/util/effect-http-client.ts +11 -0
  481. package/src/util/effect-zod.ts +98 -0
  482. package/src/util/error.ts +77 -0
  483. package/src/util/filesystem.ts +245 -0
  484. package/src/util/flock.ts +333 -0
  485. package/src/util/fn.ts +21 -0
  486. package/src/util/format.ts +20 -0
  487. package/src/util/glob.ts +34 -0
  488. package/src/util/hash.ts +7 -0
  489. package/src/util/iife.ts +3 -0
  490. package/src/util/keybind.ts +103 -0
  491. package/src/util/lazy.ts +23 -0
  492. package/src/util/locale.ts +81 -0
  493. package/src/util/lock.ts +98 -0
  494. package/src/util/log-parser.ts +114 -0
  495. package/src/util/log.ts +250 -0
  496. package/src/util/network.ts +23 -0
  497. package/src/util/process.ts +176 -0
  498. package/src/util/queue.ts +32 -0
  499. package/src/util/record.ts +3 -0
  500. package/src/util/rpc.ts +66 -0
  501. package/src/util/schema.ts +53 -0
  502. package/src/util/scrap.ts +10 -0
  503. package/src/util/session-analyzer.ts +331 -0
  504. package/src/util/session-telemetry.ts +91 -0
  505. package/src/util/signal.ts +12 -0
  506. package/src/util/timeout.ts +14 -0
  507. package/src/util/token.ts +7 -0
  508. package/src/util/tokenizer.ts +50 -0
  509. package/src/util/toon.ts +45 -0
  510. package/src/util/update-schema.ts +13 -0
  511. package/src/util/which.ts +14 -0
  512. package/src/util/wildcard.ts +59 -0
  513. package/src/worktree/index.ts +612 -0
  514. package/sst-env.d.ts +10 -0
  515. package/test/AGENTS.md +81 -0
  516. package/test/account/repo.test.ts +326 -0
  517. package/test/account/service.test.ts +393 -0
  518. package/test/acp/agent-interface.test.ts +51 -0
  519. package/test/acp/event-subscription.test.ts +685 -0
  520. package/test/agent/agent.test.ts +716 -0
  521. package/test/auth/auth.test.ts +58 -0
  522. package/test/bus/bus-effect.test.ts +164 -0
  523. package/test/bus/bus-integration.test.ts +87 -0
  524. package/test/bus/bus.test.ts +219 -0
  525. package/test/cli/account.test.ts +26 -0
  526. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  527. package/test/cli/github-action.test.ts +198 -0
  528. package/test/cli/github-remote.test.ts +80 -0
  529. package/test/cli/plugin-auth-picker.test.ts +120 -0
  530. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  531. package/test/cli/tui/plugin-add.test.ts +107 -0
  532. package/test/cli/tui/plugin-install.test.ts +89 -0
  533. package/test/cli/tui/plugin-lifecycle.test.ts +225 -0
  534. package/test/cli/tui/plugin-loader-entrypoint.test.ts +492 -0
  535. package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
  536. package/test/cli/tui/plugin-loader.test.ts +752 -0
  537. package/test/cli/tui/plugin-toggle.test.ts +159 -0
  538. package/test/cli/tui/slot-replace.test.tsx +47 -0
  539. package/test/cli/tui/theme-store.test.ts +51 -0
  540. package/test/cli/tui/thread.test.ts +128 -0
  541. package/test/cli/tui/transcript.test.ts +426 -0
  542. package/test/config/agent-color.test.ts +71 -0
  543. package/test/config/config.test.ts +2337 -0
  544. package/test/config/fixtures/empty-frontmatter.md +4 -0
  545. package/test/config/fixtures/frontmatter.md +28 -0
  546. package/test/config/fixtures/markdown-header.md +11 -0
  547. package/test/config/fixtures/no-frontmatter.md +1 -0
  548. package/test/config/fixtures/weird-model-id.md +13 -0
  549. package/test/config/markdown.test.ts +228 -0
  550. package/test/config/tui.test.ts +800 -0
  551. package/test/control-plane/sse.test.ts +56 -0
  552. package/test/effect/cross-spawn-spawner.test.ts +412 -0
  553. package/test/effect/instance-state.test.ts +482 -0
  554. package/test/effect/run-service.test.ts +46 -0
  555. package/test/effect/runner.test.ts +523 -0
  556. package/test/fake/provider.ts +82 -0
  557. package/test/file/fsmonitor.test.ts +62 -0
  558. package/test/file/ignore.test.ts +10 -0
  559. package/test/file/index.test.ts +946 -0
  560. package/test/file/path-traversal.test.ts +198 -0
  561. package/test/file/ripgrep.test.ts +54 -0
  562. package/test/file/time.test.ts +445 -0
  563. package/test/file/watcher.test.ts +247 -0
  564. package/test/filesystem/filesystem.test.ts +319 -0
  565. package/test/fixture/db.ts +11 -0
  566. package/test/fixture/fixture.test.ts +26 -0
  567. package/test/fixture/fixture.ts +172 -0
  568. package/test/fixture/flock-worker.ts +72 -0
  569. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  570. package/test/fixture/plug-worker.ts +93 -0
  571. package/test/fixture/plugin-meta-worker.ts +26 -0
  572. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  573. package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
  574. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  575. package/test/fixture/skills/index.json +6 -0
  576. package/test/fixture/tui-plugin.ts +328 -0
  577. package/test/fixture/tui-runtime.ts +27 -0
  578. package/test/format/format.test.ts +171 -0
  579. package/test/git/git.test.ts +128 -0
  580. package/test/ide/ide.test.ts +82 -0
  581. package/test/installation/installation.test.ts +152 -0
  582. package/test/keybind.test.ts +421 -0
  583. package/test/lib/effect.ts +53 -0
  584. package/test/lib/filesystem.ts +10 -0
  585. package/test/lib/llm-server.ts +794 -0
  586. package/test/lsp/client.test.ts +95 -0
  587. package/test/lsp/index.test.ts +133 -0
  588. package/test/lsp/launch.test.ts +22 -0
  589. package/test/lsp/lifecycle.test.ts +147 -0
  590. package/test/mcp/headers.test.ts +153 -0
  591. package/test/mcp/lifecycle.test.ts +750 -0
  592. package/test/mcp/oauth-auto-connect.test.ts +199 -0
  593. package/test/mcp/oauth-browser.test.ts +249 -0
  594. package/test/mcp/sc-approve-validator.test.ts +431 -0
  595. package/test/memory/abort-leak.test.ts +137 -0
  596. package/test/npm.test.ts +18 -0
  597. package/test/patch/patch.test.ts +348 -0
  598. package/test/permission/arity.test.ts +33 -0
  599. package/test/permission/next.test.ts +1123 -0
  600. package/test/permission-task.test.ts +323 -0
  601. package/test/plugin/auth-override.test.ts +74 -0
  602. package/test/plugin/codex.test.ts +123 -0
  603. package/test/plugin/github-copilot-models.test.ts +117 -0
  604. package/test/plugin/install-concurrency.test.ts +140 -0
  605. package/test/plugin/install.test.ts +570 -0
  606. package/test/plugin/loader-shared.test.ts +1136 -0
  607. package/test/plugin/meta.test.ts +137 -0
  608. package/test/plugin/shared.test.ts +88 -0
  609. package/test/plugin/trigger.test.ts +111 -0
  610. package/test/preload.ts +90 -0
  611. package/test/project/migrate-global.test.ts +140 -0
  612. package/test/project/project.test.ts +459 -0
  613. package/test/project/state.test.ts +115 -0
  614. package/test/project/vcs.test.ts +228 -0
  615. package/test/project/worktree-remove.test.ts +96 -0
  616. package/test/project/worktree.test.ts +173 -0
  617. package/test/provider/amazon-bedrock.test.ts +447 -0
  618. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  619. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  620. package/test/provider/error.test.ts +49 -0
  621. package/test/provider/gitlab-duo.test.ts +412 -0
  622. package/test/provider/provider.test.ts +2494 -0
  623. package/test/provider/transform.test.ts +2944 -0
  624. package/test/pty/pty-output-isolation.test.ts +141 -0
  625. package/test/pty/pty-session.test.ts +92 -0
  626. package/test/pty/pty-shell.test.ts +59 -0
  627. package/test/question/question.test.ts +453 -0
  628. package/test/server/global-session-list.test.ts +89 -0
  629. package/test/server/project-init-git.test.ts +121 -0
  630. package/test/server/session-actions.test.ts +83 -0
  631. package/test/server/session-list.test.ts +98 -0
  632. package/test/server/session-messages.test.ts +159 -0
  633. package/test/server/session-select.test.ts +84 -0
  634. package/test/session/compaction.test.ts +683 -0
  635. package/test/session/continuity-handover.test.ts +620 -0
  636. package/test/session/deterministic-handover.test.ts +328 -0
  637. package/test/session/doom-protection.test.ts +247 -0
  638. package/test/session/hard-reset.test.ts +179 -0
  639. package/test/session/instruction.test.ts +286 -0
  640. package/test/session/llm/monitor.test.ts +53 -0
  641. package/test/session/llm-sanitizer.test.ts +90 -0
  642. package/test/session/llm-zones-e2e.test.ts +61 -0
  643. package/test/session/llm.test.ts +1308 -0
  644. package/test/session/mcpx-normalization.test.ts +86 -0
  645. package/test/session/mcpx-syntax-recovery.test.ts +28 -0
  646. package/test/session/message-v2.test.ts +957 -0
  647. package/test/session/messages-pagination.test.ts +885 -0
  648. package/test/session/processor-effect.test.ts +805 -0
  649. package/test/session/prompt/builder.test.ts +71 -0
  650. package/test/session/prompt/engine-loop.test.ts +80 -0
  651. package/test/session/prompt/orchestrator.test.ts +108 -0
  652. package/test/session/prompt/resolver.test.ts +211 -0
  653. package/test/session/prompt/router.test.ts +84 -0
  654. package/test/session/prompt/state.test.ts +57 -0
  655. package/test/session/prompt-effect.test.ts +1241 -0
  656. package/test/session/prompt.test.ts +522 -0
  657. package/test/session/refactor-system-zones.test.ts +241 -0
  658. package/test/session/retry.test.ts +232 -0
  659. package/test/session/revert-compact.test.ts +621 -0
  660. package/test/session/sanitizer.test.ts +61 -0
  661. package/test/session/session.test.ts +142 -0
  662. package/test/session/snapshot-tool-race.test.ts +242 -0
  663. package/test/session/structured-output-integration.test.ts +233 -0
  664. package/test/session/structured-output.test.ts +391 -0
  665. package/test/session/system.test.ts +59 -0
  666. package/test/session/telemetry.test.ts +35 -0
  667. package/test/shell/shell.test.ts +73 -0
  668. package/test/skill/discovery.test.ts +116 -0
  669. package/test/skill/skill.test.ts +392 -0
  670. package/test/snapshot/snapshot.test.ts +1404 -0
  671. package/test/storage/db.test.ts +14 -0
  672. package/test/storage/json-migration.test.ts +791 -0
  673. package/test/storage/storage.test.ts +295 -0
  674. package/test/sync/index.test.ts +191 -0
  675. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  676. package/test/tool/apply_patch.test.ts +567 -0
  677. package/test/tool/bash.test.ts +1099 -0
  678. package/test/tool/edit.test.ts +681 -0
  679. package/test/tool/external-directory.test.ts +198 -0
  680. package/test/tool/fixtures/large-image.png +0 -0
  681. package/test/tool/fixtures/models-api.json +65179 -0
  682. package/test/tool/grep.test.ts +111 -0
  683. package/test/tool/question.test.ts +126 -0
  684. package/test/tool/read.test.ts +468 -0
  685. package/test/tool/registry.test.ts +126 -0
  686. package/test/tool/skill.test.ts +167 -0
  687. package/test/tool/task.test.ts +49 -0
  688. package/test/tool/tool-define.test.ts +101 -0
  689. package/test/tool/truncation.test.ts +161 -0
  690. package/test/tool/webfetch.test.ts +101 -0
  691. package/test/tool/write.test.ts +354 -0
  692. package/test/util/data-url.test.ts +14 -0
  693. package/test/util/effect-zod.test.ts +61 -0
  694. package/test/util/error.test.ts +38 -0
  695. package/test/util/filesystem.test.ts +656 -0
  696. package/test/util/flock.test.ts +383 -0
  697. package/test/util/format.test.ts +59 -0
  698. package/test/util/glob.test.ts +164 -0
  699. package/test/util/iife.test.ts +36 -0
  700. package/test/util/lazy.test.ts +50 -0
  701. package/test/util/lock.test.ts +72 -0
  702. package/test/util/log-parser.test.ts +61 -0
  703. package/test/util/module.test.ts +59 -0
  704. package/test/util/process.test.ts +128 -0
  705. package/test/util/telemetry-integration.test.ts +104 -0
  706. package/test/util/timeout.test.ts +21 -0
  707. package/test/util/which.test.ts +100 -0
  708. package/test/util/wildcard.test.ts +90 -0
  709. package/test-regex.js +50 -0
  710. package/tsconfig.json +23 -0
@@ -0,0 +1,1676 @@
1
+ import { Provider } from "@/provider/provider"
2
+ import { Log } from "@/util/log"
3
+ import { SessionTelemetry } from "@/util/session-telemetry"
4
+ import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
5
+ import * as Queue from "effect/Queue"
6
+ import * as Stream from "effect/Stream"
7
+ import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema, generateText } from "@/util/ai-sdk"
8
+ import Ajv from "ajv"
9
+
10
+ const ajv = new Ajv({
11
+ strict: false,
12
+ allErrors: true,
13
+ })
14
+
15
+ import { mergeDeep, pipe } from "remeda"
16
+ import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
17
+ import { ProviderTransform } from "@/provider/transform"
18
+ import { Config } from "@/config/config"
19
+ import { Instance } from "@/project/instance"
20
+ import type { Agent } from "@/agent/agent"
21
+ import { MessageV2 } from "./message-v2"
22
+ import { SanitizerMiddleware } from "./sanitizer"
23
+ import { Plugin } from "@/plugin"
24
+ import { SystemPrompt } from "./system"
25
+ import { PromptBuilder, type ZoneStructuredPayload } from "./prompt/builder"
26
+ import { Flag } from "@/flag/flag"
27
+ import { Permission } from "@/permission"
28
+ import { Auth } from "@/auth"
29
+ import { Installation } from "@/installation"
30
+ import { ToonEncoder } from "@/util/toon"
31
+ import { MCP } from "@/mcp/index"
32
+ import { StreamingMonitor } from "./llm/monitor"
33
+ import { SchemaContextLoader } from "@/mcp/schema-loader"
34
+
35
+ export namespace LLM {
36
+ const log = Log.create({ service: "llm" })
37
+ export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
38
+
39
+ interface SessionMetadata {
40
+ phaseTurnCount: number
41
+ lastPhase: string
42
+ consecutiveFailures: Map<string, number>
43
+ }
44
+
45
+ const sessionMetadata = new Map<string, SessionMetadata>()
46
+
47
+ export type StreamInput = {
48
+ user: MessageV2.User
49
+ sessionID: string
50
+ parentSessionID?: string
51
+ model: Provider.Model
52
+ agent: Agent.Info
53
+ permission?: Permission.Ruleset
54
+ system: { zone1: string[]; zone2: string[] }
55
+ operationalFacts?: string[]
56
+ instructions?: string[]
57
+ messages: ModelMessage[]
58
+ small?: boolean
59
+ yolo?: boolean
60
+ isContinue?: boolean
61
+ tools: Record<string, Tool>
62
+ retries?: number
63
+ toolChoice?: "auto" | "required" | "none"
64
+ }
65
+
66
+ export type StreamRequest = StreamInput & {
67
+ abort: AbortSignal
68
+ }
69
+
70
+ export type Event = Awaited<ReturnType<typeof stream>>["fullStream"] extends AsyncIterable<infer T> ? T : never
71
+
72
+ export interface Interface {
73
+ readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
74
+ }
75
+
76
+ export class Service extends ServiceMap.Service<Service, Interface>()("@epochcli/LLM") {}
77
+
78
+ export const layer = Layer.effect(
79
+ Service,
80
+ Effect.gen(function* () {
81
+ return Service.of({
82
+ stream(input) {
83
+ return Stream.scoped(
84
+ Stream.unwrap(
85
+ Effect.gen(function* () {
86
+ const ctrl = yield* Effect.acquireRelease(
87
+ Effect.sync(() => new AbortController()),
88
+ (ctrl) => Effect.sync(() => ctrl.abort()),
89
+ )
90
+
91
+ const result = yield* Effect.promise(() => LLM.stream({ ...input, abort: ctrl.signal }))
92
+
93
+ // Robustly suppress unhandled rejections on known background promises returned by the AI SDK
94
+ // Explicitly access getters because for...in misses non-enumerable properties on the prototype.
95
+ const promiseKeys = ["text", "usage", "finishReason", "toolCalls", "toolResults", "warnings", "providerMetadata", "steps", "response"]
96
+ for (const key of promiseKeys) {
97
+ try {
98
+ const value = (result as any)[key]
99
+ if (value instanceof Promise) {
100
+ value.catch(() => {})
101
+ }
102
+ } catch {}
103
+ }
104
+
105
+ return Stream.fromAsyncIterable(result.fullStream, (e) =>
106
+ e instanceof Error ? e : new Error(String(e)),
107
+ )
108
+ }),
109
+ ),
110
+ )
111
+ },
112
+ })
113
+ }),
114
+ )
115
+
116
+ export const defaultLayer = layer
117
+
118
+ export function parseGroundTruthRules(
119
+ raw: string,
120
+ activePacks: string[] = ["core_interaction_pack"],
121
+ contextLimit?: number,
122
+ ): { operationalFacts: string; behavioralRules: string; projectSpecific: string } {
123
+ const zones = {
124
+ operationalFacts: "",
125
+ behavioralRules: "",
126
+ projectSpecific: "",
127
+ }
128
+
129
+ const zone1Match = raw.match(/(ZONE 1 & 3:.*?)(?=ZONE 2:|$)/s)
130
+ if (zone1Match) {
131
+ const factsStr = zone1Match[1]
132
+ const factsRegex = /fact_\d+,\s*"[^"]+",\s*"[^"]+",\s*"([^"]+)"/g
133
+ const extractedFacts: string[] = []
134
+ let match
135
+ while ((match = factsRegex.exec(factsStr)) !== null) {
136
+ let fact = match[1]
137
+ // Dynamic Context Limit Injection
138
+ if (fact.includes("[CONTEXT_LIMIT]")) {
139
+ const limitStr = `${Math.round((contextLimit ?? 32000) / 1000)}K tokens`
140
+ fact = fact.replace("[CONTEXT_LIMIT]", limitStr)
141
+ }
142
+ extractedFacts.push(`- ${fact}`)
143
+ }
144
+
145
+ if (extractedFacts.length > 0) {
146
+ zones.operationalFacts = `ZONE 1 & 3: OPERATIONAL FACTS\n${extractedFacts.join("\n")}`
147
+ } else {
148
+ zones.operationalFacts = factsStr.trim()
149
+ }
150
+ }
151
+
152
+ const zone2Match = raw.match(/(ZONE 2: BEHAVIORAL RULE PACKS.*?)(?=ZONE 3: PROJECT-SPECIFIC RULES|$)/s)
153
+ if (zone2Match) {
154
+ const zone2Str = zone2Match[1]
155
+
156
+ const ruleIds = new Set<string>()
157
+ for (const targetPack of activePacks) {
158
+ const packRegex = new RegExp(`${targetPack}:\\s*\\[(.*?)\\]`)
159
+ const packMatch = zone2Str.match(packRegex)
160
+ if (packMatch) {
161
+ packMatch[1].split(",").forEach((s: string) => {
162
+ const id = s.trim().split(".").pop() || ""
163
+ if (id) ruleIds.add(id)
164
+ })
165
+ }
166
+ }
167
+
168
+ if (ruleIds.size > 0) {
169
+ const rulesRegex = /([a-z]+_\d+),\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)"/g
170
+ const extractedRules: string[] = []
171
+ let rMatch
172
+ while ((rMatch = rulesRegex.exec(zone2Str)) !== null) {
173
+ if (ruleIds.has(rMatch[1])) {
174
+ extractedRules.push(`Trigger: ${rMatch[2]}\nBehaviour: ${rMatch[3]}\nExample: ${rMatch[4]}\n`)
175
+ }
176
+ }
177
+
178
+ if (extractedRules.length > 0) {
179
+ zones.behavioralRules = `ZONE 2: BEHAVIORAL RULES\n\n${extractedRules.join("\n")}`
180
+ } else {
181
+ zones.behavioralRules = zone2Str.trim()
182
+ }
183
+ } else {
184
+ zones.behavioralRules = zone2Str.trim()
185
+ }
186
+ }
187
+
188
+ const zone3Specific = raw.match(/(ZONE 3: PROJECT-SPECIFIC RULES.*?)$/s)
189
+ if (zone3Specific) zones.projectSpecific = zone3Specific[1].trim()
190
+
191
+ if (!zones.operationalFacts && !zones.behavioralRules && !zones.projectSpecific) {
192
+ zones.behavioralRules = raw.trim()
193
+ }
194
+
195
+ return zones
196
+ }
197
+
198
+ export async function stream(input: StreamRequest) {
199
+ const l = log
200
+ .clone()
201
+ .tag("providerID", input.model.providerID)
202
+ .tag("modelID", input.model.id)
203
+ .tag("sessionID", input.sessionID)
204
+ .tag("small", (input.small ?? false).toString())
205
+ .tag("agent", input.agent.name)
206
+ .tag("mode", input.agent.mode)
207
+ l.info("stream", {
208
+ modelID: input.model.id,
209
+ providerID: input.model.providerID,
210
+ })
211
+
212
+ // Update Session Metadata for Turn Tracking & Stagnation Detection
213
+ let meta = sessionMetadata.get(input.sessionID)
214
+ if (!meta) {
215
+ meta = { phaseTurnCount: 0, lastPhase: input.agent.name, consecutiveFailures: new Map() }
216
+ sessionMetadata.set(input.sessionID, meta)
217
+ }
218
+
219
+ if (meta.lastPhase !== input.agent.name) {
220
+ meta.phaseTurnCount = 0
221
+ meta.lastPhase = input.agent.name
222
+ }
223
+ meta.phaseTurnCount++
224
+
225
+ const [language, cfg, providerInfo, auth] = await Promise.all([
226
+ Provider.getLanguage(input.model),
227
+ Config.get(),
228
+ Provider.getProvider(input.model.providerID),
229
+ Auth.get(input.model.providerID),
230
+ ])
231
+ // TODO: move this to a proper hook
232
+ const isOpenaiOauth = providerInfo.id === "openai" && auth?.type === "oauth"
233
+
234
+ const payload: ZoneStructuredPayload = {
235
+ zone1_critical_rules: [`Current Phase: [${input.agent.name.toUpperCase()}].`],
236
+ zone2_context_files: [],
237
+ zone3_active_cursor: [],
238
+ zone4_guidelines: [],
239
+ }
240
+
241
+ try {
242
+ const fsNode = await import("fs/promises")
243
+ const pathNode = await import("path")
244
+ const lastUsedPath = pathNode.join(Instance.directory, ".spec_last_used")
245
+ const lastUsed = await fsNode.readFile(lastUsedPath, "utf-8").catch(() => null)
246
+ if (lastUsed) {
247
+ payload.zone1_critical_rules.push(`ACTIVE PROJECT CONTEXT:\n- Feature Path: ${lastUsed.trim()}\n- Note: Subagents MUST NOT run \`sc_init\` if an active feature path is already established. Use \`map\` tools to explore this path.`)
248
+ }
249
+ } catch (e) {
250
+ l.debug("Failed to fetch active project context for subagent", { error: String(e) })
251
+ }
252
+
253
+ if (input.operationalFacts && input.operationalFacts.length > 0) {
254
+ payload.zone1_critical_rules.push(...input.operationalFacts)
255
+ }
256
+
257
+ if (input.system?.zone1) {
258
+ payload.zone1_critical_rules.push(...input.system.zone1)
259
+ }
260
+
261
+ if (input.yolo) {
262
+ payload.zone1_critical_rules.push(
263
+ "CRITICAL: YOLO mode is active. You MUST execute tasks autonomously until the work is completely finished. " +
264
+ "You are NOT allowed to stop and ask for user input. " +
265
+ "When AND ONLY WHEN the entire job is done, you MUST call the 'task_complete' tool to terminate the session.",
266
+ )
267
+ }
268
+
269
+ payload.zone1_critical_rules.push(
270
+ "ENVIRONMENT CONTEXT: You are operating in a context-constrained environment. " +
271
+ "You MUST work strictly file-by-file. NEVER attempt to create or modify multiple files in a single response or generate massive code blocks at once. " +
272
+ "Wait for the tool result confirmation before proceeding to the next file.",
273
+ )
274
+
275
+ if (input.instructions && input.instructions.length > 0) {
276
+ payload.zone4_guidelines.push(...input.instructions)
277
+ }
278
+
279
+ // Phase Stagnation Detection (Task 3.2)
280
+ if (input.agent.name === "plan" && meta.phaseTurnCount >= 8) {
281
+ const nudge = `Supervisor Note: You have been in the [PLAN] phase for ${meta.phaseTurnCount} turns. If the implementation plan is complete and tasks are defined, you should run 'sc_approve' to transition to the [BUILD] phase.`
282
+ payload.zone1_critical_rules.push(nudge)
283
+ l.info("stagnation nudge", { turnCount: meta.phaseTurnCount })
284
+ }
285
+
286
+ if (input.agent.name === "build" && meta.phaseTurnCount >= 5) {
287
+ const nudge = `Supervisor Note: You have been in the [BUILD] phase for ${meta.phaseTurnCount} turns. You should proceed immediately to implementation using the 'write' or 'edit' tools to advance the project state.`
288
+ payload.zone1_critical_rules.push(nudge)
289
+ l.info("stagnation nudge", { turnCount: meta.phaseTurnCount, phase: "build" })
290
+ }
291
+
292
+ let activeRulePacks: string[] = ["core_interaction_pack"]
293
+ let thinkingEffort: "high" | "low" = "low"
294
+
295
+ // Phase 1: Intent Classification (Clerk / local-side) - Task 1.1
296
+ // The Clerk detects user intent and shifts the active epochcli Agent.
297
+ if (providerInfo.id === "local-main") {
298
+ try {
299
+ const sideModel = await Provider.getSideModel()
300
+ if (sideModel) {
301
+ const sideLanguage = await Provider.getLanguage(sideModel)
302
+
303
+ // Extract conversation tail for structural context (Task 1.1)
304
+ const tailCount = 10
305
+ const recentMessages = input.messages.slice(-tailCount)
306
+ const conversationTail = recentMessages
307
+ .map((m) => {
308
+ let content = ""
309
+ if (typeof m.content === "string") {
310
+ content = m.content
311
+ } else if (Array.isArray(m.content)) {
312
+ content = m.content
313
+ .map((c) => {
314
+ if (c.type === "text") return c.text
315
+ if (c.type === "tool-call") return `[Tool Call: ${c.toolName}]`
316
+ if (c.type === "tool-result") return `[Tool Result: ${c.toolName}]`
317
+ return `[${c.type}]`
318
+ })
319
+ .join(" ")
320
+ }
321
+ // Truncate individual message content to keep the transcript lean
322
+ const truncated = content.length > 300 ? content.slice(0, 250) + "... [truncated]" : content
323
+ return `${m.role.toUpperCase()}: ${truncated}`
324
+ })
325
+ .join("\n\n")
326
+
327
+ if (conversationTail) {
328
+ const { RuleRouter } = await import("./prompt/router")
329
+ const { Agent } = await import("@/agent/agent")
330
+
331
+ // Debug info for the Clerk Turn
332
+ const sideProvider = await Provider.getProvider(sideModel.providerID)
333
+ l.debug("clerk", {
334
+ message: `Using model: ${sideModel.providerID}/${sideModel.id} at ${sideProvider?.options?.baseURL}`,
335
+ })
336
+
337
+ let groundTruths = ""
338
+ let continuityReportContext = ""
339
+ try {
340
+ const fsNode = await import("fs/promises")
341
+ const pathNode = await import("path")
342
+ const rulesPath = pathNode.join(".history", "project_rules.toon")
343
+ const rulesContext = await fsNode.readFile(rulesPath, "utf-8").catch(() => "")
344
+ // Use an empty array for packs here since we just want operational facts for the Clerk
345
+ if (rulesContext) {
346
+ const parsedRules = parseGroundTruthRules(rulesContext, [], sideModel.limit.context)
347
+ if (parsedRules.operationalFacts) groundTruths = parsedRules.operationalFacts
348
+ }
349
+
350
+ // Load continuity report to help Clerk understand overarching state
351
+ try {
352
+ const continuityContent = await fsNode.readFile(".epoch-continuity.toon", "utf-8")
353
+ // Truncate if it's absurdly large, though toon files should be managed.
354
+ continuityReportContext =
355
+ continuityContent.length > 2000
356
+ ? continuityContent.slice(0, 2000) + "... [truncated]"
357
+ : continuityContent
358
+ } catch (e) {
359
+ // File might not exist yet (first epoch), ignore
360
+ }
361
+ } catch (e) {}
362
+
363
+ l.debug("clerk", { message: "Supervising conversation..." })
364
+
365
+ // Task 4.2: Arbitration Mechanism
366
+ let identifiedAgent: string | undefined
367
+
368
+ // Check for initial persona lock (Spec CLI One-Shot)
369
+ const firstUserMsg = input.messages.find((m) => m.role === "user")
370
+ let firstMsgText = ""
371
+ if (typeof firstUserMsg?.content === "string") {
372
+ firstMsgText = firstUserMsg.content
373
+ } else if (Array.isArray(firstUserMsg?.content)) {
374
+ firstMsgText = firstUserMsg.content
375
+ .filter((c) => c.type === "text")
376
+ .map((c) => c.text)
377
+ .join("\n")
378
+ }
379
+ const isOneShot =
380
+ firstMsgText.toLowerCase().includes("one-shot") || firstMsgText.toLowerCase().includes("spec cli")
381
+
382
+ // Check if planning is actually finished
383
+ let planningFinished = false
384
+ try {
385
+ const fsNode = await import("fs/promises")
386
+ const pathNode = await import("path")
387
+ // We don't know the feature name easily here without parsing,
388
+ // but we can look for any .spec-tasks-approved file in projects/active
389
+ const projectDir = "projects/active"
390
+ const entries = await fsNode.readdir(projectDir, { withFileTypes: true })
391
+ for (const entry of entries) {
392
+ if (entry.isDirectory()) {
393
+ const approvedFile = pathNode.join(projectDir, entry.name, ".spec-tasks-approved")
394
+ const exists = await fsNode
395
+ .access(approvedFile)
396
+ .then(() => true)
397
+ .catch(() => false)
398
+ if (exists) {
399
+ planningFinished = true
400
+ break
401
+ }
402
+ }
403
+ }
404
+ } catch (e) {}
405
+
406
+ // Check for recent objections to the supervisor
407
+ // Give the proxy a moment of silence between the end of the last turn and the start of supervision
408
+ await new Promise((r) => setTimeout(r, 1000))
409
+
410
+ // Concurrently identify agent, rule packs, and thinking effort
411
+ const [identifiedAgentResult, identifiedPacks, identifiedEffort] = await Promise.all([
412
+ (async () => {
413
+ if (isOneShot && !planningFinished) {
414
+ l.debug("clerk", { message: "One-Shot planning in progress. Locking persona to: plan" })
415
+ return "plan"
416
+ } else if (planningFinished) {
417
+ l.debug("clerk", { message: "Planning finished semaphore detected. Deterministic handover to: build" })
418
+ return "build"
419
+ } else {
420
+ return RuleRouter.identifyAgent(conversationTail, input.agent.name, continuityReportContext)
421
+ }
422
+ })(),
423
+ RuleRouter.identifyRulePacks(conversationTail, sideLanguage),
424
+ RuleRouter.identifyThinkingEffort(conversationTail, sideLanguage),
425
+ ])
426
+
427
+ identifiedAgent = identifiedAgentResult
428
+ activeRulePacks = identifiedPacks
429
+ thinkingEffort = identifiedEffort
430
+
431
+ l.debug("clerk", { message: `Identified agent: ${identifiedAgent}` })
432
+ l.debug("clerk", { message: `Identified rule packs: ${activeRulePacks.join(", ")}` })
433
+ l.debug("clerk", { message: `Identified thinking effort: ${thinkingEffort}` })
434
+
435
+ if (identifiedAgent !== input.agent.name) {
436
+ log.debug("Clerk identified agent shift", { from: input.agent.name, to: identifiedAgent })
437
+ const newAgent = await Agent.get(identifiedAgent)
438
+ if (newAgent) {
439
+ l.debug("clerk", { message: `SHIFTING agent to: ${identifiedAgent}` })
440
+ input.agent = newAgent
441
+ // Update the directive in Zone 1
442
+ payload.zone1_critical_rules[0] = `Current Phase: [${input.agent.name.toUpperCase()}].`
443
+ }
444
+ }
445
+ }
446
+ }
447
+ } catch (e) {
448
+ l.error("clerk", { message: "Intent classification failed", error: String(e) })
449
+ log.warn("Clerk intent classification failed", { error: String(e) })
450
+ }
451
+ }
452
+
453
+ if (providerInfo.id === "local-main") {
454
+ try {
455
+ let rulesContext = ""
456
+ const mcpClientsRecord = await MCP.clients()
457
+ const mcpClients = Object.values(mcpClientsRecord) as any[]
458
+ const gtCli = mcpClients.find((c: any) => c.id === "ground")
459
+ if (gtCli) {
460
+ log.debug("Fetching ground truth rules")
461
+ const gtRes = await gtCli.client.callTool({ name: "gt_status", arguments: {} })
462
+ if (gtRes.content && gtRes.content.length > 0 && gtRes.content[0].type === "text") {
463
+ rulesContext = gtRes.content[0].text
464
+ }
465
+ } else {
466
+ // Fallback rule load
467
+ try {
468
+ const fsNode = await import("fs/promises")
469
+ const pathNode = await import("path")
470
+ rulesContext = await fsNode.readFile(pathNode.join(".history", "project_rules.toon"), "utf-8")
471
+ } catch (e) {
472
+ // ignore missing file
473
+ }
474
+ }
475
+
476
+ if (rulesContext) {
477
+ const parsedRules = parseGroundTruthRules(rulesContext, activeRulePacks, input.model.limit.context)
478
+ if (parsedRules.operationalFacts) {
479
+ payload.zone1_critical_rules.push(parsedRules.operationalFacts)
480
+ }
481
+ if (parsedRules.behavioralRules) payload.zone2_context_files.push(parsedRules.behavioralRules)
482
+ if (parsedRules.projectSpecific) payload.zone3_active_cursor.push(parsedRules.projectSpecific)
483
+ }
484
+ } catch (e) {
485
+ log.warn("Phase 1 Pre-Generation Rules Context fetch failed", { error: String(e) })
486
+ }
487
+ }
488
+
489
+ payload.zone2_context_files.push(
490
+ [
491
+ // use agent prompt otherwise provider prompt
492
+ ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model, input.isContinue)),
493
+ // any custom prompt passed into this call
494
+ ...(input.system?.zone2 ?? []),
495
+ // any custom prompt from last user message
496
+ ...(input.user.system ? [input.user.system] : []),
497
+ ]
498
+ .filter((x) => x)
499
+ .join("\n\n"),
500
+ )
501
+
502
+ if (input.user.cursorContext) {
503
+ const { file, line, code } = input.user.cursorContext
504
+ payload.zone3_active_cursor.push(
505
+ `Active Cursor Context:\n File: ${file}\n Line ${line}: ${code} # <--- CURSOR HERE`,
506
+ )
507
+ }
508
+
509
+ const system: string[] = [PromptBuilder.build(payload)]
510
+ const header = system[0]
511
+ await Plugin.trigger(
512
+ "experimental.chat.system.transform",
513
+ { sessionID: input.sessionID, model: input.model },
514
+ { system },
515
+ )
516
+ // rejoin to maintain 2-part structure for caching if header unchanged
517
+ if (system.length > 2 && system[0] === header) {
518
+ const rest = system.slice(1)
519
+ system.length = 0
520
+ system.push(header, rest.join("\n"))
521
+ }
522
+
523
+ const isWorkflow = language instanceof GitLabWorkflowLanguageModel
524
+ const isSmallReasoningModel =
525
+ input.model.api?.id?.includes("gemma-4") ||
526
+ input.model.api?.id?.includes("google-gemma-26b") ||
527
+ input.model.api?.id?.toLowerCase().includes("qwen") ||
528
+ input.model.id?.includes("big-pickle")
529
+ const isReasoningModel = input.model.capabilities.reasoning || isSmallReasoningModel
530
+
531
+ const variant =
532
+ !input.small && input.model.variants && input.user.model.variant
533
+ ? input.model.variants[input.user.model.variant]
534
+ : {}
535
+ const base = input.small
536
+ ? ProviderTransform.smallOptions(input.model)
537
+ : ProviderTransform.options({
538
+ model: input.model,
539
+ sessionID: input.sessionID,
540
+ providerOptions: providerInfo.options,
541
+ thinkingEffort: isReasoningModel ? thinkingEffort : undefined,
542
+ })
543
+ const options: Record<string, any> = pipe(
544
+ base,
545
+ mergeDeep(input.model.options),
546
+ mergeDeep(input.agent.options),
547
+ mergeDeep(variant),
548
+ )
549
+ if (input.operationalFacts && input.operationalFacts.length > 0) {
550
+ options.operationalFacts = input.operationalFacts
551
+ }
552
+ if (isOpenaiOauth) {
553
+ options.instructions = system.join("\n")
554
+ }
555
+
556
+ // Phase 1: Context Fetching skipped (agent uses mcpx tool directly)
557
+ if (false) {
558
+ try {
559
+ let mcpContext = ""
560
+ const mcpxTool = input.tools["mcpx"]
561
+
562
+ let activePath = "."
563
+
564
+ if (mcpxTool) {
565
+ try {
566
+ log.debug("Fetching current state from spec via mcpx")
567
+ const statusRes = await mcpxTool.execute!({ server: "spec", tool: "sc_status", flags: {} }, options as any)
568
+ if (statusRes.content && statusRes.content.length > 0 && statusRes.content[0].type === "text") {
569
+ const text = statusRes.content[0].text
570
+ const featureMatch = text.match(/Feature: (.+)/)
571
+ if (featureMatch) {
572
+ activePath = featureMatch[1].trim()
573
+ }
574
+ mcpContext += `Spec CLI Context:\n${ToonEncoder.encode({ active_feature: activePath, status: text })}\n`
575
+ }
576
+ } catch (e) {
577
+ log.debug("Failed to fetch spec status via mcpx", { error: String(e) })
578
+ }
579
+
580
+ try {
581
+ log.debug("Fetching localized map from map for path via mcpx", { activePath })
582
+ const mapRes = await mcpxTool.execute!(
583
+ { server: "map", tool: "pm_query", flags: { path: activePath } },
584
+ options as any,
585
+ )
586
+ if (mapRes.content && mapRes.content.length > 0 && mapRes.content[0].type === "text") {
587
+ mcpContext += `Project Map Context:\n${ToonEncoder.encode({ localized_map: mapRes.content[0].text })}\n`
588
+ }
589
+ } catch (e) {
590
+ log.debug("Failed to fetch map localized map via mcpx", { error: String(e) })
591
+ }
592
+ }
593
+
594
+ if (mcpContext) {
595
+ payload.zone1_critical_rules.push(mcpContext)
596
+ }
597
+ } catch (e) {
598
+ log.warn("Phase 1 Pre-Generation MCP Context fetch failed", { error: String(e) })
599
+ }
600
+ }
601
+
602
+ // Positional Prompt Architecture: Assemble Zone-based messages
603
+ const initialMessages = PromptBuilder.buildMessages(payload, {
604
+ isSmallReasoningModel,
605
+ thinkingEffort: isReasoningModel ? thinkingEffort : undefined,
606
+ })
607
+ const systemContent = initialMessages
608
+ .filter((m) => m.role === "system")
609
+ .map((m) => m.content)
610
+ .join("\n\n")
611
+ const otherInitial = initialMessages.filter((m) => m.role !== "system")
612
+
613
+ const internalStateCheck = `
614
+ <|channel>thought
615
+ [INTERNAL STATE CHECK]
616
+ - Mode: ${thinkingEffort.toUpperCase()} thinking / Adaptive efficiency active.
617
+ - Role: Assigned to [${input.agent.name.toUpperCase()}].
618
+ - Constraint: ${Math.round(input.model.limit.context / 1000)}K token budget. Concise CoT.
619
+ Ready to process user request strictly under these parameters.
620
+ `.trim()
621
+
622
+ // Truncate older proactive validation errors to prevent streaming loops and context bloat
623
+ let validationErrorCount = 0
624
+ for (let i = input.messages.length - 1; i >= 0; i--) {
625
+ const msg = input.messages[i]
626
+ if (msg.role === "tool" && Array.isArray(msg.content)) {
627
+ for (let j = 0; j < msg.content.length; j++) {
628
+ const part: any = msg.content[j]
629
+ if (
630
+ part.type === "tool-result" &&
631
+ part.isError &&
632
+ typeof part.result === "string" &&
633
+ part.result.includes("INVALID ARGUMENTS:")
634
+ ) {
635
+ validationErrorCount++
636
+ if (validationErrorCount > 2) {
637
+ part.result = "INVALID ARGUMENTS: [TRUNCATED - Refer to most recent validation error]"
638
+ }
639
+ }
640
+ }
641
+ }
642
+ }
643
+
644
+ const messages = isOpenaiOauth
645
+ ? input.messages
646
+ : isWorkflow
647
+ ? input.messages
648
+ : mergeMessages([...otherInitial, ...input.messages, { role: "user" as const, content: internalStateCheck }])
649
+
650
+ const params = await Plugin.trigger(
651
+ "chat.params",
652
+ {
653
+ sessionID: input.sessionID,
654
+ agent: input.agent.name,
655
+ model: input.model,
656
+ provider: providerInfo,
657
+ message: input.user,
658
+ },
659
+ {
660
+ temperature: input.model.capabilities.temperature
661
+ ? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
662
+ : undefined,
663
+ topP: input.agent.topP ?? ProviderTransform.topP(input.model),
664
+ topK: ProviderTransform.topK(input.model),
665
+ maxOutputTokens: ProviderTransform.maxOutputTokens(input.model),
666
+ options,
667
+ },
668
+ )
669
+
670
+ const { headers } = await Plugin.trigger(
671
+ "chat.headers",
672
+ {
673
+ sessionID: input.sessionID,
674
+ agent: input.agent.name,
675
+ model: input.model,
676
+ provider: providerInfo,
677
+ message: input.user,
678
+ },
679
+ {
680
+ headers: {},
681
+ },
682
+ )
683
+
684
+ let resolvedTools = resolveTools(input)
685
+
686
+ try {
687
+ const availableSkills = await (await import("../skill")).Skill.available(input.agent)
688
+ if (availableSkills.length === 0) {
689
+ const { skill, ...rest } = resolvedTools as any
690
+ resolvedTools = rest as any
691
+ }
692
+ } catch (e) {}
693
+
694
+ const tools = Record.map(resolvedTools, (toolDef, toolName) => {
695
+ if (!toolDef.execute) return toolDef
696
+ const originalExecute = toolDef.execute
697
+ return {
698
+ ...toolDef,
699
+ execute: async (args: any, options: any) => {
700
+ const interception = await interceptToolLoop({
701
+ toolName,
702
+ args,
703
+ messages: options.messages ?? input.messages,
704
+ provider: providerInfo,
705
+ cfg,
706
+ })
707
+ if (interception) return interception
708
+
709
+ let result
710
+ let isError = false
711
+ try {
712
+ result = await originalExecute(args, options)
713
+ } catch (e: any) {
714
+ result = e
715
+ isError = true
716
+ }
717
+
718
+ const resultStr =
719
+ typeof result === "string"
720
+ ? result
721
+ : result instanceof Error
722
+ ? String(result.message || result)
723
+ : JSON.stringify(result)
724
+
725
+ // Task 1.1: Auto-Fallback for sc_guidance prerequisite
726
+ if (
727
+ resultStr.includes("You must run `spec sc_guidance`") ||
728
+ resultStr.includes("You must run \\`spec sc_guidance\\`")
729
+ ) {
730
+ log.info("Auto-fallback triggered for missing prerequisite sc_guidance")
731
+ let guidanceOutput = ""
732
+ try {
733
+ const allTools = resolveTools(input)
734
+ const guidanceToolKey = Object.keys(allTools).find((k) => k.includes("sc_guidance"))
735
+ if (guidanceToolKey && allTools[guidanceToolKey] && allTools[guidanceToolKey].execute) {
736
+ const guidanceResult = await allTools[guidanceToolKey].execute!({}, options)
737
+ guidanceOutput = typeof guidanceResult === "string" ? guidanceResult : JSON.stringify(guidanceResult)
738
+ } else if (allTools["mcpx"] && allTools["mcpx"].execute) {
739
+ const guidanceResult = await allTools["mcpx"].execute!(
740
+ { server: "spec", tool: "sc_guidance", flags: {} },
741
+ options,
742
+ )
743
+ guidanceOutput = typeof guidanceResult === "string" ? guidanceResult : JSON.stringify(guidanceResult)
744
+ }
745
+ } catch (fallbackError) {
746
+ guidanceOutput = "Failed to auto-execute sc_guidance: " + String(fallbackError)
747
+ }
748
+
749
+ const hybridResponse =
750
+ "System overriding sc_approve. Prerequisite missing. Auto-executing sc_guidance. Here is the guidance you must review... Read this, then you may call sc_approve.\n\n" +
751
+ guidanceOutput
752
+ if (isError) {
753
+ return hybridResponse
754
+ } else {
755
+ return hybridResponse
756
+ }
757
+ }
758
+
759
+ // Task 2.1 & 2.2: Clerk Interceptor (Middleware) for generic prerequisite errors
760
+ if (isError && (resultStr.includes("You must run") || resultStr.includes("prerequisite"))) {
761
+ if (providerInfo.id === "local-main") {
762
+ try {
763
+ log.info("Triggering Clerk Interceptor for prerequisite error")
764
+ const sideModel = await Provider.getSideModel()
765
+ if (sideModel) {
766
+ const sideLanguage = await Provider.getLanguage(sideModel)
767
+
768
+ // Build history string
769
+ const tailCount = 5
770
+ const recentMessages = (options.messages ?? input.messages).slice(-tailCount)
771
+ const historyStr = recentMessages
772
+ .map(
773
+ (m: any) =>
774
+ `${m.role}: ${typeof m.content === "string" ? m.content : JSON.stringify(m.content).slice(0, 200)}`,
775
+ )
776
+ .join("\n")
777
+
778
+ const systemPrompt =
779
+ "Look at the last tool error and the chat history. Write a concise, commanding one-sentence instruction telling the main agent exactly which tool to use next to resolve the prerequisite."
780
+ const prompt = `History:\n${historyStr}\n\nTool Error:\n${resultStr}\n\nDirective:`
781
+
782
+ const { generateText } = await import("@/util/ai-sdk")
783
+ const res = await generateText({
784
+ model: sideLanguage,
785
+ system: systemPrompt,
786
+ prompt: prompt,
787
+ abortSignal: AbortSignal.timeout(10000),
788
+ maxRetries: 0,
789
+ })
790
+
791
+ const clerkText = res.text.trim()
792
+ if (clerkText) {
793
+ log.info("Clerk interceptor generated directive", { directive: clerkText })
794
+ throw new Error(`CRITICAL SYSTEM DIRECTIVE: ${clerkText}\n\nOriginal Error:\n${resultStr}`)
795
+ }
796
+ }
797
+ } catch (clerkErr) {
798
+ log.warn("Clerk interceptor failed", { error: String(clerkErr) })
799
+ }
800
+ }
801
+ }
802
+
803
+ if (isError) {
804
+ // Task: Reactive Help Injection for mcpx syntax errors
805
+ if (toolName === "mcpx" && args.server && args.tool) {
806
+ try {
807
+ const resultText = typeof result === "string" ? result : (result?.message || JSON.stringify(result))
808
+ if (resultText.includes("Error (Exit") || resultText.includes("missing field") || resultText.includes("invalid type")) {
809
+ log.info("mcpx syntax error detected, fetching help output", { server: args.server, tool: args.tool })
810
+ const allTools = resolveTools(input)
811
+ if (allTools["mcpx"] && allTools["mcpx"].execute) {
812
+ const helpResult = await allTools["mcpx"].execute!({
813
+ server: args.server,
814
+ tool: args.tool,
815
+ args: ["--help"]
816
+ }, options)
817
+
818
+ const helpOutput = typeof helpResult === "string" ? helpResult : (helpResult?.output ?? JSON.stringify(helpResult))
819
+
820
+ let guidance = "The command failed with a syntax error. Review the correct schema below and retry with fixed arguments."
821
+ if (resultText.includes("missing field title") && args.server === "spec") {
822
+ guidance = "The 'spec' tool failed because Tasks.json is invalid. Specifically, it is missing the 'title' field in one or more task objects. Review your Tasks.json file, ensure 'title' is used instead of (or in addition to) 'description', and retry."
823
+ } else if (resultText.includes("missing field id") && args.server === "spec" && args.tool === "sc_todo_start") {
824
+ guidance = "The 'sc_todo_start' tool requires an '--id' flag. Additionally, ensure that the ID you are passing exactly matches the ID in Tasks.json, and that all IDs in Tasks.json follow a numeric-style format (e.g., '1', '1.1')."
825
+ }
826
+
827
+ const augmentedError = `${resultText}\n\n[SYSTEM GUIDANCE: ${guidance}]\n\n${helpOutput}`
828
+
829
+ if (result instanceof Error) {
830
+ result.message = augmentedError
831
+ } else if (typeof result === "object") {
832
+ result.output = augmentedError
833
+ } else {
834
+ result = augmentedError
835
+ }
836
+ }
837
+ }
838
+ } catch (helpErr) {
839
+ log.warn("Failed to fetch reactive help for mcpx", { error: String(helpErr) })
840
+ }
841
+ }
842
+ throw result
843
+ }
844
+ return result
845
+ },
846
+ }
847
+ })
848
+
849
+ // LiteLLM and some Anthropic proxies require the tools parameter to be present
850
+ // when message history contains tool calls, even if no tools are being used.
851
+ // Add a dummy tool that is never called to satisfy this validation.
852
+ // This is enabled for:
853
+ // 1. Providers with "litellm" in their ID or API ID (auto-detected)
854
+ // 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways)
855
+ const isLiteLLMProxy =
856
+ providerInfo.options?.["litellmProxy"] === true ||
857
+ input.model.providerID.toLowerCase().includes("litellm") ||
858
+ input.model.api.id.toLowerCase().includes("litellm")
859
+
860
+ // LiteLLM/Bedrock rejects requests where the message history contains tool
861
+ // calls but no tools param is present. When there are no active tools (e.g.
862
+ // during compaction), inject a stub tool to satisfy the validation requirement.
863
+ // The stub description explicitly tells the model not to call it.
864
+ if (isLiteLLMProxy && Object.keys(tools).length === 0 && hasToolCalls(input.messages)) {
865
+ tools["_noop"] = tool({
866
+ description: "Do not call this tool. It exists only for API compatibility and must never be invoked.",
867
+ inputSchema: jsonSchema({
868
+ type: "object",
869
+ properties: {
870
+ reason: { type: "string", description: "Unused" },
871
+ },
872
+ }),
873
+ execute: async () => ({ output: "", title: "", metadata: {} }),
874
+ })
875
+ }
876
+
877
+ // Wire up toolExecutor for DWS workflow models so that tool calls
878
+ // from the workflow service are executed via epochcli's tool system
879
+ // and results sent back over the WebSocket.
880
+ if (language instanceof GitLabWorkflowLanguageModel) {
881
+ const workflowModel = language
882
+ workflowModel.systemPrompt = system.join("\n")
883
+ workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => {
884
+ const t = tools[toolName]
885
+ if (!t || !t.execute) {
886
+ return { result: "", error: `Unknown tool: ${toolName}` }
887
+ }
888
+ try {
889
+ const result = await t.execute!(JSON.parse(argsJson), {
890
+ toolCallId: _requestID,
891
+ messages: input.messages,
892
+ abortSignal: input.abort,
893
+ })
894
+ const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result))
895
+ return {
896
+ result: output,
897
+ metadata: typeof result === "object" ? result?.metadata : undefined,
898
+ title: typeof result === "object" ? result?.title : undefined,
899
+ }
900
+ } catch (e: any) {
901
+ return { result: "", error: e.message ?? String(e) }
902
+ }
903
+ }
904
+ }
905
+
906
+ if (providerInfo.id.startsWith("local-")) {
907
+ await new Promise((r) => setTimeout(r, 2000))
908
+ }
909
+
910
+ return streamText({
911
+ system: systemContent,
912
+ onFinish(event) {
913
+ // Worker is now handled sequentially in engine.ts
914
+ },
915
+ onError(error) {
916
+ l.error("stream error", {
917
+ error,
918
+ })
919
+ },
920
+ async experimental_repairToolCall(failed) {
921
+ // Phase 2 OutputInterceptor: Catch broken JSON from local-main and use local-side to fix it.
922
+ if (providerInfo.id === "local-main") {
923
+ try {
924
+ const sideProviderConfig = cfg.provider?.["local-side"]
925
+ if (sideProviderConfig) {
926
+ log.debug("Attempting to repair broken JSON tool call with local-side Clerk")
927
+ const sideModel = await Provider.getSideModel()
928
+ if (!sideModel) return failed.toolCall
929
+ const sideLanguage = await Provider.getLanguage(sideModel)
930
+ const repairResponse = await generateText({
931
+ model: sideLanguage,
932
+ system:
933
+ "You are a JSON repair utility. The user will provide a broken JSON tool call. Your ONLY job is to output the repaired, valid JSON object that matches the intended schema. DO NOT output any markdown, explanations, or other text. ONLY the valid JSON object.",
934
+ prompt: `Broken JSON: ${(failed.toolCall as any).args}\n\nError: ${failed.error.message}`,
935
+ abortSignal: AbortSignal.timeout(15000),
936
+ maxRetries: 0,
937
+ })
938
+ try {
939
+ const repairedArgs = JSON.parse(repairResponse.text.trim())
940
+ log.info("Successfully repaired JSON with local-side")
941
+
942
+ const repairEvent: Log.EnhancedModelExecutionEvent = {
943
+ timestamp: Date.now(),
944
+ mainEpochId: input.sessionID,
945
+ event: "END_GENERATE",
946
+ providerId: "local-side",
947
+ phase: "Phase 2",
948
+ metrics: { json_repaired: true },
949
+ }
950
+ l.info("json repair", repairEvent)
951
+
952
+ return {
953
+ ...failed.toolCall,
954
+ args: repairedArgs,
955
+ }
956
+ } catch (parseErr) {
957
+ log.warn("local-side failed to output valid JSON for repair")
958
+ }
959
+ }
960
+ } catch (e) {
961
+ log.error("Failed during local-side JSON repair attempt", { error: String(e) })
962
+ }
963
+ }
964
+
965
+ const lower = failed.toolCall.toolName.toLowerCase()
966
+ if (lower !== failed.toolCall.toolName && tools[lower]) {
967
+ l.info("repairing tool call", {
968
+ tool: failed.toolCall.toolName,
969
+ repaired: lower,
970
+ })
971
+ return {
972
+ ...failed.toolCall,
973
+ toolName: lower,
974
+ }
975
+ }
976
+ throw new Error(`Tool call failed: ${failed.error.message}`)
977
+ },
978
+ temperature: params.temperature,
979
+ topP: params.topP,
980
+ topK: params.topK,
981
+ providerOptions: ProviderTransform.providerOptions(input.model, params.options),
982
+ activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
983
+ tools,
984
+ toolChoice: input.toolChoice,
985
+ maxOutputTokens: params.maxOutputTokens,
986
+ abortSignal: input.abort,
987
+ headers: {
988
+ ...(input.model.providerID.startsWith("epochcli")
989
+ ? {
990
+ "x-epochcli-project": Instance.project.id,
991
+ "x-epochcli-session": input.sessionID,
992
+ "x-epochcli-request": input.user.id,
993
+ "x-epochcli-client": Flag.EPOCHCLI_CLIENT,
994
+ }
995
+ : {
996
+ "x-session-affinity": input.sessionID,
997
+ ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
998
+ "User-Agent": `epochcli/${Installation.VERSION}`,
999
+ }),
1000
+ ...input.model.headers,
1001
+ ...headers,
1002
+ },
1003
+ maxRetries: input.retries ?? 0,
1004
+ messages,
1005
+ model: wrapLanguageModel({
1006
+ model: language,
1007
+ middleware: [
1008
+ {
1009
+ specificationVersion: "v3" as const,
1010
+ async transformParams(args) {
1011
+ if (args.type === "stream" || args.type === "generate") {
1012
+ const targetMessages = ProviderTransform.message(
1013
+ args.params.prompt as ModelMessage[],
1014
+ input.model,
1015
+ options,
1016
+ )
1017
+ const targetKey = ProviderTransform.sdkKey(input.model.api.npm) ?? input.model.providerID
1018
+ const isAzure = input.model.api.npm === "@ai-sdk/azure"
1019
+ args.params.prompt = SanitizerMiddleware.stripProviderOptions(targetMessages, targetKey, isAzure)
1020
+ }
1021
+ return args.params
1022
+ },
1023
+ },
1024
+ // Telemetry middleware
1025
+ {
1026
+ specificationVersion: "v3" as const,
1027
+ wrapGenerate: async ({ doGenerate, params }) => {
1028
+ const startTime = Date.now()
1029
+ const truncatedPayload = Log.truncatePayload(params.prompt)
1030
+ const mainEpochId = input.sessionID
1031
+ const phase = input.model.providerID.includes("local-side") ? "Phase 1/3" : "Phase 2"
1032
+
1033
+ const startEvent: Log.EnhancedModelExecutionEvent = {
1034
+ timestamp: startTime,
1035
+ mainEpochId,
1036
+ event: "START_GENERATE",
1037
+ providerId: input.model.providerID,
1038
+ phase,
1039
+ activeAgent: input.agent.name,
1040
+ contextLimit: input.model.limit.context,
1041
+ toolCount: Object.keys(tools).length,
1042
+ payload: truncatedPayload,
1043
+ tools: Flag.EPOCHCLI_DEBUG_FULL_PROMPT ? tools : undefined,
1044
+ }
1045
+ l.debug("model execution start", startEvent)
1046
+ SessionTelemetry.emitModelEvent(startEvent)
1047
+
1048
+ try {
1049
+ const res = await doGenerate()
1050
+ const endTime = Date.now()
1051
+
1052
+ const endEvent: Log.EnhancedModelExecutionEvent = {
1053
+ timestamp: endTime,
1054
+ mainEpochId,
1055
+ event: "END_GENERATE",
1056
+ providerId: input.model.providerID,
1057
+ phase,
1058
+ activeAgent: input.agent.name,
1059
+ toolCount: Object.keys(tools).length,
1060
+ metrics: {
1061
+ ttftMs: endTime - startTime,
1062
+ promptTokens: (res.usage as any)?.promptTokens,
1063
+ completionTokens: (res.usage as any)?.completionTokens,
1064
+ tps: (res.usage as any)?.completionTokens
1065
+ ? (res.usage as any).completionTokens / ((endTime - startTime) / 1000)
1066
+ : undefined,
1067
+ },
1068
+ }
1069
+ SessionTelemetry.emitModelEvent(endEvent)
1070
+
1071
+ l.debug("model execution end", endEvent)
1072
+ return res
1073
+ } catch (e) {
1074
+ const errorEvent: Log.EnhancedModelExecutionEvent = {
1075
+ timestamp: Date.now(),
1076
+ mainEpochId,
1077
+ event: "ERROR",
1078
+ providerId: input.model.providerID,
1079
+ phase,
1080
+ payload: { error: String(e) },
1081
+ }
1082
+ l.error("model execution error", errorEvent)
1083
+ throw e
1084
+ }
1085
+ },
1086
+ wrapStream: async ({ doStream, params }) => {
1087
+ const startTime = Date.now()
1088
+ const truncatedPayload = Log.truncatePayload(params.prompt)
1089
+ const mainEpochId = input.sessionID
1090
+ const phase = input.model.providerID.includes("local-side") ? "Phase 1/3" : "Phase 2"
1091
+
1092
+ const startEvent: Log.EnhancedModelExecutionEvent = {
1093
+ timestamp: startTime,
1094
+ mainEpochId,
1095
+ event: "START_GENERATE",
1096
+ providerId: input.model.providerID,
1097
+ phase,
1098
+ activeAgent: input.agent.name,
1099
+ contextLimit: input.model.limit.context,
1100
+ toolCount: Object.keys(tools).length,
1101
+ payload: truncatedPayload,
1102
+ tools: Flag.EPOCHCLI_DEBUG_FULL_PROMPT ? tools : undefined,
1103
+ }
1104
+ l.debug("model execution start", startEvent)
1105
+ SessionTelemetry.emitModelEvent(startEvent)
1106
+
1107
+ try {
1108
+ const { stream, ...rest } = await doStream()
1109
+ let firstTokenTime: number | undefined
1110
+ let tokenCount = 0
1111
+ const monitor = new StreamingMonitor()
1112
+
1113
+ const iterator = (async function* () {
1114
+ for await (const chunk of stream as any) {
1115
+ const textDelta = chunk.textDelta ?? chunk.delta
1116
+ if (!firstTokenTime && chunk.type === "text-delta" && textDelta) {
1117
+ firstTokenTime = Date.now()
1118
+ }
1119
+ if (chunk.type === "text-delta" || chunk.type === "tool-call-delta") {
1120
+ tokenCount++
1121
+ }
1122
+
1123
+ if (chunk.type === "text-delta" && typeof textDelta === "string") {
1124
+ if (monitor.push(textDelta)) {
1125
+ const offendingText = monitor.getOffendingText()
1126
+ l.warn("Streaming loop detected, aborting...", { offendingText })
1127
+
1128
+ // Signal abortion to the provider if possible (though we only have the signal)
1129
+ try {
1130
+ ;(input.abort as any).dispatchEvent?.(new Event("abort"))
1131
+ } catch (e) {}
1132
+
1133
+ yield {
1134
+ type: "text-delta",
1135
+ textDelta: `\n\n[SYSTEM INTERVENTION: Thinking loop detected. Offending sequence: "${offendingText}". Generation aborted.]`,
1136
+ delta: `\n\n[SYSTEM INTERVENTION: Thinking loop detected. Offending sequence: "${offendingText}". Generation aborted.]`,
1137
+ } as any
1138
+ return
1139
+ }
1140
+ }
1141
+
1142
+ yield chunk
1143
+ }
1144
+ })()
1145
+
1146
+ const readableStream = new ReadableStream({
1147
+ async pull(controller) {
1148
+ try {
1149
+ const { value, done } = await iterator.next()
1150
+ if (done) {
1151
+ const endTime = Date.now()
1152
+ const endEvent: Log.EnhancedModelExecutionEvent = {
1153
+ timestamp: endTime,
1154
+ mainEpochId,
1155
+ event: "END_GENERATE",
1156
+ providerId: input.model.providerID,
1157
+ phase,
1158
+ activeAgent: input.agent.name,
1159
+ toolCount: Object.keys(tools).length,
1160
+ metrics: {
1161
+ ttftMs: firstTokenTime ? firstTokenTime - startTime : undefined,
1162
+ tps:
1163
+ tokenCount && firstTokenTime
1164
+ ? tokenCount / ((endTime - firstTokenTime) / 1000)
1165
+ : undefined,
1166
+ promptTokens: (await (rest as any).usage)?.promptTokens,
1167
+ completionTokens: (await (rest as any).usage)?.completionTokens,
1168
+ loop_detected: monitor.getOffendingText() !== undefined,
1169
+ },
1170
+ }
1171
+ l.debug("model execution end", endEvent)
1172
+ controller.close()
1173
+ } else {
1174
+ controller.enqueue(value)
1175
+ }
1176
+ } catch (e) {
1177
+ const errorEvent: Log.EnhancedModelExecutionEvent = {
1178
+ timestamp: Date.now(),
1179
+ mainEpochId,
1180
+ event: "ERROR",
1181
+ providerId: input.model.providerID,
1182
+ phase,
1183
+ payload: { error: String(e) },
1184
+ }
1185
+ l.error("model execution error", errorEvent)
1186
+ controller.error(e)
1187
+ }
1188
+ },
1189
+ cancel(reason) {
1190
+ iterator.return?.(reason)
1191
+ },
1192
+ })
1193
+
1194
+ return { stream: readableStream, ...rest }
1195
+ } catch (e) {
1196
+ const errorEvent: Log.EnhancedModelExecutionEvent = {
1197
+ timestamp: Date.now(),
1198
+ mainEpochId,
1199
+ event: "ERROR",
1200
+ providerId: input.model.providerID,
1201
+ phase,
1202
+ payload: { error: String(e) },
1203
+ }
1204
+ l.error("model execution error", errorEvent)
1205
+ throw e
1206
+ }
1207
+ },
1208
+ },
1209
+ // NativeTokenParser middleware
1210
+ {
1211
+ specificationVersion: "v3" as const,
1212
+ wrapGenerate: async ({ doGenerate, params }) => {
1213
+ const res = await doGenerate()
1214
+ // Replace <|"> with markdown backticks
1215
+ ;(res as any).text = (res as any).text?.replace(/<\|">/g, "```")
1216
+ return res
1217
+ },
1218
+ wrapStream: async ({ doStream, params }) => {
1219
+ const { stream, ...rest } = await doStream()
1220
+
1221
+ const iterator = (async function* () {
1222
+ for await (const chunk of stream as any) {
1223
+ if (chunk.type === "text-delta") {
1224
+ if (typeof chunk.textDelta === "string") {
1225
+ chunk.textDelta = chunk.textDelta.replace(/<\|">/g, "```")
1226
+ }
1227
+ if (typeof chunk.delta === "string") {
1228
+ chunk.delta = chunk.delta.replace(/<\|">/g, "```")
1229
+ }
1230
+ }
1231
+ yield chunk
1232
+ }
1233
+ })()
1234
+
1235
+ const readableStream = new ReadableStream({
1236
+ async pull(controller) {
1237
+ const { value, done } = await iterator.next()
1238
+ if (done) {
1239
+ controller.close()
1240
+ } else {
1241
+ controller.enqueue(value)
1242
+ }
1243
+ },
1244
+ cancel(reason) {
1245
+ iterator.return?.(reason)
1246
+ },
1247
+ })
1248
+
1249
+ return { stream: readableStream, ...rest }
1250
+ },
1251
+ },
1252
+ ],
1253
+ }),
1254
+ experimental_telemetry: {
1255
+ isEnabled: cfg.experimental?.openTelemetry,
1256
+ metadata: {
1257
+ userId: cfg.username ?? "unknown",
1258
+ sessionId: input.sessionID,
1259
+ },
1260
+ },
1261
+ })
1262
+ }
1263
+
1264
+ function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
1265
+ const disabled = Permission.disabled(
1266
+ Object.keys(input.tools),
1267
+ Permission.merge(input.agent.permission, input.permission ?? []),
1268
+ )
1269
+ return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
1270
+ }
1271
+
1272
+ function mergeMessages(messages: ModelMessage[]): ModelMessage[] {
1273
+ const result: ModelMessage[] = []
1274
+ for (const msg of messages) {
1275
+ const last = result[result.length - 1]
1276
+ if (last && last.role === msg.role) {
1277
+ if (typeof last.content === "string" && typeof msg.content === "string") {
1278
+ last.content += "\n\n" + msg.content
1279
+ continue
1280
+ }
1281
+ if (Array.isArray(last.content) && Array.isArray(msg.content)) {
1282
+ ;(last.content as any[]).push(...msg.content)
1283
+ continue
1284
+ }
1285
+ }
1286
+ result.push(msg)
1287
+ }
1288
+ return result
1289
+ }
1290
+
1291
+ // Check if messages contain any tool-call content
1292
+ // Used to determine if a dummy tool should be added for LiteLLM proxy compatibility
1293
+ export function hasToolCalls(messages: ModelMessage[]): boolean {
1294
+ for (const msg of messages) {
1295
+ if (!Array.isArray(msg.content)) continue
1296
+ for (const part of msg.content) {
1297
+ if (part.type === "tool-call" || part.type === "tool-result") return true
1298
+ }
1299
+ }
1300
+ return false
1301
+ }
1302
+
1303
+ /**
1304
+ * Normalizes mcpx tool arguments (pos/flags) into a single object for validation.
1305
+ */
1306
+ export function normalizeMcpxArguments(args: any): Record<string, any> {
1307
+ const normalized: Record<string, any> = {}
1308
+
1309
+ // Extract from flags
1310
+ if (args.flags && typeof args.flags === "object") {
1311
+ Object.assign(normalized, args.flags)
1312
+ }
1313
+
1314
+ // Extract from args (heuristic for common patterns)
1315
+ if (Array.isArray(args.args)) {
1316
+ for (let i = 0; i < args.args.length; i++) {
1317
+ const arg = args.args[i]
1318
+ if (typeof arg === "string" && arg.startsWith("--")) {
1319
+ const parts = arg.slice(2).split("=")
1320
+ const key = parts[0]
1321
+ const val = parts.length > 1 ? parts[1] : args.args[i + 1]
1322
+ if (key) {
1323
+ normalized[key] = val
1324
+ if (parts.length === 1) i++ // skip next since it was used as value
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ return normalized
1331
+ }
1332
+
1333
+ /**
1334
+ * Performs a proactive validation turn with the Clerk.
1335
+ */
1336
+ async function validateArgumentsProactively(input: {
1337
+ toolName: string
1338
+ args: any
1339
+ schema: any
1340
+ provider: any
1341
+ cfg: Config.Info
1342
+ isMcpx?: boolean
1343
+ mcpxServer?: string
1344
+ l: any
1345
+ }): Promise<string | null> {
1346
+ try {
1347
+ if (input.schema) {
1348
+ // Attempt strict deterministic validation first to avoid LLM hallucinations
1349
+ try {
1350
+ const validate = ajv.compile(input.schema)
1351
+ // For MCPX wrappers, the actual arguments to validate are nested
1352
+ // The interceptToolLoop already attempts to unwrap them into validationArgs,
1353
+ // but we ensure we are validating the correct payload against the sub-tool schema.
1354
+ const payloadToValidate = input.isMcpx ? normalizeMcpxArguments(input.args) : input.args
1355
+
1356
+ if (validate(payloadToValidate)) {
1357
+ input.l.info("Deterministic schema validation passed", { tool: input.toolName })
1358
+ return null // Valid, bypass side-model
1359
+ }
1360
+
1361
+ input.l.info("Deterministic schema validation failed, falling back to side-model for explanation", {
1362
+ tool: input.toolName,
1363
+ errors: validate.errors
1364
+ })
1365
+ } catch (e) {
1366
+ input.l.warn("Failed to compile schema for deterministic validation", { error: String(e) })
1367
+ // Fall through to side-model if we can't compile
1368
+ }
1369
+ }
1370
+
1371
+ const sideModel = await Provider.getSideModel()
1372
+ if (!sideModel) return null
1373
+ const sideLanguage = await Provider.getLanguage(sideModel)
1374
+
1375
+ const systemPrompt = input.isMcpx
1376
+ ? `You are a tool argument validator. The user is attempting to call a sub-tool via the 'mcpx' tool wrapper. Compare the proposed SUB-TOOL arguments with the provided JSON schema.
1377
+
1378
+ CRITICAL CONSTRAINTS:
1379
+ 1. You MUST ONLY validate against the properties explicitly defined in the provided JSON schema.
1380
+ 2. DO NOT invent new required properties or 'extra' fields. If a property is NOT in the schema, it is NOT required.
1381
+ 3. If the proposed arguments satisfy the schema's 'required' array and property types, you MUST output 'VALID'.
1382
+ 4. If they are invalid, output 'INVALID: <concise_reason> EXAMPLE: <strict_valid_json_example>'. You MUST explicitly explain what was missing or incorrect based SOLELY on the schema.
1383
+
1384
+ TECHNICAL FORMATTING for 'mcpx' wrapper:
1385
+ - Positional arguments (like command names or paths) MUST be passed in the 'args' array of strings.
1386
+ - Named flags (like --path) should be in the 'flags' record.
1387
+ - EXAMPLE for 'spec sc_status': \`{ "server": "spec", "tool": "sc_status", "args": [] }\`
1388
+ - Your output example MUST be a JSON object containing "server", "tool", and "args" (and/or "flags").`
1389
+ : `You are a tool argument validator. Compare the proposed arguments with the provided JSON schema.
1390
+
1391
+ CRITICAL CONSTRAINTS:
1392
+ 1. You MUST ONLY validate against the properties explicitly defined in the provided JSON schema.
1393
+ 2. DO NOT invent new required properties, 'extra' fields, or metadata requirements. If a property is NOT in the schema, it is NOT required.
1394
+ 3. If the proposed arguments satisfy the schema's 'required' array and property types, you MUST output 'VALID'.
1395
+ 4. If they are invalid, output 'INVALID: <concise_reason> EXAMPLE: <strict_valid_json_example>'. You MUST explicitly explain what was missing or incorrect based SOLELY on the schema, and provide a strict, concrete JSON example of what the valid arguments should look like according to that schema.`
1396
+
1397
+ const prompt = `Tool: ${input.toolName}\nProposed Args: ${JSON.stringify(input.args)}\nSchema: ${JSON.stringify(input.schema)}`
1398
+
1399
+ const res = await generateText({
1400
+ model: sideLanguage,
1401
+ system: systemPrompt,
1402
+ prompt: `${prompt}\n\nValidation Result:`,
1403
+ abortSignal: AbortSignal.timeout(10000),
1404
+ maxRetries: 0,
1405
+ })
1406
+
1407
+ const result = res.text.trim()
1408
+ if (result.startsWith("INVALID")) {
1409
+ return result.replace(/^INVALID:\s*/, "")
1410
+ }
1411
+ return null
1412
+ } catch (e) {
1413
+ input.l.warn("Proactive validation failed", { error: String(e) })
1414
+ return null // Graceful fall-through
1415
+ }
1416
+ }
1417
+
1418
+ export async function interceptToolLoop(input: {
1419
+ toolName: string
1420
+ args: any
1421
+ messages: ModelMessage[]
1422
+ provider: any
1423
+ cfg: Config.Info
1424
+ }) {
1425
+ const l = log.clone()
1426
+ l.error(`!!! DEBUG: interceptToolLoop called for ${input.toolName}. History length: ${input.messages.length}`)
1427
+
1428
+ // Task: Proactive Validation (Clerk / local-side)
1429
+ if (input.provider.id === "local-main") {
1430
+ try {
1431
+ let schemaToolName = input.toolName
1432
+ let validationArgs = input.args
1433
+
1434
+ let schema: any = undefined
1435
+
1436
+ let isMcpx = false
1437
+ let mcpxServer = ""
1438
+
1439
+ // Specialized handling for mcpx sub-tools
1440
+ if (input.toolName === "mcpx" && input.args.server && input.args.tool) {
1441
+ isMcpx = true
1442
+ mcpxServer = input.args.server
1443
+ schemaToolName = input.args.tool
1444
+ validationArgs = normalizeMcpxArguments(input.args)
1445
+ schema = await SchemaContextLoader.getMcpxToolSchema(
1446
+ input.args.server,
1447
+ input.args.tool,
1448
+ MCP.resolveMcpxBinary(input.cfg),
1449
+ )
1450
+ } else {
1451
+ schema = await SchemaContextLoader.getToolSchema(schemaToolName)
1452
+ }
1453
+
1454
+ if (schema) {
1455
+ log.info("Performing proactive validation", { tool: schemaToolName })
1456
+ const hint = await validateArgumentsProactively({
1457
+ toolName: schemaToolName,
1458
+ args: validationArgs,
1459
+ schema,
1460
+ provider: input.provider,
1461
+ cfg: input.cfg,
1462
+ isMcpx,
1463
+ mcpxServer,
1464
+ l,
1465
+ })
1466
+
1467
+ if (hint) {
1468
+ log.info("Proactive validation caught error", { tool: schemaToolName, hint })
1469
+ return {
1470
+ error: `INVALID ARGUMENTS: ${hint}`,
1471
+ output: "",
1472
+ title: "Argument Validation",
1473
+ metadata: { schema_validated: true, proactive: true },
1474
+ }
1475
+ }
1476
+ }
1477
+ } catch (e) {
1478
+ l.warn("Proactive validation turn failed", { error: String(e) })
1479
+ }
1480
+ }
1481
+
1482
+ let identicalCount = 0
1483
+ let globalSequentialFailureCount = 0
1484
+ let identicalChainActive = true
1485
+ let failureChainActive = true
1486
+
1487
+ // For constructing the timeline prompt
1488
+ const actionTimeline: string[] = []
1489
+
1490
+ // Scan backwards through messages
1491
+ for (let i = input.messages.length - 1; i >= 0; i--) {
1492
+ if (!identicalChainActive && !failureChainActive && actionTimeline.length >= 8) break
1493
+
1494
+ const msg = input.messages[i]
1495
+
1496
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
1497
+ for (const part of msg.content) {
1498
+ if (part.type === "tool-call") {
1499
+ const tName = (part as any).toolName || (part as any).name
1500
+ const tArgs = (part as any).args
1501
+ const tId = (part as any).toolCallId || (part as any).id
1502
+
1503
+ let callResult = "unknown"
1504
+ let callOutput = ""
1505
+ let isErrorResult = false
1506
+
1507
+ // Find corresponding tool result
1508
+ const nextMsg = input.messages[i + 1]
1509
+ if (nextMsg && nextMsg.role === "user" && Array.isArray(nextMsg.content)) {
1510
+ const result = (nextMsg.content as any[]).find((c) => c.type === "tool-result" && c.toolCallId === tId)
1511
+ if (result) {
1512
+ isErrorResult = !!(result as any).isError
1513
+ callResult = isErrorResult ? "error" : "completed"
1514
+
1515
+ const outputVal = (result as any).output?.value || (result as any).output || ""
1516
+ callOutput = typeof outputVal === "string" ? outputVal : JSON.stringify(outputVal)
1517
+
1518
+ // Trim output for the timeline to prevent massive prompts
1519
+ if (callOutput.length > 500) {
1520
+ callOutput = callOutput.substring(0, 500) + "... [truncated]"
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ // 1. Build Action Timeline (limit to 8)
1526
+ if (actionTimeline.length < 8) {
1527
+ actionTimeline.unshift(
1528
+ `Tool '${tName}' executed (input: ${JSON.stringify(tArgs)}) -> Result: ${callResult} -> Output/Error: ${callOutput}`,
1529
+ )
1530
+ }
1531
+
1532
+ // 2. Global Sequential Failures
1533
+ if (failureChainActive) {
1534
+ if (isErrorResult) {
1535
+ globalSequentialFailureCount++
1536
+ } else {
1537
+ // Any successful tool call breaks the global failure chain
1538
+ failureChainActive = false
1539
+ }
1540
+ }
1541
+
1542
+ // 3. Identical arguments chain (only for the currently invoked tool)
1543
+ if (tName === input.toolName) {
1544
+ if (identicalChainActive) {
1545
+ if (JSON.stringify(tArgs) === JSON.stringify(input.args)) {
1546
+ identicalCount++
1547
+ } else {
1548
+ identicalChainActive = false
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+ }
1554
+ } else if (msg.role === "user" && typeof msg.content === "string") {
1555
+ // Ignore internal state checks
1556
+ if (!msg.content.includes("[INTERNAL STATE CHECK]")) {
1557
+ // User interrupted or added new text
1558
+ identicalChainActive = false
1559
+ failureChainActive = false
1560
+ }
1561
+ }
1562
+ }
1563
+
1564
+ const isIdenticalLoop = identicalCount >= 1
1565
+ const isFailureLoop = globalSequentialFailureCount >= 3
1566
+
1567
+ // Schema-Aware Error Recovery (Task 2.2)
1568
+ if (globalSequentialFailureCount >= 2 && !isIdenticalLoop && !isFailureLoop) {
1569
+ try {
1570
+ const schema = await SchemaContextLoader.getToolSchema(input.toolName)
1571
+ if (schema && input.provider.id === "local-main") {
1572
+ log.debug("Generating schema-aware correction hint with local-side Clerk")
1573
+ const sideModel = await Provider.getSideModel()
1574
+ if (!sideModel) return null
1575
+ const sideLanguage = await Provider.getLanguage(sideModel)
1576
+
1577
+ const systemPrompt =
1578
+ "You are a tool argument validator. Compare the failed arguments with the provided JSON schema. Identify the mistake and provide a concise, helpful correction hint. Do not be verbose. Example: 'You are using --path, but sc_guidance accepts no arguments. Try calling it without flags.'"
1579
+
1580
+ const prompt = `Tool: ${input.toolName}\nFailed Args: ${JSON.stringify(input.args)}\nSchema: ${JSON.stringify(schema)}`
1581
+
1582
+ const hint = await generateText({
1583
+ model: sideLanguage,
1584
+ system: systemPrompt,
1585
+ prompt: `${prompt}\n\nCorrection Hint:`,
1586
+ abortSignal: AbortSignal.timeout(10000),
1587
+ maxRetries: 0,
1588
+ })
1589
+
1590
+ return {
1591
+ error: `INVALID ARGUMENTS: ${hint.text}`,
1592
+ output: "",
1593
+ title: "Argument Validation",
1594
+ metadata: { schema_validated: true },
1595
+ }
1596
+ }
1597
+ } catch (e) {
1598
+ log.warn("Failed to generate schema-aware hint", { error: String(e) })
1599
+ }
1600
+ }
1601
+
1602
+ if (isIdenticalLoop || isFailureLoop) {
1603
+ const loopType = isIdenticalLoop ? "IDENTICAL_ARGS" : "SEQUENTIAL_FAILURES"
1604
+ log.warn(`Loop detected for tool ${input.toolName}`, {
1605
+ toolName: input.toolName,
1606
+ args: input.args,
1607
+ loopType,
1608
+ globalSequentialFailureCount,
1609
+ })
1610
+
1611
+ // Use local-side to generate an intervention
1612
+ if (input.provider.id === "local-main") {
1613
+ try {
1614
+ const sideProviderConfig = input.cfg.provider?.["local-side"]
1615
+ if (sideProviderConfig) {
1616
+ log.debug("Generating intervention directive with local-side Clerk")
1617
+ const sideModel = await Provider.getSideModel()
1618
+ if (!sideModel) return null
1619
+ const sideLanguage = await Provider.getLanguage(sideModel)
1620
+
1621
+ let continuityContext = ""
1622
+ for (let i = 0; i < input.messages.length; i++) {
1623
+ const m = input.messages[i]
1624
+ if (
1625
+ m.role === "user" &&
1626
+ typeof m.content === "string" &&
1627
+ m.content.includes("Hi, we are continuing a project as the context window ran out")
1628
+ ) {
1629
+ continuityContext = `\n\nContext (Epoch Continuity Report):\n${m.content}\n`
1630
+ break
1631
+ }
1632
+ }
1633
+
1634
+ const systemPrompt =
1635
+ "You are an AI supervisor monitoring a main agent. The main agent is stuck in a doom loop or has stagnated. Analyze the recent failed attempts and the provided Action Timeline. Provide a concise, stern directive telling the agent to STOP calling this tool or repeating this behavior. Explain why its current approach is failing based on the error messages in the timeline, and suggest a specific actionable alternative strategy (like editing a file or using a different tool). Do not output anything other than the directive."
1636
+
1637
+ const timelineStr = actionTimeline.map((a, idx) => `Turn ${idx + 1}: ${a}`).join("\n")
1638
+
1639
+ const prompt = isIdenticalLoop
1640
+ ? `Tool: ${input.toolName}\nArgs: ${JSON.stringify(input.args)}\nStatus: Stuck in an infinite loop with identical arguments.${continuityContext}\n\nRecent Action Timeline:\n${timelineStr}`
1641
+ : `Tool: ${input.toolName}\nStatus: Stuck in a trial-and-error loop where all recent attempts have failed.${continuityContext}\n\nRecent Action Timeline:\n${timelineStr}`
1642
+
1643
+ const intervention = await generateText({
1644
+ model: sideLanguage,
1645
+ system: systemPrompt,
1646
+ prompt: `${prompt}\n\nPlease provide the intervention directive:`,
1647
+ abortSignal: AbortSignal.timeout(15000),
1648
+ maxRetries: 0,
1649
+ })
1650
+
1651
+ const interventionEvent: Log.EnhancedModelExecutionEvent = {
1652
+ timestamp: Date.now(),
1653
+ mainEpochId: "test", // placeholder, will be real in stream()
1654
+ event: "END_GENERATE",
1655
+ providerId: "local-side",
1656
+ phase: "Phase 2",
1657
+ metrics: { loop_detected: true, loop_type: loopType },
1658
+ }
1659
+ log.info("intervention", interventionEvent)
1660
+
1661
+ return { error: `SYSTEM INTERVENTION: ${intervention.text}`, output: "", title: "", metadata: {} }
1662
+ }
1663
+ } catch (e) {
1664
+ log.error("Failed to generate intervention with local-side", { error: String(e) })
1665
+ }
1666
+ }
1667
+ return {
1668
+ error: `SYSTEM INTERVENTION: You are stuck in a ${isIdenticalLoop ? "loop calling this tool with the exact same arguments" : "repeated failure loop with this tool"}. Stop and reconsider your approach.`,
1669
+ output: "",
1670
+ title: "",
1671
+ metadata: {},
1672
+ }
1673
+ }
1674
+ return null
1675
+ }
1676
+ }