cerebras-cli 1.0.0

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