mastracode 0.0.1

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 (336) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +18 -0
  3. package/README.md +15 -0
  4. package/bin/opencode +84 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +113 -0
  7. package/parsers-config.ts +239 -0
  8. package/script/build.ts +167 -0
  9. package/script/postinstall.mjs +122 -0
  10. package/script/publish-registries.ts +187 -0
  11. package/script/publish.ts +70 -0
  12. package/script/schema.ts +47 -0
  13. package/src/acp/README.md +164 -0
  14. package/src/acp/agent.ts +1051 -0
  15. package/src/acp/session.ts +101 -0
  16. package/src/acp/types.ts +22 -0
  17. package/src/agent/agent.ts +398 -0
  18. package/src/agent/generate.txt +75 -0
  19. package/src/agent/prompt/compaction.txt +12 -0
  20. package/src/agent/prompt/explore.txt +18 -0
  21. package/src/agent/prompt/summary.txt +10 -0
  22. package/src/agent/prompt/title.txt +36 -0
  23. package/src/auth/index.ts +70 -0
  24. package/src/bun/index.ts +114 -0
  25. package/src/bus/bus-event.ts +43 -0
  26. package/src/bus/global.ts +10 -0
  27. package/src/bus/index.ts +105 -0
  28. package/src/cli/bootstrap.ts +17 -0
  29. package/src/cli/cmd/acp.ts +88 -0
  30. package/src/cli/cmd/agent.ts +256 -0
  31. package/src/cli/cmd/auth.ts +391 -0
  32. package/src/cli/cmd/cmd.ts +7 -0
  33. package/src/cli/cmd/debug/config.ts +15 -0
  34. package/src/cli/cmd/debug/file.ts +91 -0
  35. package/src/cli/cmd/debug/index.ts +43 -0
  36. package/src/cli/cmd/debug/lsp.ts +48 -0
  37. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  38. package/src/cli/cmd/debug/scrap.ts +15 -0
  39. package/src/cli/cmd/debug/skill.ts +15 -0
  40. package/src/cli/cmd/debug/snapshot.ts +48 -0
  41. package/src/cli/cmd/export.ts +88 -0
  42. package/src/cli/cmd/generate.ts +38 -0
  43. package/src/cli/cmd/github.ts +1408 -0
  44. package/src/cli/cmd/import.ts +98 -0
  45. package/src/cli/cmd/mcp.ts +654 -0
  46. package/src/cli/cmd/models.ts +77 -0
  47. package/src/cli/cmd/pr.ts +112 -0
  48. package/src/cli/cmd/run.ts +368 -0
  49. package/src/cli/cmd/serve.ts +31 -0
  50. package/src/cli/cmd/session.ts +106 -0
  51. package/src/cli/cmd/stats.ts +298 -0
  52. package/src/cli/cmd/tui/app.tsx +686 -0
  53. package/src/cli/cmd/tui/attach.ts +30 -0
  54. package/src/cli/cmd/tui/component/border.tsx +21 -0
  55. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  56. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  57. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  58. package/src/cli/cmd/tui/component/dialog-model.tsx +230 -0
  59. package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
  60. package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
  61. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  62. package/src/cli/cmd/tui/component/dialog-stash.tsx +86 -0
  63. package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
  64. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  65. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  66. package/src/cli/cmd/tui/component/did-you-know.tsx +85 -0
  67. package/src/cli/cmd/tui/component/logo.tsx +27 -0
  68. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +574 -0
  69. package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
  70. package/src/cli/cmd/tui/component/prompt/index.tsx +1117 -0
  71. package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
  72. package/src/cli/cmd/tui/component/tips.ts +103 -0
  73. package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
  74. package/src/cli/cmd/tui/context/args.tsx +14 -0
  75. package/src/cli/cmd/tui/context/directory.ts +13 -0
  76. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  77. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  78. package/src/cli/cmd/tui/context/keybind.tsx +101 -0
  79. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  80. package/src/cli/cmd/tui/context/local.tsx +339 -0
  81. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  82. package/src/cli/cmd/tui/context/route.tsx +46 -0
  83. package/src/cli/cmd/tui/context/sdk.tsx +74 -0
  84. package/src/cli/cmd/tui/context/sync.tsx +372 -0
  85. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  86. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  87. package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
  88. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  89. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  90. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  91. package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
  92. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  93. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  94. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  95. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  96. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  97. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  98. package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
  99. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  100. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  101. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  102. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  103. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  104. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  105. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  106. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  107. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  108. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  109. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  110. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  111. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  112. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  113. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  114. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  115. package/src/cli/cmd/tui/context/theme.tsx +1109 -0
  116. package/src/cli/cmd/tui/event.ts +40 -0
  117. package/src/cli/cmd/tui/routes/home.tsx +140 -0
  118. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
  119. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
  120. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
  121. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
  122. package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
  123. package/src/cli/cmd/tui/routes/session/header.tsx +141 -0
  124. package/src/cli/cmd/tui/routes/session/index.tsx +1885 -0
  125. package/src/cli/cmd/tui/routes/session/sidebar.tsx +322 -0
  126. package/src/cli/cmd/tui/spawn.ts +60 -0
  127. package/src/cli/cmd/tui/thread.ts +120 -0
  128. package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
  129. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
  130. package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
  131. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
  132. package/src/cli/cmd/tui/ui/dialog-select.tsx +332 -0
  133. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  134. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  135. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  136. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  137. package/src/cli/cmd/tui/util/editor.ts +32 -0
  138. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  139. package/src/cli/cmd/tui/worker.ts +63 -0
  140. package/src/cli/cmd/uninstall.ts +344 -0
  141. package/src/cli/cmd/upgrade.ts +67 -0
  142. package/src/cli/cmd/web.ts +84 -0
  143. package/src/cli/error.ts +56 -0
  144. package/src/cli/ui.ts +84 -0
  145. package/src/cli/upgrade.ts +25 -0
  146. package/src/command/index.ts +80 -0
  147. package/src/command/template/initialize.txt +10 -0
  148. package/src/command/template/review.txt +97 -0
  149. package/src/config/config.ts +997 -0
  150. package/src/config/markdown.ts +41 -0
  151. package/src/env/index.ts +26 -0
  152. package/src/file/ignore.ts +83 -0
  153. package/src/file/index.ts +328 -0
  154. package/src/file/ripgrep.ts +393 -0
  155. package/src/file/time.ts +64 -0
  156. package/src/file/watcher.ts +103 -0
  157. package/src/flag/flag.ts +46 -0
  158. package/src/format/formatter.ts +315 -0
  159. package/src/format/index.ts +137 -0
  160. package/src/global/index.ts +52 -0
  161. package/src/id/id.ts +73 -0
  162. package/src/ide/index.ts +76 -0
  163. package/src/index.ts +158 -0
  164. package/src/installation/index.ts +196 -0
  165. package/src/lsp/client.ts +229 -0
  166. package/src/lsp/index.ts +485 -0
  167. package/src/lsp/language.ts +116 -0
  168. package/src/lsp/server.ts +1895 -0
  169. package/src/mcp/auth.ts +135 -0
  170. package/src/mcp/index.ts +654 -0
  171. package/src/mcp/oauth-callback.ts +200 -0
  172. package/src/mcp/oauth-provider.ts +154 -0
  173. package/src/patch/index.ts +622 -0
  174. package/src/permission/index.ts +199 -0
  175. package/src/plugin/index.ts +91 -0
  176. package/src/project/bootstrap.ts +31 -0
  177. package/src/project/instance.ts +78 -0
  178. package/src/project/project.ts +221 -0
  179. package/src/project/state.ts +65 -0
  180. package/src/project/vcs.ts +76 -0
  181. package/src/provider/auth.ts +143 -0
  182. package/src/provider/models-macro.ts +11 -0
  183. package/src/provider/models.ts +106 -0
  184. package/src/provider/provider.ts +1056 -0
  185. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  186. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  187. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
  190. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  191. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  192. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  193. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  194. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  195. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  196. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  197. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  198. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  199. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  200. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  201. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  202. package/src/provider/transform.ts +455 -0
  203. package/src/pty/index.ts +231 -0
  204. package/src/server/error.ts +36 -0
  205. package/src/server/project.ts +79 -0
  206. package/src/server/server.ts +2642 -0
  207. package/src/server/tui.ts +71 -0
  208. package/src/session/compaction.ts +223 -0
  209. package/src/session/index.ts +458 -0
  210. package/src/session/llm-mastra.ts +412 -0
  211. package/src/session/llm-shared.ts +172 -0
  212. package/src/session/llm.ts +439 -0
  213. package/src/session/message-v2.ts +675 -0
  214. package/src/session/message.ts +189 -0
  215. package/src/session/processor.ts +171 -0
  216. package/src/session/prompt/anthropic-20250930.txt +166 -0
  217. package/src/session/prompt/anthropic.txt +105 -0
  218. package/src/session/prompt/anthropic_spoof.txt +1 -0
  219. package/src/session/prompt/beast.txt +147 -0
  220. package/src/session/prompt/build-switch.txt +5 -0
  221. package/src/session/prompt/codex.txt +318 -0
  222. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  223. package/src/session/prompt/gemini.txt +155 -0
  224. package/src/session/prompt/max-steps.txt +16 -0
  225. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  226. package/src/session/prompt/plan.txt +26 -0
  227. package/src/session/prompt/polaris.txt +107 -0
  228. package/src/session/prompt/qwen.txt +109 -0
  229. package/src/session/prompt.ts +1454 -0
  230. package/src/session/retry.ts +86 -0
  231. package/src/session/revert.ts +108 -0
  232. package/src/session/status.ts +76 -0
  233. package/src/session/summary.ts +194 -0
  234. package/src/session/system.ts +120 -0
  235. package/src/session/todo.ts +37 -0
  236. package/src/share/share-next.ts +194 -0
  237. package/src/share/share.ts +87 -0
  238. package/src/shell/shell.ts +67 -0
  239. package/src/skill/index.ts +1 -0
  240. package/src/skill/skill.ts +83 -0
  241. package/src/snapshot/index.ts +197 -0
  242. package/src/storage/storage.ts +226 -0
  243. package/src/tool/bash.ts +306 -0
  244. package/src/tool/bash.txt +158 -0
  245. package/src/tool/batch.ts +175 -0
  246. package/src/tool/batch.txt +24 -0
  247. package/src/tool/codesearch.ts +138 -0
  248. package/src/tool/codesearch.txt +12 -0
  249. package/src/tool/edit.ts +675 -0
  250. package/src/tool/edit.txt +10 -0
  251. package/src/tool/glob.ts +65 -0
  252. package/src/tool/glob.txt +6 -0
  253. package/src/tool/grep.ts +121 -0
  254. package/src/tool/grep.txt +8 -0
  255. package/src/tool/invalid.ts +17 -0
  256. package/src/tool/ls.ts +110 -0
  257. package/src/tool/ls.txt +1 -0
  258. package/src/tool/lsp-diagnostics.ts +26 -0
  259. package/src/tool/lsp-diagnostics.txt +1 -0
  260. package/src/tool/lsp-hover.ts +31 -0
  261. package/src/tool/lsp-hover.txt +1 -0
  262. package/src/tool/lsp.ts +87 -0
  263. package/src/tool/lsp.txt +19 -0
  264. package/src/tool/multiedit.ts +46 -0
  265. package/src/tool/multiedit.txt +41 -0
  266. package/src/tool/patch.ts +233 -0
  267. package/src/tool/patch.txt +1 -0
  268. package/src/tool/read.ts +219 -0
  269. package/src/tool/read.txt +12 -0
  270. package/src/tool/registry.ts +162 -0
  271. package/src/tool/skill.ts +100 -0
  272. package/src/tool/task.ts +136 -0
  273. package/src/tool/task.txt +60 -0
  274. package/src/tool/todo.ts +39 -0
  275. package/src/tool/todoread.txt +14 -0
  276. package/src/tool/todowrite.txt +167 -0
  277. package/src/tool/tool.ts +71 -0
  278. package/src/tool/webfetch.ts +187 -0
  279. package/src/tool/webfetch.txt +13 -0
  280. package/src/tool/websearch.ts +150 -0
  281. package/src/tool/websearch.txt +11 -0
  282. package/src/tool/write.ts +110 -0
  283. package/src/tool/write.txt +8 -0
  284. package/src/util/archive.ts +16 -0
  285. package/src/util/color.ts +19 -0
  286. package/src/util/context.ts +25 -0
  287. package/src/util/defer.ts +12 -0
  288. package/src/util/eventloop.ts +20 -0
  289. package/src/util/filesystem.ts +83 -0
  290. package/src/util/fn.ts +11 -0
  291. package/src/util/iife.ts +3 -0
  292. package/src/util/keybind.ts +102 -0
  293. package/src/util/lazy.ts +11 -0
  294. package/src/util/locale.ts +81 -0
  295. package/src/util/lock.ts +98 -0
  296. package/src/util/log.ts +180 -0
  297. package/src/util/queue.ts +32 -0
  298. package/src/util/rpc.ts +42 -0
  299. package/src/util/scrap.ts +10 -0
  300. package/src/util/signal.ts +12 -0
  301. package/src/util/timeout.ts +14 -0
  302. package/src/util/token.ts +7 -0
  303. package/src/util/wildcard.ts +54 -0
  304. package/sst-env.d.ts +9 -0
  305. package/test/agent/agent.test.ts +146 -0
  306. package/test/bun.test.ts +53 -0
  307. package/test/cli/github-remote.test.ts +80 -0
  308. package/test/config/agent-color.test.ts +66 -0
  309. package/test/config/config.test.ts +535 -0
  310. package/test/config/markdown.test.ts +89 -0
  311. package/test/file/ignore.test.ts +10 -0
  312. package/test/fixture/fixture.ts +34 -0
  313. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  314. package/test/ide/ide.test.ts +82 -0
  315. package/test/keybind.test.ts +421 -0
  316. package/test/lsp/client.test.ts +95 -0
  317. package/test/mcp/headers.test.ts +153 -0
  318. package/test/patch/patch.test.ts +348 -0
  319. package/test/preload.ts +57 -0
  320. package/test/project/project.test.ts +72 -0
  321. package/test/provider/provider.test.ts +1809 -0
  322. package/test/provider/transform.test.ts +411 -0
  323. package/test/session/retry.test.ts +61 -0
  324. package/test/session/session.test.ts +71 -0
  325. package/test/skill/skill.test.ts +131 -0
  326. package/test/snapshot/snapshot.test.ts +939 -0
  327. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  328. package/test/tool/bash.test.ts +434 -0
  329. package/test/tool/grep.test.ts +108 -0
  330. package/test/tool/patch.test.ts +259 -0
  331. package/test/tool/read.test.ts +42 -0
  332. package/test/util/iife.test.ts +36 -0
  333. package/test/util/lazy.test.ts +50 -0
  334. package/test/util/timeout.test.ts +21 -0
  335. package/test/util/wildcard.test.ts +55 -0
  336. package/tsconfig.json +16 -0
@@ -0,0 +1,1454 @@
1
+ import path from "path"
2
+ import os from "os"
3
+ import fs from "fs/promises"
4
+ import z from "zod"
5
+ import { Identifier } from "../id/id"
6
+ import { MessageV2 } from "./message-v2"
7
+ import { Log } from "../util/log"
8
+ import { SessionRevert } from "./revert"
9
+ import { Session } from "."
10
+ import { Agent } from "../agent/agent"
11
+ import { Provider } from "../provider/provider"
12
+ import { type Tool as AITool, tool, jsonSchema } from "ai"
13
+ import { SessionCompaction } from "./compaction"
14
+ import { Instance } from "../project/instance"
15
+ import { Bus } from "../bus"
16
+ import { ProviderTransform } from "../provider/transform"
17
+ import { SystemPrompt } from "./system"
18
+ import { Plugin } from "../plugin"
19
+ import PROMPT_PLAN from "../session/prompt/plan.txt"
20
+ import BUILD_SWITCH from "../session/prompt/build-switch.txt"
21
+ import MAX_STEPS from "../session/prompt/max-steps.txt"
22
+ import { defer } from "../util/defer"
23
+ import { clone, mergeDeep, pipe } from "remeda"
24
+ import { ToolRegistry } from "../tool/registry"
25
+ import { Wildcard } from "../util/wildcard"
26
+ import { MCP } from "../mcp"
27
+ import { LSP } from "../lsp"
28
+ import { ReadTool } from "../tool/read"
29
+ import { ListTool } from "../tool/ls"
30
+ import { FileTime } from "../file/time"
31
+ import { Flag } from "../flag/flag"
32
+ import { ulid } from "ulid"
33
+ import { spawn } from "child_process"
34
+ import { Command } from "../command"
35
+ import { $, fileURLToPath } from "bun"
36
+ import { ConfigMarkdown } from "../config/markdown"
37
+ import { SessionSummary } from "./summary"
38
+ import { NamedError } from "@opencode-ai/util/error"
39
+ import { fn } from "@/util/fn"
40
+ import { SessionProcessor } from "./processor"
41
+ import { TaskTool } from "@/tool/task"
42
+ import { SessionStatus } from "./status"
43
+ import { LLM } from "./llm"
44
+ import { iife } from "@/util/iife"
45
+ import { Shell } from "@/shell/shell"
46
+
47
+ // @ts-ignore
48
+ globalThis.AI_SDK_LOG_WARNINGS = false
49
+
50
+ export namespace SessionPrompt {
51
+ const log = Log.create({ service: "session.prompt" })
52
+ export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
53
+
54
+ const state = Instance.state(
55
+ () => {
56
+ const data: Record<
57
+ string,
58
+ {
59
+ abort: AbortController
60
+ callbacks: {
61
+ resolve(input: MessageV2.WithParts): void
62
+ reject(): void
63
+ }[]
64
+ }
65
+ > = {}
66
+ return data
67
+ },
68
+ async (current) => {
69
+ for (const item of Object.values(current)) {
70
+ item.abort.abort()
71
+ }
72
+ },
73
+ )
74
+
75
+ export function assertNotBusy(sessionID: string) {
76
+ const match = state()[sessionID]
77
+ if (match) throw new Session.BusyError(sessionID)
78
+ }
79
+
80
+ export const PromptInput = z.object({
81
+ sessionID: Identifier.schema("session"),
82
+ messageID: Identifier.schema("message").optional(),
83
+ model: z
84
+ .object({
85
+ providerID: z.string(),
86
+ modelID: z.string(),
87
+ })
88
+ .optional(),
89
+ agent: z.string().optional(),
90
+ noReply: z.boolean().optional(),
91
+ tools: z.record(z.string(), z.boolean()).optional(),
92
+ system: z.string().optional(),
93
+ parts: z.array(
94
+ z.discriminatedUnion("type", [
95
+ MessageV2.TextPart.omit({
96
+ messageID: true,
97
+ sessionID: true,
98
+ })
99
+ .partial({
100
+ id: true,
101
+ })
102
+ .meta({
103
+ ref: "TextPartInput",
104
+ }),
105
+ MessageV2.FilePart.omit({
106
+ messageID: true,
107
+ sessionID: true,
108
+ })
109
+ .partial({
110
+ id: true,
111
+ })
112
+ .meta({
113
+ ref: "FilePartInput",
114
+ }),
115
+ MessageV2.AgentPart.omit({
116
+ messageID: true,
117
+ sessionID: true,
118
+ })
119
+ .partial({
120
+ id: true,
121
+ })
122
+ .meta({
123
+ ref: "AgentPartInput",
124
+ }),
125
+ MessageV2.SubtaskPart.omit({
126
+ messageID: true,
127
+ sessionID: true,
128
+ })
129
+ .partial({
130
+ id: true,
131
+ })
132
+ .meta({
133
+ ref: "SubtaskPartInput",
134
+ }),
135
+ ]),
136
+ ),
137
+ })
138
+ export type PromptInput = z.infer<typeof PromptInput>
139
+
140
+ export const prompt = fn(PromptInput, async (input) => {
141
+ const session = await Session.get(input.sessionID)
142
+ await SessionRevert.cleanup(session)
143
+
144
+ const message = await createUserMessage(input)
145
+ await Session.touch(input.sessionID)
146
+
147
+ if (input.noReply === true) {
148
+ return message
149
+ }
150
+
151
+ return loop(input.sessionID)
152
+ })
153
+
154
+ export async function resolvePromptParts(template: string): Promise<PromptInput["parts"]> {
155
+ const parts: PromptInput["parts"] = [
156
+ {
157
+ type: "text",
158
+ text: template,
159
+ },
160
+ ]
161
+ const files = ConfigMarkdown.files(template)
162
+ const seen = new Set<string>()
163
+ await Promise.all(
164
+ files.map(async (match) => {
165
+ const name = match[1]
166
+ if (seen.has(name)) return
167
+ seen.add(name)
168
+ const filepath = name.startsWith("~/")
169
+ ? path.join(os.homedir(), name.slice(2))
170
+ : path.resolve(Instance.worktree, name)
171
+
172
+ const stats = await fs.stat(filepath).catch(() => undefined)
173
+ if (!stats) {
174
+ const agent = await Agent.get(name)
175
+ if (agent) {
176
+ parts.push({
177
+ type: "agent",
178
+ name: agent.name,
179
+ })
180
+ }
181
+ return
182
+ }
183
+
184
+ if (stats.isDirectory()) {
185
+ parts.push({
186
+ type: "file",
187
+ url: `file://${filepath}`,
188
+ filename: name,
189
+ mime: "application/x-directory",
190
+ })
191
+ return
192
+ }
193
+
194
+ parts.push({
195
+ type: "file",
196
+ url: `file://${filepath}`,
197
+ filename: name,
198
+ mime: "text/plain",
199
+ })
200
+ }),
201
+ )
202
+ return parts
203
+ }
204
+
205
+ function start(sessionID: string) {
206
+ const s = state()
207
+ if (s[sessionID]) return
208
+ const controller = new AbortController()
209
+ s[sessionID] = {
210
+ abort: controller,
211
+ callbacks: [],
212
+ }
213
+ return controller.signal
214
+ }
215
+
216
+ export function cancel(sessionID: string) {
217
+ log.info("cancel", { sessionID })
218
+ const s = state()
219
+ const match = s[sessionID]
220
+ if (!match) return
221
+ match.abort.abort()
222
+ for (const item of match.callbacks) {
223
+ item.reject()
224
+ }
225
+ delete s[sessionID]
226
+ SessionStatus.set(sessionID, { type: "idle" })
227
+ return
228
+ }
229
+
230
+ export const loop = fn(Identifier.schema("session"), async (sessionID) => {
231
+ const abort = start(sessionID)
232
+ if (!abort) {
233
+ return new Promise<MessageV2.WithParts>((resolve, reject) => {
234
+ const callbacks = state()[sessionID].callbacks
235
+ callbacks.push({ resolve, reject })
236
+ })
237
+ }
238
+
239
+ using _ = defer(() => cancel(sessionID))
240
+
241
+ let step = 0
242
+ while (true) {
243
+ SessionStatus.set(sessionID, { type: "busy" })
244
+ log.info("loop", { step, sessionID })
245
+ if (abort.aborted) break
246
+ let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
247
+
248
+ let lastUser: MessageV2.User | undefined
249
+ let lastAssistant: MessageV2.Assistant | undefined
250
+ let lastFinished: MessageV2.Assistant | undefined
251
+ let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
252
+ for (let i = msgs.length - 1; i >= 0; i--) {
253
+ const msg = msgs[i]
254
+ if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
255
+ if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
256
+ if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
257
+ lastFinished = msg.info as MessageV2.Assistant
258
+ if (lastUser && lastFinished) break
259
+ const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
260
+ if (task && !lastFinished) {
261
+ tasks.push(...task)
262
+ }
263
+ }
264
+
265
+ if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
266
+ if (
267
+ lastAssistant?.finish &&
268
+ !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
269
+ lastUser.id < lastAssistant.id
270
+ ) {
271
+ log.info("exiting loop", { sessionID })
272
+ break
273
+ }
274
+
275
+ step++
276
+ if (step === 1)
277
+ ensureTitle({
278
+ session: await Session.get(sessionID),
279
+ modelID: lastUser.model.modelID,
280
+ providerID: lastUser.model.providerID,
281
+ message: msgs.find((m) => m.info.role === "user")!,
282
+ history: msgs,
283
+ })
284
+
285
+ const model = await Provider.getModel(lastUser.model.providerID, lastUser.model.modelID)
286
+ const task = tasks.pop()
287
+
288
+ // pending subtask
289
+ // TODO: centralize "invoke tool" logic
290
+ if (task?.type === "subtask") {
291
+ const taskTool = await TaskTool.init()
292
+ const assistantMessage = (await Session.updateMessage({
293
+ id: Identifier.ascending("message"),
294
+ role: "assistant",
295
+ parentID: lastUser.id,
296
+ sessionID,
297
+ mode: task.agent,
298
+ agent: task.agent,
299
+ path: {
300
+ cwd: Instance.directory,
301
+ root: Instance.worktree,
302
+ },
303
+ cost: 0,
304
+ tokens: {
305
+ input: 0,
306
+ output: 0,
307
+ reasoning: 0,
308
+ cache: { read: 0, write: 0 },
309
+ },
310
+ modelID: model.id,
311
+ providerID: model.providerID,
312
+ time: {
313
+ created: Date.now(),
314
+ },
315
+ })) as MessageV2.Assistant
316
+ let part = (await Session.updatePart({
317
+ id: Identifier.ascending("part"),
318
+ messageID: assistantMessage.id,
319
+ sessionID: assistantMessage.sessionID,
320
+ type: "tool",
321
+ callID: ulid(),
322
+ tool: TaskTool.id,
323
+ state: {
324
+ status: "running",
325
+ input: {
326
+ prompt: task.prompt,
327
+ description: task.description,
328
+ subagent_type: task.agent,
329
+ command: task.command,
330
+ },
331
+ time: {
332
+ start: Date.now(),
333
+ },
334
+ },
335
+ })) as MessageV2.ToolPart
336
+ const taskArgs = {
337
+ prompt: task.prompt,
338
+ description: task.description,
339
+ subagent_type: task.agent,
340
+ command: task.command,
341
+ }
342
+ await Plugin.trigger(
343
+ "tool.execute.before",
344
+ {
345
+ tool: "task",
346
+ sessionID,
347
+ callID: part.id,
348
+ },
349
+ { args: taskArgs },
350
+ )
351
+ let executionError: Error | undefined
352
+ const result = await taskTool
353
+ .execute(taskArgs, {
354
+ agent: task.agent,
355
+ messageID: assistantMessage.id,
356
+ sessionID: sessionID,
357
+ abort,
358
+ async metadata(input) {
359
+ await Session.updatePart({
360
+ ...part,
361
+ type: "tool",
362
+ state: {
363
+ ...part.state,
364
+ ...input,
365
+ },
366
+ } satisfies MessageV2.ToolPart)
367
+ },
368
+ })
369
+ .catch((error) => {
370
+ executionError = error
371
+ log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
372
+ return undefined
373
+ })
374
+ await Plugin.trigger(
375
+ "tool.execute.after",
376
+ {
377
+ tool: "task",
378
+ sessionID,
379
+ callID: part.id,
380
+ },
381
+ result,
382
+ )
383
+ assistantMessage.finish = "tool-calls"
384
+ assistantMessage.time.completed = Date.now()
385
+ await Session.updateMessage(assistantMessage)
386
+ if (result && part.state.status === "running") {
387
+ await Session.updatePart({
388
+ ...part,
389
+ state: {
390
+ status: "completed",
391
+ input: part.state.input,
392
+ title: result.title,
393
+ metadata: result.metadata,
394
+ output: result.output,
395
+ attachments: result.attachments,
396
+ time: {
397
+ ...part.state.time,
398
+ end: Date.now(),
399
+ },
400
+ },
401
+ } satisfies MessageV2.ToolPart)
402
+ }
403
+ if (!result) {
404
+ await Session.updatePart({
405
+ ...part,
406
+ state: {
407
+ status: "error",
408
+ error: executionError ? `Tool execution failed: ${executionError.message}` : "Tool execution failed",
409
+ time: {
410
+ start: part.state.status === "running" ? part.state.time.start : Date.now(),
411
+ end: Date.now(),
412
+ },
413
+ metadata: part.metadata,
414
+ input: part.state.input,
415
+ },
416
+ } satisfies MessageV2.ToolPart)
417
+ }
418
+
419
+ // Add synthetic user message to prevent certain reasoning models from erroring
420
+ // If we create assistant messages w/ out user ones following mid loop thinking signatures
421
+ // will be missing and it can cause errors for models like gemini for example
422
+ const summaryUserMsg: MessageV2.User = {
423
+ id: Identifier.ascending("message"),
424
+ sessionID,
425
+ role: "user",
426
+ time: {
427
+ created: Date.now(),
428
+ },
429
+ agent: lastUser.agent,
430
+ model: lastUser.model,
431
+ }
432
+ await Session.updateMessage(summaryUserMsg)
433
+ await Session.updatePart({
434
+ id: Identifier.ascending("part"),
435
+ messageID: summaryUserMsg.id,
436
+ sessionID,
437
+ type: "text",
438
+ text: "Summarize the task tool output above and continue with your task.",
439
+ synthetic: true,
440
+ } satisfies MessageV2.TextPart)
441
+
442
+ continue
443
+ }
444
+
445
+ // pending compaction
446
+ if (task?.type === "compaction") {
447
+ const result = await SessionCompaction.process({
448
+ messages: msgs,
449
+ parentID: lastUser.id,
450
+ abort,
451
+ sessionID,
452
+ auto: task.auto,
453
+ })
454
+ if (result === "stop") break
455
+ continue
456
+ }
457
+
458
+ // context overflow, needs compaction
459
+ if (
460
+ lastFinished &&
461
+ lastFinished.summary !== true &&
462
+ SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
463
+ ) {
464
+ await SessionCompaction.create({
465
+ sessionID,
466
+ agent: lastUser.agent,
467
+ model: lastUser.model,
468
+ auto: true,
469
+ })
470
+ continue
471
+ }
472
+
473
+ // normal processing
474
+ const agent = await Agent.get(lastUser.agent)
475
+ const maxSteps = agent.maxSteps ?? Infinity
476
+ const isLastStep = step >= maxSteps
477
+ msgs = insertReminders({
478
+ messages: msgs,
479
+ agent,
480
+ })
481
+
482
+ const processor = SessionProcessor.create({
483
+ assistantMessage: (await Session.updateMessage({
484
+ id: Identifier.ascending("message"),
485
+ parentID: lastUser.id,
486
+ role: "assistant",
487
+ mode: agent.name,
488
+ agent: agent.name,
489
+ path: {
490
+ cwd: Instance.directory,
491
+ root: Instance.worktree,
492
+ },
493
+ cost: 0,
494
+ tokens: {
495
+ input: 0,
496
+ output: 0,
497
+ reasoning: 0,
498
+ cache: { read: 0, write: 0 },
499
+ },
500
+ modelID: model.id,
501
+ providerID: model.providerID,
502
+ time: {
503
+ created: Date.now(),
504
+ },
505
+ sessionID,
506
+ })) as MessageV2.Assistant,
507
+ sessionID: sessionID,
508
+ model,
509
+ abort,
510
+ })
511
+ const tools = await resolveTools({
512
+ agent,
513
+ sessionID,
514
+ model,
515
+ tools: lastUser.tools,
516
+ processor,
517
+ })
518
+
519
+ if (step === 1) {
520
+ SessionSummary.summarize({
521
+ sessionID: sessionID,
522
+ messageID: lastUser.id,
523
+ })
524
+ }
525
+
526
+ const sessionMessages = clone(msgs)
527
+
528
+ await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages })
529
+
530
+ const result = await processor.process({
531
+ user: lastUser,
532
+ agent,
533
+ abort,
534
+ sessionID,
535
+ system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
536
+ messages: [
537
+ ...MessageV2.toModelMessage(sessionMessages),
538
+ ...(isLastStep
539
+ ? [
540
+ {
541
+ role: "assistant" as const,
542
+ content: MAX_STEPS,
543
+ },
544
+ ]
545
+ : []),
546
+ ],
547
+ tools,
548
+ model,
549
+ maxSteps: agent.maxSteps,
550
+ })
551
+ if (result === "stop") break
552
+ continue
553
+ }
554
+ SessionCompaction.prune({ sessionID })
555
+ for await (const item of MessageV2.stream(sessionID)) {
556
+ if (item.info.role === "user") continue
557
+ const queued = state()[sessionID]?.callbacks ?? []
558
+ for (const q of queued) {
559
+ q.resolve(item)
560
+ }
561
+ return item
562
+ }
563
+ throw new Error("Impossible")
564
+ })
565
+
566
+ async function lastModel(sessionID: string) {
567
+ for await (const item of MessageV2.stream(sessionID)) {
568
+ if (item.info.role === "user" && item.info.model) return item.info.model
569
+ }
570
+ return Provider.defaultModel()
571
+ }
572
+
573
+ async function resolveTools(input: {
574
+ agent: Agent.Info
575
+ model: Provider.Model
576
+ sessionID: string
577
+ tools?: Record<string, boolean>
578
+ processor: SessionProcessor.Info
579
+ }) {
580
+ using _ = log.time("resolveTools")
581
+ const tools: Record<string, AITool> = {}
582
+ const enabledTools = pipe(
583
+ input.agent.tools,
584
+ mergeDeep(await ToolRegistry.enabled(input.agent)),
585
+ mergeDeep(input.tools ?? {}),
586
+ )
587
+ for (const item of await ToolRegistry.tools(input.model.providerID, input.agent)) {
588
+ if (Wildcard.all(item.id, enabledTools) === false) continue
589
+ const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
590
+ tools[item.id] = tool({
591
+ id: item.id as any,
592
+ description: item.description,
593
+ inputSchema: jsonSchema(schema as any),
594
+ async execute(args, options) {
595
+ await Plugin.trigger(
596
+ "tool.execute.before",
597
+ {
598
+ tool: item.id,
599
+ sessionID: input.sessionID,
600
+ callID: options.toolCallId,
601
+ },
602
+ {
603
+ args,
604
+ },
605
+ )
606
+ const result = await item.execute(args, {
607
+ sessionID: input.sessionID,
608
+ abort: options.abortSignal!,
609
+ messageID: input.processor.message.id,
610
+ callID: options.toolCallId,
611
+ extra: { model: input.model },
612
+ agent: input.agent.name,
613
+ metadata: async (val) => {
614
+ const match = input.processor.partFromToolCall(options.toolCallId)
615
+ if (match && match.state.status === "running") {
616
+ await Session.updatePart({
617
+ ...match,
618
+ state: {
619
+ title: val.title,
620
+ metadata: val.metadata,
621
+ status: "running",
622
+ input: args,
623
+ time: {
624
+ start: Date.now(),
625
+ },
626
+ },
627
+ })
628
+ }
629
+ },
630
+ })
631
+ await Plugin.trigger(
632
+ "tool.execute.after",
633
+ {
634
+ tool: item.id,
635
+ sessionID: input.sessionID,
636
+ callID: options.toolCallId,
637
+ },
638
+ result,
639
+ )
640
+ return result
641
+ },
642
+ toModelOutput(result) {
643
+ return {
644
+ type: "text",
645
+ value: result.output,
646
+ }
647
+ },
648
+ })
649
+ }
650
+ for (const [key, item] of Object.entries(await MCP.tools())) {
651
+ if (Wildcard.all(key, enabledTools) === false) continue
652
+ const execute = item.execute
653
+ if (!execute) continue
654
+
655
+ // Wrap execute to add plugin hooks and format output
656
+ item.execute = async (args, opts) => {
657
+ await Plugin.trigger(
658
+ "tool.execute.before",
659
+ {
660
+ tool: key,
661
+ sessionID: input.sessionID,
662
+ callID: opts.toolCallId,
663
+ },
664
+ {
665
+ args,
666
+ },
667
+ )
668
+ const result = await execute(args, opts)
669
+
670
+ await Plugin.trigger(
671
+ "tool.execute.after",
672
+ {
673
+ tool: key,
674
+ sessionID: input.sessionID,
675
+ callID: opts.toolCallId,
676
+ },
677
+ result,
678
+ )
679
+
680
+ const textParts: string[] = []
681
+ const attachments: MessageV2.FilePart[] = []
682
+
683
+ for (const contentItem of result.content) {
684
+ if (contentItem.type === "text") {
685
+ textParts.push(contentItem.text)
686
+ } else if (contentItem.type === "image") {
687
+ attachments.push({
688
+ id: Identifier.ascending("part"),
689
+ sessionID: input.sessionID,
690
+ messageID: input.processor.message.id,
691
+ type: "file",
692
+ mime: contentItem.mimeType,
693
+ url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
694
+ })
695
+ }
696
+ // Add support for other types if needed
697
+ }
698
+
699
+ return {
700
+ title: "",
701
+ metadata: result.metadata ?? {},
702
+ output: textParts.join("\n\n"),
703
+ attachments,
704
+ content: result.content, // directly return content to preserve ordering when outputting to model
705
+ }
706
+ }
707
+ item.toModelOutput = (result) => {
708
+ return {
709
+ type: "text",
710
+ value: result.output,
711
+ }
712
+ }
713
+ tools[key] = item
714
+ }
715
+ return tools
716
+ }
717
+
718
+ async function createUserMessage(input: PromptInput) {
719
+ const agent = await Agent.get(input.agent ?? (await Agent.defaultAgent()))
720
+ const info: MessageV2.Info = {
721
+ id: input.messageID ?? Identifier.ascending("message"),
722
+ role: "user",
723
+ sessionID: input.sessionID,
724
+ time: {
725
+ created: Date.now(),
726
+ },
727
+ tools: input.tools,
728
+ agent: agent.name,
729
+ model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
730
+ system: input.system,
731
+ }
732
+
733
+ const parts = await Promise.all(
734
+ input.parts.map(async (part): Promise<MessageV2.Part[]> => {
735
+ if (part.type === "file") {
736
+ const url = new URL(part.url)
737
+ switch (url.protocol) {
738
+ case "data:":
739
+ if (part.mime === "text/plain") {
740
+ return [
741
+ {
742
+ id: Identifier.ascending("part"),
743
+ messageID: info.id,
744
+ sessionID: input.sessionID,
745
+ type: "text",
746
+ synthetic: true,
747
+ text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
748
+ },
749
+ {
750
+ id: Identifier.ascending("part"),
751
+ messageID: info.id,
752
+ sessionID: input.sessionID,
753
+ type: "text",
754
+ synthetic: true,
755
+ text: Buffer.from(part.url, "base64url").toString(),
756
+ },
757
+ {
758
+ ...part,
759
+ id: part.id ?? Identifier.ascending("part"),
760
+ messageID: info.id,
761
+ sessionID: input.sessionID,
762
+ },
763
+ ]
764
+ }
765
+ break
766
+ case "file:":
767
+ log.info("file", { mime: part.mime })
768
+ // have to normalize, symbol search returns absolute paths
769
+ // Decode the pathname since URL constructor doesn't automatically decode it
770
+ const filepath = fileURLToPath(part.url)
771
+ const stat = await Bun.file(filepath).stat()
772
+
773
+ if (stat.isDirectory()) {
774
+ part.mime = "application/x-directory"
775
+ }
776
+
777
+ if (part.mime === "text/plain") {
778
+ let offset: number | undefined = undefined
779
+ let limit: number | undefined = undefined
780
+ const range = {
781
+ start: url.searchParams.get("start"),
782
+ end: url.searchParams.get("end"),
783
+ }
784
+ if (range.start != null) {
785
+ const filePathURI = part.url.split("?")[0]
786
+ let start = parseInt(range.start)
787
+ let end = range.end ? parseInt(range.end) : undefined
788
+ // some LSP servers (eg, gopls) don't give full range in
789
+ // workspace/symbol searches, so we'll try to find the
790
+ // symbol in the document to get the full range
791
+ if (start === end) {
792
+ const symbols = await LSP.documentSymbol(filePathURI)
793
+ for (const symbol of symbols) {
794
+ let range: LSP.Range | undefined
795
+ if ("range" in symbol) {
796
+ range = symbol.range
797
+ } else if ("location" in symbol) {
798
+ range = symbol.location.range
799
+ }
800
+ if (range?.start?.line && range?.start?.line === start) {
801
+ start = range.start.line
802
+ end = range?.end?.line ?? start
803
+ break
804
+ }
805
+ }
806
+ }
807
+ offset = Math.max(start - 1, 0)
808
+ if (end) {
809
+ limit = end - offset
810
+ }
811
+ }
812
+ const args = { filePath: filepath, offset, limit }
813
+
814
+ const pieces: MessageV2.Part[] = [
815
+ {
816
+ id: Identifier.ascending("part"),
817
+ messageID: info.id,
818
+ sessionID: input.sessionID,
819
+ type: "text",
820
+ synthetic: true,
821
+ text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
822
+ },
823
+ ]
824
+
825
+ await ReadTool.init()
826
+ .then(async (t) => {
827
+ const model = await Provider.getModel(info.model.providerID, info.model.modelID)
828
+ const result = await t.execute(args, {
829
+ sessionID: input.sessionID,
830
+ abort: new AbortController().signal,
831
+ agent: input.agent!,
832
+ messageID: info.id,
833
+ extra: { bypassCwdCheck: true, model },
834
+ metadata: async () => { },
835
+ })
836
+ pieces.push({
837
+ id: Identifier.ascending("part"),
838
+ messageID: info.id,
839
+ sessionID: input.sessionID,
840
+ type: "text",
841
+ synthetic: true,
842
+ text: result.output,
843
+ })
844
+ if (result.attachments?.length) {
845
+ pieces.push(
846
+ ...result.attachments.map((attachment) => ({
847
+ ...attachment,
848
+ synthetic: true,
849
+ filename: attachment.filename ?? part.filename,
850
+ messageID: info.id,
851
+ sessionID: input.sessionID,
852
+ })),
853
+ )
854
+ } else {
855
+ pieces.push({
856
+ ...part,
857
+ id: part.id ?? Identifier.ascending("part"),
858
+ messageID: info.id,
859
+ sessionID: input.sessionID,
860
+ })
861
+ }
862
+ })
863
+ .catch((error) => {
864
+ log.error("failed to read file", { error })
865
+ const message = error instanceof Error ? error.message : error.toString()
866
+ Bus.publish(Session.Event.Error, {
867
+ sessionID: input.sessionID,
868
+ error: new NamedError.Unknown({
869
+ message,
870
+ }).toObject(),
871
+ })
872
+ pieces.push({
873
+ id: Identifier.ascending("part"),
874
+ messageID: info.id,
875
+ sessionID: input.sessionID,
876
+ type: "text",
877
+ synthetic: true,
878
+ text: `Read tool failed to read ${filepath} with the following error: ${message}`,
879
+ })
880
+ })
881
+
882
+ return pieces
883
+ }
884
+
885
+ if (part.mime === "application/x-directory") {
886
+ const args = { path: filepath }
887
+ const result = await ListTool.init().then((t) =>
888
+ t.execute(args, {
889
+ sessionID: input.sessionID,
890
+ abort: new AbortController().signal,
891
+ agent: input.agent!,
892
+ messageID: info.id,
893
+ extra: { bypassCwdCheck: true },
894
+ metadata: async () => { },
895
+ }),
896
+ )
897
+ return [
898
+ {
899
+ id: Identifier.ascending("part"),
900
+ messageID: info.id,
901
+ sessionID: input.sessionID,
902
+ type: "text",
903
+ synthetic: true,
904
+ text: `Called the list tool with the following input: ${JSON.stringify(args)}`,
905
+ },
906
+ {
907
+ id: Identifier.ascending("part"),
908
+ messageID: info.id,
909
+ sessionID: input.sessionID,
910
+ type: "text",
911
+ synthetic: true,
912
+ text: result.output,
913
+ },
914
+ {
915
+ ...part,
916
+ id: part.id ?? Identifier.ascending("part"),
917
+ messageID: info.id,
918
+ sessionID: input.sessionID,
919
+ },
920
+ ]
921
+ }
922
+
923
+ const file = Bun.file(filepath)
924
+ FileTime.read(input.sessionID, filepath)
925
+ return [
926
+ {
927
+ id: Identifier.ascending("part"),
928
+ messageID: info.id,
929
+ sessionID: input.sessionID,
930
+ type: "text",
931
+ text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`,
932
+ synthetic: true,
933
+ },
934
+ {
935
+ id: part.id ?? Identifier.ascending("part"),
936
+ messageID: info.id,
937
+ sessionID: input.sessionID,
938
+ type: "file",
939
+ url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
940
+ mime: part.mime,
941
+ filename: part.filename!,
942
+ source: part.source,
943
+ },
944
+ ]
945
+ }
946
+ }
947
+
948
+ if (part.type === "agent") {
949
+ return [
950
+ {
951
+ id: Identifier.ascending("part"),
952
+ ...part,
953
+ messageID: info.id,
954
+ sessionID: input.sessionID,
955
+ },
956
+ {
957
+ id: Identifier.ascending("part"),
958
+ messageID: info.id,
959
+ sessionID: input.sessionID,
960
+ type: "text",
961
+ synthetic: true,
962
+ text:
963
+ "Use the above message and context to generate a prompt and call the task tool with subagent: " +
964
+ part.name,
965
+ },
966
+ ]
967
+ }
968
+
969
+ return [
970
+ {
971
+ id: Identifier.ascending("part"),
972
+ ...part,
973
+ messageID: info.id,
974
+ sessionID: input.sessionID,
975
+ },
976
+ ]
977
+ }),
978
+ ).then((x) => x.flat())
979
+
980
+ await Plugin.trigger(
981
+ "chat.message",
982
+ {
983
+ sessionID: input.sessionID,
984
+ agent: input.agent,
985
+ model: input.model,
986
+ messageID: input.messageID,
987
+ },
988
+ {
989
+ message: info,
990
+ parts,
991
+ },
992
+ )
993
+
994
+ await Session.updateMessage(info)
995
+ for (const part of parts) {
996
+ await Session.updatePart(part)
997
+ }
998
+
999
+ return {
1000
+ info,
1001
+ parts,
1002
+ }
1003
+ }
1004
+
1005
+ function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info }) {
1006
+ const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
1007
+ if (!userMessage) return input.messages
1008
+ if (input.agent.name === "plan") {
1009
+ userMessage.parts.push({
1010
+ id: Identifier.ascending("part"),
1011
+ messageID: userMessage.info.id,
1012
+ sessionID: userMessage.info.sessionID,
1013
+ type: "text",
1014
+ // TODO (for mr dax): update to use the anthropic full fledged one (see plan-reminder-anthropic.txt)
1015
+ text: PROMPT_PLAN,
1016
+ synthetic: true,
1017
+ })
1018
+ }
1019
+ const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
1020
+ if (wasPlan && input.agent.name === "build") {
1021
+ userMessage.parts.push({
1022
+ id: Identifier.ascending("part"),
1023
+ messageID: userMessage.info.id,
1024
+ sessionID: userMessage.info.sessionID,
1025
+ type: "text",
1026
+ text: BUILD_SWITCH,
1027
+ synthetic: true,
1028
+ })
1029
+ }
1030
+ return input.messages
1031
+ }
1032
+
1033
+ export const ShellInput = z.object({
1034
+ sessionID: Identifier.schema("session"),
1035
+ agent: z.string(),
1036
+ model: z
1037
+ .object({
1038
+ providerID: z.string(),
1039
+ modelID: z.string(),
1040
+ })
1041
+ .optional(),
1042
+ command: z.string(),
1043
+ })
1044
+ export type ShellInput = z.infer<typeof ShellInput>
1045
+ export async function shell(input: ShellInput) {
1046
+ const abort = start(input.sessionID)
1047
+ if (!abort) {
1048
+ throw new Session.BusyError(input.sessionID)
1049
+ }
1050
+ using _ = defer(() => cancel(input.sessionID))
1051
+
1052
+ const session = await Session.get(input.sessionID)
1053
+ if (session.revert) {
1054
+ SessionRevert.cleanup(session)
1055
+ }
1056
+ const agent = await Agent.get(input.agent)
1057
+ const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
1058
+ const userMsg: MessageV2.User = {
1059
+ id: Identifier.ascending("message"),
1060
+ sessionID: input.sessionID,
1061
+ time: {
1062
+ created: Date.now(),
1063
+ },
1064
+ role: "user",
1065
+ agent: input.agent,
1066
+ model: {
1067
+ providerID: model.providerID,
1068
+ modelID: model.modelID,
1069
+ },
1070
+ }
1071
+ await Session.updateMessage(userMsg)
1072
+ const userPart: MessageV2.Part = {
1073
+ type: "text",
1074
+ id: Identifier.ascending("part"),
1075
+ messageID: userMsg.id,
1076
+ sessionID: input.sessionID,
1077
+ text: "The following tool was executed by the user",
1078
+ synthetic: true,
1079
+ }
1080
+ await Session.updatePart(userPart)
1081
+
1082
+ const msg: MessageV2.Assistant = {
1083
+ id: Identifier.ascending("message"),
1084
+ sessionID: input.sessionID,
1085
+ parentID: userMsg.id,
1086
+ mode: input.agent,
1087
+ agent: input.agent,
1088
+ cost: 0,
1089
+ path: {
1090
+ cwd: Instance.directory,
1091
+ root: Instance.worktree,
1092
+ },
1093
+ time: {
1094
+ created: Date.now(),
1095
+ },
1096
+ role: "assistant",
1097
+ tokens: {
1098
+ input: 0,
1099
+ output: 0,
1100
+ reasoning: 0,
1101
+ cache: { read: 0, write: 0 },
1102
+ },
1103
+ modelID: model.modelID,
1104
+ providerID: model.providerID,
1105
+ }
1106
+ await Session.updateMessage(msg)
1107
+ const part: MessageV2.Part = {
1108
+ type: "tool",
1109
+ id: Identifier.ascending("part"),
1110
+ messageID: msg.id,
1111
+ sessionID: input.sessionID,
1112
+ tool: "bash",
1113
+ callID: ulid(),
1114
+ state: {
1115
+ status: "running",
1116
+ time: {
1117
+ start: Date.now(),
1118
+ },
1119
+ input: {
1120
+ command: input.command,
1121
+ },
1122
+ },
1123
+ }
1124
+ await Session.updatePart(part)
1125
+ const shell = Shell.preferred()
1126
+ const shellName = (
1127
+ process.platform === "win32" ? path.win32.basename(shell, ".exe") : path.basename(shell)
1128
+ ).toLowerCase()
1129
+
1130
+ const invocations: Record<string, { args: string[] }> = {
1131
+ nu: {
1132
+ args: ["-c", input.command],
1133
+ },
1134
+ fish: {
1135
+ args: ["-c", input.command],
1136
+ },
1137
+ zsh: {
1138
+ args: [
1139
+ "-c",
1140
+ "-l",
1141
+ `
1142
+ [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
1143
+ [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
1144
+ eval ${JSON.stringify(input.command)}
1145
+ `,
1146
+ ],
1147
+ },
1148
+ bash: {
1149
+ args: [
1150
+ "-c",
1151
+ "-l",
1152
+ `
1153
+ shopt -s expand_aliases
1154
+ [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
1155
+ eval ${JSON.stringify(input.command)}
1156
+ `,
1157
+ ],
1158
+ },
1159
+ // Windows cmd
1160
+ cmd: {
1161
+ args: ["/c", input.command],
1162
+ },
1163
+ // Windows PowerShell
1164
+ powershell: {
1165
+ args: ["-NoProfile", "-Command", input.command],
1166
+ },
1167
+ pwsh: {
1168
+ args: ["-NoProfile", "-Command", input.command],
1169
+ },
1170
+ // Fallback: any shell that doesn't match those above
1171
+ // - No -l, for max compatibility
1172
+ "": {
1173
+ args: ["-c", `${input.command}`],
1174
+ },
1175
+ }
1176
+
1177
+ const matchingInvocation = invocations[shellName] ?? invocations[""]
1178
+ const args = matchingInvocation?.args
1179
+
1180
+ const proc = spawn(shell, args, {
1181
+ cwd: Instance.directory,
1182
+ detached: process.platform !== "win32",
1183
+ stdio: ["ignore", "pipe", "pipe"],
1184
+ env: {
1185
+ ...process.env,
1186
+ TERM: "dumb",
1187
+ },
1188
+ })
1189
+
1190
+ let output = ""
1191
+
1192
+ proc.stdout?.on("data", (chunk) => {
1193
+ output += chunk.toString()
1194
+ if (part.state.status === "running") {
1195
+ part.state.metadata = {
1196
+ output: output,
1197
+ description: "",
1198
+ }
1199
+ Session.updatePart(part)
1200
+ }
1201
+ })
1202
+
1203
+ proc.stderr?.on("data", (chunk) => {
1204
+ output += chunk.toString()
1205
+ if (part.state.status === "running") {
1206
+ part.state.metadata = {
1207
+ output: output,
1208
+ description: "",
1209
+ }
1210
+ Session.updatePart(part)
1211
+ }
1212
+ })
1213
+
1214
+ let aborted = false
1215
+ let exited = false
1216
+
1217
+ const kill = () => Shell.killTree(proc, { exited: () => exited })
1218
+
1219
+ if (abort.aborted) {
1220
+ aborted = true
1221
+ await kill()
1222
+ }
1223
+
1224
+ const abortHandler = () => {
1225
+ aborted = true
1226
+ void kill()
1227
+ }
1228
+
1229
+ abort.addEventListener("abort", abortHandler, { once: true })
1230
+
1231
+ await new Promise<void>((resolve) => {
1232
+ proc.on("close", () => {
1233
+ exited = true
1234
+ abort.removeEventListener("abort", abortHandler)
1235
+ resolve()
1236
+ })
1237
+ })
1238
+
1239
+ if (aborted) {
1240
+ output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
1241
+ }
1242
+ msg.time.completed = Date.now()
1243
+ await Session.updateMessage(msg)
1244
+ if (part.state.status === "running") {
1245
+ part.state = {
1246
+ status: "completed",
1247
+ time: {
1248
+ ...part.state.time,
1249
+ end: Date.now(),
1250
+ },
1251
+ input: part.state.input,
1252
+ title: "",
1253
+ metadata: {
1254
+ output,
1255
+ description: "",
1256
+ },
1257
+ output,
1258
+ }
1259
+ await Session.updatePart(part)
1260
+ }
1261
+ return { info: msg, parts: [part] }
1262
+ }
1263
+
1264
+ export const CommandInput = z.object({
1265
+ messageID: Identifier.schema("message").optional(),
1266
+ sessionID: Identifier.schema("session"),
1267
+ agent: z.string().optional(),
1268
+ model: z.string().optional(),
1269
+ arguments: z.string(),
1270
+ command: z.string(),
1271
+ })
1272
+ export type CommandInput = z.infer<typeof CommandInput>
1273
+ const bashRegex = /!`([^`]+)`/g
1274
+ const argsRegex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g
1275
+ const placeholderRegex = /\$(\d+)/g
1276
+ const quoteTrimRegex = /^["']|["']$/g
1277
+ /**
1278
+ * Regular expression to match @ file references in text
1279
+ * Matches @ followed by file paths, excluding commas, periods at end of sentences, and backticks
1280
+ * Does not match when preceded by word characters or backticks (to avoid email addresses and quoted references)
1281
+ */
1282
+
1283
+ export async function command(input: CommandInput) {
1284
+ log.info("command", input)
1285
+ const command = await Command.get(input.command)
1286
+ const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())
1287
+
1288
+ const raw = input.arguments.match(argsRegex) ?? []
1289
+ const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
1290
+
1291
+ const placeholders = command.template.match(placeholderRegex) ?? []
1292
+ let last = 0
1293
+ for (const item of placeholders) {
1294
+ const value = Number(item.slice(1))
1295
+ if (value > last) last = value
1296
+ }
1297
+
1298
+ // Let the final placeholder swallow any extra arguments so prompts read naturally
1299
+ const withArgs = command.template.replaceAll(placeholderRegex, (_, index) => {
1300
+ const position = Number(index)
1301
+ const argIndex = position - 1
1302
+ if (argIndex >= args.length) return ""
1303
+ if (position === last) return args.slice(argIndex).join(" ")
1304
+ return args[argIndex]
1305
+ })
1306
+ let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
1307
+
1308
+ const shell = ConfigMarkdown.shell(template)
1309
+ if (shell.length > 0) {
1310
+ const results = await Promise.all(
1311
+ shell.map(async ([, cmd]) => {
1312
+ try {
1313
+ return await $`${{ raw: cmd }}`.quiet().nothrow().text()
1314
+ } catch (error) {
1315
+ return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
1316
+ }
1317
+ }),
1318
+ )
1319
+ let index = 0
1320
+ template = template.replace(bashRegex, () => results[index++])
1321
+ }
1322
+ template = template.trim()
1323
+
1324
+ const model = await (async () => {
1325
+ if (command.model) {
1326
+ return Provider.parseModel(command.model)
1327
+ }
1328
+ if (command.agent) {
1329
+ const cmdAgent = await Agent.get(command.agent)
1330
+ if (cmdAgent.model) {
1331
+ return cmdAgent.model
1332
+ }
1333
+ }
1334
+ if (input.model) return Provider.parseModel(input.model)
1335
+ return await lastModel(input.sessionID)
1336
+ })()
1337
+
1338
+ try {
1339
+ await Provider.getModel(model.providerID, model.modelID)
1340
+ } catch (e) {
1341
+ if (Provider.ModelNotFoundError.isInstance(e)) {
1342
+ const { providerID, modelID, suggestions } = e.data
1343
+ const hint = suggestions?.length ? ` Did you mean: ${suggestions.join(", ")}?` : ""
1344
+ Bus.publish(Session.Event.Error, {
1345
+ sessionID: input.sessionID,
1346
+ error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(),
1347
+ })
1348
+ }
1349
+ throw e
1350
+ }
1351
+ const agent = await Agent.get(agentName)
1352
+
1353
+ const parts =
1354
+ (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true
1355
+ ? [
1356
+ {
1357
+ type: "subtask" as const,
1358
+ agent: agent.name,
1359
+ description: command.description ?? "",
1360
+ command: input.command,
1361
+ // TODO: how can we make task tool accept a more complex input?
1362
+ prompt: await resolvePromptParts(template).then((x) => x.find((y) => y.type === "text")?.text ?? ""),
1363
+ },
1364
+ ]
1365
+ : await resolvePromptParts(template)
1366
+
1367
+ const result = (await prompt({
1368
+ sessionID: input.sessionID,
1369
+ messageID: input.messageID,
1370
+ model,
1371
+ agent: agentName,
1372
+ parts,
1373
+ })) as MessageV2.WithParts
1374
+
1375
+ Bus.publish(Command.Event.Executed, {
1376
+ name: input.command,
1377
+ sessionID: input.sessionID,
1378
+ arguments: input.arguments,
1379
+ messageID: result.info.id,
1380
+ })
1381
+
1382
+ return result
1383
+ }
1384
+
1385
+ async function ensureTitle(input: {
1386
+ session: Session.Info
1387
+ message: MessageV2.WithParts
1388
+ history: MessageV2.WithParts[]
1389
+ providerID: string
1390
+ modelID: string
1391
+ }) {
1392
+ if (input.session.parentID) return
1393
+ if (!Session.isDefaultTitle(input.session.title)) return
1394
+ const isFirst =
1395
+ input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
1396
+ .length === 1
1397
+ if (!isFirst) return
1398
+ const agent = await Agent.get("title")
1399
+ if (!agent) return
1400
+ const result = await LLM.streamFromInput({
1401
+ agent,
1402
+ user: input.message.info as MessageV2.User,
1403
+ system: [],
1404
+ small: true,
1405
+ tools: {},
1406
+ model: await iife(async () => {
1407
+ if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
1408
+ return (
1409
+ (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
1410
+ )
1411
+ }),
1412
+ abort: new AbortController().signal,
1413
+ sessionID: input.session.id,
1414
+ retries: 2,
1415
+ messages: [
1416
+ {
1417
+ role: "user",
1418
+ content: "Generate a title for this conversation:\n",
1419
+ },
1420
+ ...MessageV2.toModelMessage([
1421
+ {
1422
+ info: {
1423
+ id: Identifier.ascending("message"),
1424
+ role: "user",
1425
+ sessionID: input.session.id,
1426
+ time: {
1427
+ created: Date.now(),
1428
+ },
1429
+ agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(),
1430
+ model: {
1431
+ providerID: input.providerID,
1432
+ modelID: input.modelID,
1433
+ },
1434
+ },
1435
+ parts: input.message.parts,
1436
+ },
1437
+ ]),
1438
+ ],
1439
+ })
1440
+ const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))
1441
+ if (text)
1442
+ return Session.update(input.session.id, (draft) => {
1443
+ const cleaned = text
1444
+ .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
1445
+ .split("\n")
1446
+ .map((line) => line.trim())
1447
+ .find((line) => line.length > 0)
1448
+ if (!cleaned) return
1449
+
1450
+ const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
1451
+ draft.title = title
1452
+ })
1453
+ }
1454
+ }