prompt.ts 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777
  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. streamText,
  15. type ModelMessage,
  16. type Tool as AITool,
  17. tool,
  18. wrapLanguageModel,
  19. type StreamTextResult,
  20. stepCountIs,
  21. jsonSchema,
  22. } from "ai"
  23. import { SessionCompaction } from "./compaction"
  24. import { SessionLock } from "./lock"
  25. import { Instance } from "../project/instance"
  26. import { Bus } from "../bus"
  27. import { ProviderTransform } from "../provider/transform"
  28. import { SystemPrompt } from "./system"
  29. import { Plugin } from "../plugin"
  30. import { SessionRetry } from "./retry"
  31. import PROMPT_PLAN from "../session/prompt/plan.txt"
  32. import BUILD_SWITCH from "../session/prompt/build-switch.txt"
  33. import { ModelsDev } from "../provider/models"
  34. import { defer } from "../util/defer"
  35. import { mergeDeep, pipe } from "remeda"
  36. import { ToolRegistry } from "../tool/registry"
  37. import { Wildcard } from "../util/wildcard"
  38. import { MCP } from "../mcp"
  39. import { LSP } from "../lsp"
  40. import { ReadTool } from "../tool/read"
  41. import { ListTool } from "../tool/ls"
  42. import { TaskTool } from "../tool/task"
  43. import { FileTime } from "../file/time"
  44. import { Permission } from "../permission"
  45. import { Snapshot } from "../snapshot"
  46. import { ulid } from "ulid"
  47. import { spawn } from "child_process"
  48. import { Command } from "../command"
  49. import { $, fileURLToPath } from "bun"
  50. import { ConfigMarkdown } from "../config/markdown"
  51. import { SessionSummary } from "./summary"
  52. export namespace SessionPrompt {
  53. const log = Log.create({ service: "session.prompt" })
  54. export const OUTPUT_TOKEN_MAX = 32_000
  55. const MAX_RETRIES = 10
  56. export const Event = {
  57. Idle: Bus.event(
  58. "session.idle",
  59. z.object({
  60. sessionID: z.string(),
  61. }),
  62. ),
  63. }
  64. const state = Instance.state(
  65. () => {
  66. const queued = new Map<
  67. string,
  68. {
  69. messageID: string
  70. callback: (input: MessageV2.WithParts) => void
  71. }[]
  72. >()
  73. return {
  74. queued,
  75. }
  76. },
  77. async (current) => {
  78. current.queued.clear()
  79. },
  80. )
  81. export const PromptInput = z.object({
  82. sessionID: Identifier.schema("session"),
  83. messageID: Identifier.schema("message").optional(),
  84. model: z
  85. .object({
  86. providerID: z.string(),
  87. modelID: z.string(),
  88. })
  89. .optional(),
  90. agent: z.string().optional(),
  91. noReply: z.boolean().optional(),
  92. system: z.string().optional(),
  93. tools: z.record(z.string(), z.boolean()).optional(),
  94. parts: z.array(
  95. z.discriminatedUnion("type", [
  96. MessageV2.TextPart.omit({
  97. messageID: true,
  98. sessionID: true,
  99. })
  100. .partial({
  101. id: true,
  102. })
  103. .meta({
  104. ref: "TextPartInput",
  105. }),
  106. MessageV2.FilePart.omit({
  107. messageID: true,
  108. sessionID: true,
  109. })
  110. .partial({
  111. id: true,
  112. })
  113. .meta({
  114. ref: "FilePartInput",
  115. }),
  116. MessageV2.AgentPart.omit({
  117. messageID: true,
  118. sessionID: true,
  119. })
  120. .partial({
  121. id: true,
  122. })
  123. .meta({
  124. ref: "AgentPartInput",
  125. }),
  126. ]),
  127. ),
  128. })
  129. export type PromptInput = z.infer<typeof PromptInput>
  130. export async function prompt(input: PromptInput): Promise<MessageV2.WithParts> {
  131. const l = log.clone().tag("session", input.sessionID)
  132. l.info("prompt")
  133. const session = await Session.get(input.sessionID)
  134. await SessionRevert.cleanup(session)
  135. const userMsg = await createUserMessage(input)
  136. await Session.touch(input.sessionID)
  137. // Early return for context-only messages (no AI inference)
  138. if (input.noReply) {
  139. return userMsg
  140. }
  141. if (isBusy(input.sessionID)) {
  142. return new Promise((resolve) => {
  143. const queue = state().queued.get(input.sessionID) ?? []
  144. queue.push({
  145. messageID: userMsg.info.id,
  146. callback: resolve,
  147. })
  148. state().queued.set(input.sessionID, queue)
  149. })
  150. }
  151. const agent = await Agent.get(input.agent ?? "build")
  152. const model = await resolveModel({
  153. agent,
  154. model: input.model,
  155. }).then((x) => Provider.getModel(x.providerID, x.modelID))
  156. using abort = lock(input.sessionID)
  157. const system = await resolveSystemPrompt({
  158. providerID: model.providerID,
  159. modelID: model.info.id,
  160. agent,
  161. system: input.system,
  162. })
  163. const processor = await createProcessor({
  164. sessionID: input.sessionID,
  165. model: model.info,
  166. providerID: model.providerID,
  167. agent: agent.name,
  168. system,
  169. abort: abort.signal,
  170. })
  171. const tools = await resolveTools({
  172. agent,
  173. sessionID: input.sessionID,
  174. modelID: model.modelID,
  175. providerID: model.providerID,
  176. tools: input.tools,
  177. processor,
  178. })
  179. // const permUnsub = (() => {
  180. // const handled = new Set<string>()
  181. // const options = [
  182. // { optionId: "allow_once", kind: "allow_once", name: "Allow once" },
  183. // { optionId: "allow_always", kind: "allow_always", name: "Always allow" },
  184. // { optionId: "reject_once", kind: "reject_once", name: "Reject" },
  185. // ]
  186. // return Bus.subscribe(Permission.Event.Updated, async (event) => {
  187. // const info = event.properties
  188. // if (info.sessionID !== input.sessionID) return
  189. // if (handled.has(info.id)) return
  190. // handled.add(info.id)
  191. // const toolCallId = info.callID ?? info.id
  192. // const metadata = info.metadata ?? {}
  193. // // TODO: emit permission event to bus for ACP to handle
  194. // Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" })
  195. // })
  196. // })()
  197. // await using _permSub = defer(() => {
  198. // permUnsub?.()
  199. // })
  200. const params = await Plugin.trigger(
  201. "chat.params",
  202. {
  203. model: model.info,
  204. provider: await Provider.getProvider(model.providerID),
  205. message: userMsg,
  206. },
  207. {
  208. temperature: model.info.temperature
  209. ? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
  210. : undefined,
  211. topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
  212. options: {
  213. ...ProviderTransform.options(model.providerID, model.modelID, input.sessionID),
  214. ...model.info.options,
  215. ...agent.options,
  216. },
  217. },
  218. )
  219. let step = 0
  220. while (true) {
  221. const msgs: MessageV2.WithParts[] = pipe(
  222. await getMessages({
  223. sessionID: input.sessionID,
  224. model: model.info,
  225. providerID: model.providerID,
  226. signal: abort.signal,
  227. }),
  228. (messages) => insertReminders({ messages, agent }),
  229. )
  230. step++
  231. await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!)
  232. if (step === 1) {
  233. ensureTitle({
  234. session,
  235. history: msgs,
  236. message: userMsg,
  237. providerID: model.providerID,
  238. modelID: model.info.id,
  239. })
  240. SessionSummary.summarize({
  241. sessionID: input.sessionID,
  242. messageID: userMsg.info.id,
  243. })
  244. }
  245. await using _ = defer(async () => {
  246. await processor.end()
  247. })
  248. const doStream = () =>
  249. streamText({
  250. onError(error) {
  251. log.error("stream error", {
  252. error,
  253. })
  254. },
  255. async experimental_repairToolCall(input) {
  256. const lower = input.toolCall.toolName.toLowerCase()
  257. if (lower !== input.toolCall.toolName && tools[lower]) {
  258. log.info("repairing tool call", {
  259. tool: input.toolCall.toolName,
  260. repaired: lower,
  261. })
  262. return {
  263. ...input.toolCall,
  264. toolName: lower,
  265. }
  266. }
  267. return {
  268. ...input.toolCall,
  269. input: JSON.stringify({
  270. tool: input.toolCall.toolName,
  271. error: input.error.message,
  272. }),
  273. toolName: "invalid",
  274. }
  275. },
  276. headers:
  277. model.providerID === "opencode"
  278. ? {
  279. "x-opencode-session": input.sessionID,
  280. "x-opencode-request": userMsg.info.id,
  281. }
  282. : undefined,
  283. // set to 0, we handle loop
  284. maxRetries: 0,
  285. activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
  286. maxOutputTokens: ProviderTransform.maxOutputTokens(
  287. model.providerID,
  288. params.options,
  289. model.info.limit.output,
  290. OUTPUT_TOKEN_MAX,
  291. ),
  292. abortSignal: abort.signal,
  293. providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
  294. stopWhen: stepCountIs(1),
  295. temperature: params.temperature,
  296. topP: params.topP,
  297. messages: [
  298. ...system.map(
  299. (x): ModelMessage => ({
  300. role: "system",
  301. content: x,
  302. }),
  303. ),
  304. ...MessageV2.toModelMessage(
  305. msgs.filter((m) => {
  306. if (m.info.role !== "assistant" || m.info.error === undefined) {
  307. return true
  308. }
  309. if (
  310. MessageV2.AbortedError.isInstance(m.info.error) &&
  311. m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
  312. ) {
  313. return true
  314. }
  315. return false
  316. }),
  317. ),
  318. ],
  319. tools: model.info.tool_call === false ? undefined : tools,
  320. model: wrapLanguageModel({
  321. model: model.language,
  322. middleware: [
  323. {
  324. async transformParams(args) {
  325. if (args.type === "stream") {
  326. // @ts-expect-error
  327. args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
  328. }
  329. return args.params
  330. },
  331. },
  332. ],
  333. }),
  334. })
  335. let stream = doStream()
  336. let result = await processor.process(stream, {
  337. count: 0,
  338. max: MAX_RETRIES,
  339. })
  340. if (result.shouldRetry) {
  341. for (let retry = 1; retry < MAX_RETRIES; retry++) {
  342. const lastRetryPart = result.parts.findLast((p) => p.type === "retry")
  343. if (lastRetryPart) {
  344. const delayMs = SessionRetry.getRetryDelayInMs(lastRetryPart.error, retry)
  345. log.info("retrying with backoff", {
  346. attempt: retry,
  347. delayMs,
  348. })
  349. const stop = await SessionRetry.sleep(delayMs, abort.signal)
  350. .then(() => false)
  351. .catch((error) => {
  352. if (error instanceof DOMException && error.name === "AbortError") {
  353. const err = new MessageV2.AbortedError(
  354. { message: error.message },
  355. {
  356. cause: error,
  357. },
  358. ).toObject()
  359. result.info.error = err
  360. Bus.publish(Session.Event.Error, {
  361. sessionID: result.info.sessionID,
  362. error: result.info.error,
  363. })
  364. return true
  365. }
  366. throw error
  367. })
  368. if (stop) break
  369. }
  370. stream = doStream()
  371. result = await processor.process(stream, {
  372. count: retry,
  373. max: MAX_RETRIES,
  374. })
  375. if (!result.shouldRetry) {
  376. break
  377. }
  378. }
  379. }
  380. await processor.end()
  381. const queued = state().queued.get(input.sessionID) ?? []
  382. if (!result.blocked && !result.info.error) {
  383. if ((await stream.finishReason) === "tool-calls") {
  384. continue
  385. }
  386. const unprocessed = queued.filter((x) => x.messageID > result.info.id)
  387. if (unprocessed.length) {
  388. continue
  389. }
  390. }
  391. for (const item of queued) {
  392. item.callback(result)
  393. }
  394. state().queued.delete(input.sessionID)
  395. SessionCompaction.prune(input)
  396. return result
  397. }
  398. }
  399. async function getMessages(input: {
  400. sessionID: string
  401. model: ModelsDev.Model
  402. providerID: string
  403. signal: AbortSignal
  404. }) {
  405. let msgs = await Session.messages(input.sessionID).then(MessageV2.filterCompacted)
  406. const lastAssistant = msgs.findLast((msg) => msg.info.role === "assistant")
  407. if (
  408. lastAssistant?.info.role === "assistant" &&
  409. SessionCompaction.isOverflow({
  410. tokens: lastAssistant.info.tokens,
  411. model: input.model,
  412. })
  413. ) {
  414. const summaryMsg = await SessionCompaction.run({
  415. sessionID: input.sessionID,
  416. providerID: input.providerID,
  417. modelID: input.model.id,
  418. signal: input.signal,
  419. })
  420. const resumeMsgID = Identifier.ascending("message")
  421. const resumeMsg = {
  422. info: await Session.updateMessage({
  423. id: resumeMsgID,
  424. role: "user",
  425. sessionID: input.sessionID,
  426. time: {
  427. created: Date.now(),
  428. },
  429. }),
  430. parts: [
  431. await Session.updatePart({
  432. type: "text",
  433. sessionID: input.sessionID,
  434. messageID: resumeMsgID,
  435. id: Identifier.ascending("part"),
  436. text: "Use the above summary generated from your last session to resume from where you left off.",
  437. time: {
  438. start: Date.now(),
  439. end: Date.now(),
  440. },
  441. synthetic: true,
  442. }),
  443. ],
  444. }
  445. msgs = [summaryMsg, resumeMsg]
  446. }
  447. return msgs
  448. }
  449. async function resolveModel(input: { model: PromptInput["model"]; agent: Agent.Info }) {
  450. if (input.model) {
  451. return input.model
  452. }
  453. if (input.agent.model) {
  454. return input.agent.model
  455. }
  456. return Provider.defaultModel()
  457. }
  458. async function resolveSystemPrompt(input: {
  459. system?: string
  460. agent: Agent.Info
  461. providerID: string
  462. modelID: string
  463. }) {
  464. let system = SystemPrompt.header(input.providerID)
  465. system.push(
  466. ...(() => {
  467. if (input.system) return [input.system]
  468. if (input.agent.prompt) return [input.agent.prompt]
  469. return SystemPrompt.provider(input.modelID)
  470. })(),
  471. )
  472. system.push(...(await SystemPrompt.environment()))
  473. system.push(...(await SystemPrompt.custom()))
  474. // max 2 system prompt messages for caching purposes
  475. const [first, ...rest] = system
  476. system = [first, rest.join("\n")]
  477. return system
  478. }
  479. async function resolveTools(input: {
  480. agent: Agent.Info
  481. sessionID: string
  482. modelID: string
  483. providerID: string
  484. tools?: Record<string, boolean>
  485. processor: Processor
  486. }) {
  487. const tools: Record<string, AITool> = {}
  488. const enabledTools = pipe(
  489. input.agent.tools,
  490. mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, input.agent)),
  491. mergeDeep(input.tools ?? {}),
  492. )
  493. for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
  494. if (Wildcard.all(item.id, enabledTools) === false) continue
  495. const schema = ProviderTransform.schema(input.providerID, input.modelID, z.toJSONSchema(item.parameters))
  496. tools[item.id] = tool({
  497. id: item.id as any,
  498. description: item.description,
  499. inputSchema: jsonSchema(schema as any),
  500. async execute(args, options) {
  501. await Plugin.trigger(
  502. "tool.execute.before",
  503. {
  504. tool: item.id,
  505. sessionID: input.sessionID,
  506. callID: options.toolCallId,
  507. },
  508. {
  509. args,
  510. },
  511. )
  512. const result = await item.execute(args, {
  513. sessionID: input.sessionID,
  514. abort: options.abortSignal!,
  515. messageID: input.processor.message.id,
  516. callID: options.toolCallId,
  517. extra: {
  518. modelID: input.modelID,
  519. providerID: input.providerID,
  520. },
  521. agent: input.agent.name,
  522. metadata: async (val) => {
  523. const match = input.processor.partFromToolCall(options.toolCallId)
  524. if (match && match.state.status === "running") {
  525. await Session.updatePart({
  526. ...match,
  527. state: {
  528. title: val.title,
  529. metadata: val.metadata,
  530. status: "running",
  531. input: args,
  532. time: {
  533. start: Date.now(),
  534. },
  535. },
  536. })
  537. }
  538. },
  539. })
  540. await Plugin.trigger(
  541. "tool.execute.after",
  542. {
  543. tool: item.id,
  544. sessionID: input.sessionID,
  545. callID: options.toolCallId,
  546. },
  547. result,
  548. )
  549. return result
  550. },
  551. toModelOutput(result) {
  552. return {
  553. type: "text",
  554. value: result.output,
  555. }
  556. },
  557. })
  558. }
  559. for (const [key, item] of Object.entries(await MCP.tools())) {
  560. if (Wildcard.all(key, enabledTools) === false) continue
  561. const execute = item.execute
  562. if (!execute) continue
  563. item.execute = async (args, opts) => {
  564. await Plugin.trigger(
  565. "tool.execute.before",
  566. {
  567. tool: key,
  568. sessionID: input.sessionID,
  569. callID: opts.toolCallId,
  570. },
  571. {
  572. args,
  573. },
  574. )
  575. const result = await execute(args, opts)
  576. await Plugin.trigger(
  577. "tool.execute.after",
  578. {
  579. tool: key,
  580. sessionID: input.sessionID,
  581. callID: opts.toolCallId,
  582. },
  583. result,
  584. )
  585. const output = result.content
  586. .filter((x: any) => x.type === "text")
  587. .map((x: any) => x.text)
  588. .join("\n\n")
  589. return {
  590. title: "",
  591. metadata: {},
  592. output,
  593. }
  594. }
  595. item.toModelOutput = (result) => {
  596. return {
  597. type: "text",
  598. value: result.output,
  599. }
  600. }
  601. tools[key] = item
  602. }
  603. return tools
  604. }
  605. async function createUserMessage(input: PromptInput) {
  606. const info: MessageV2.Info = {
  607. id: input.messageID ?? Identifier.ascending("message"),
  608. role: "user",
  609. sessionID: input.sessionID,
  610. time: {
  611. created: Date.now(),
  612. },
  613. }
  614. const parts = await Promise.all(
  615. input.parts.map(async (part): Promise<MessageV2.Part[]> => {
  616. if (part.type === "file") {
  617. const url = new URL(part.url)
  618. switch (url.protocol) {
  619. case "data:":
  620. if (part.mime === "text/plain") {
  621. return [
  622. {
  623. id: Identifier.ascending("part"),
  624. messageID: info.id,
  625. sessionID: input.sessionID,
  626. type: "text",
  627. synthetic: true,
  628. text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
  629. },
  630. {
  631. id: Identifier.ascending("part"),
  632. messageID: info.id,
  633. sessionID: input.sessionID,
  634. type: "text",
  635. synthetic: true,
  636. text: Buffer.from(part.url, "base64url").toString(),
  637. },
  638. {
  639. ...part,
  640. id: part.id ?? Identifier.ascending("part"),
  641. messageID: info.id,
  642. sessionID: input.sessionID,
  643. },
  644. ]
  645. }
  646. break
  647. case "file:":
  648. log.info("file", { mime: part.mime })
  649. // have to normalize, symbol search returns absolute paths
  650. // Decode the pathname since URL constructor doesn't automatically decode it
  651. const filepath = fileURLToPath(part.url)
  652. const stat = await Bun.file(filepath).stat()
  653. if (stat.isDirectory()) {
  654. part.mime = "application/x-directory"
  655. }
  656. if (part.mime === "text/plain") {
  657. let offset: number | undefined = undefined
  658. let limit: number | undefined = undefined
  659. const range = {
  660. start: url.searchParams.get("start"),
  661. end: url.searchParams.get("end"),
  662. }
  663. if (range.start != null) {
  664. const filePathURI = part.url.split("?")[0]
  665. let start = parseInt(range.start)
  666. let end = range.end ? parseInt(range.end) : undefined
  667. // some LSP servers (eg, gopls) don't give full range in
  668. // workspace/symbol searches, so we'll try to find the
  669. // symbol in the document to get the full range
  670. if (start === end) {
  671. const symbols = await LSP.documentSymbol(filePathURI)
  672. for (const symbol of symbols) {
  673. let range: LSP.Range | undefined
  674. if ("range" in symbol) {
  675. range = symbol.range
  676. } else if ("location" in symbol) {
  677. range = symbol.location.range
  678. }
  679. if (range?.start?.line && range?.start?.line === start) {
  680. start = range.start.line
  681. end = range?.end?.line ?? start
  682. break
  683. }
  684. }
  685. }
  686. offset = Math.max(start - 1, 0)
  687. if (end) {
  688. limit = end - offset
  689. }
  690. }
  691. const args = { filePath: filepath, offset, limit }
  692. const result = await ReadTool.init().then((t) =>
  693. t.execute(args, {
  694. sessionID: input.sessionID,
  695. abort: new AbortController().signal,
  696. agent: input.agent!,
  697. messageID: info.id,
  698. extra: { bypassCwdCheck: true },
  699. metadata: async () => {},
  700. }),
  701. )
  702. return [
  703. {
  704. id: Identifier.ascending("part"),
  705. messageID: info.id,
  706. sessionID: input.sessionID,
  707. type: "text",
  708. synthetic: true,
  709. text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
  710. },
  711. {
  712. id: Identifier.ascending("part"),
  713. messageID: info.id,
  714. sessionID: input.sessionID,
  715. type: "text",
  716. synthetic: true,
  717. text: result.output,
  718. },
  719. {
  720. ...part,
  721. id: part.id ?? Identifier.ascending("part"),
  722. messageID: info.id,
  723. sessionID: input.sessionID,
  724. },
  725. ]
  726. }
  727. if (part.mime === "application/x-directory") {
  728. const args = { path: filepath }
  729. const result = await ListTool.init().then((t) =>
  730. t.execute(args, {
  731. sessionID: input.sessionID,
  732. abort: new AbortController().signal,
  733. agent: input.agent!,
  734. messageID: info.id,
  735. extra: { bypassCwdCheck: true },
  736. metadata: async () => {},
  737. }),
  738. )
  739. return [
  740. {
  741. id: Identifier.ascending("part"),
  742. messageID: info.id,
  743. sessionID: input.sessionID,
  744. type: "text",
  745. synthetic: true,
  746. text: `Called the list tool with the following input: ${JSON.stringify(args)}`,
  747. },
  748. {
  749. id: Identifier.ascending("part"),
  750. messageID: info.id,
  751. sessionID: input.sessionID,
  752. type: "text",
  753. synthetic: true,
  754. text: result.output,
  755. },
  756. {
  757. ...part,
  758. id: part.id ?? Identifier.ascending("part"),
  759. messageID: info.id,
  760. sessionID: input.sessionID,
  761. },
  762. ]
  763. }
  764. const file = Bun.file(filepath)
  765. FileTime.read(input.sessionID, filepath)
  766. return [
  767. {
  768. id: Identifier.ascending("part"),
  769. messageID: info.id,
  770. sessionID: input.sessionID,
  771. type: "text",
  772. text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`,
  773. synthetic: true,
  774. },
  775. {
  776. id: part.id ?? Identifier.ascending("part"),
  777. messageID: info.id,
  778. sessionID: input.sessionID,
  779. type: "file",
  780. url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
  781. mime: part.mime,
  782. filename: part.filename!,
  783. source: part.source,
  784. },
  785. ]
  786. }
  787. }
  788. if (part.type === "agent") {
  789. return [
  790. {
  791. id: Identifier.ascending("part"),
  792. ...part,
  793. messageID: info.id,
  794. sessionID: input.sessionID,
  795. },
  796. {
  797. id: Identifier.ascending("part"),
  798. messageID: info.id,
  799. sessionID: input.sessionID,
  800. type: "text",
  801. synthetic: true,
  802. text:
  803. "Use the above message and context to generate a prompt and call the task tool with subagent: " +
  804. part.name,
  805. },
  806. ]
  807. }
  808. return [
  809. {
  810. id: Identifier.ascending("part"),
  811. ...part,
  812. messageID: info.id,
  813. sessionID: input.sessionID,
  814. },
  815. ]
  816. }),
  817. ).then((x) => x.flat())
  818. await Plugin.trigger(
  819. "chat.message",
  820. {},
  821. {
  822. message: info,
  823. parts,
  824. },
  825. )
  826. await Session.updateMessage(info)
  827. for (const part of parts) {
  828. await Session.updatePart(part)
  829. }
  830. return {
  831. info,
  832. parts,
  833. }
  834. }
  835. function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info }) {
  836. const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
  837. if (!userMessage) return input.messages
  838. if (input.agent.name === "plan") {
  839. userMessage.parts.push({
  840. id: Identifier.ascending("part"),
  841. messageID: userMessage.info.id,
  842. sessionID: userMessage.info.sessionID,
  843. type: "text",
  844. text: PROMPT_PLAN,
  845. synthetic: true,
  846. })
  847. }
  848. const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan")
  849. if (wasPlan && input.agent.name === "build") {
  850. userMessage.parts.push({
  851. id: Identifier.ascending("part"),
  852. messageID: userMessage.info.id,
  853. sessionID: userMessage.info.sessionID,
  854. type: "text",
  855. text: BUILD_SWITCH,
  856. synthetic: true,
  857. })
  858. }
  859. return input.messages
  860. }
  861. export type Processor = Awaited<ReturnType<typeof createProcessor>>
  862. async function createProcessor(input: {
  863. sessionID: string
  864. providerID: string
  865. model: ModelsDev.Model
  866. system: string[]
  867. agent: string
  868. abort: AbortSignal
  869. }) {
  870. const toolcalls: Record<string, MessageV2.ToolPart> = {}
  871. let snapshot: string | undefined
  872. let blocked = false
  873. async function createMessage(parentID: string) {
  874. const msg: MessageV2.Info = {
  875. id: Identifier.ascending("message"),
  876. parentID,
  877. role: "assistant",
  878. system: input.system,
  879. mode: input.agent,
  880. path: {
  881. cwd: Instance.directory,
  882. root: Instance.worktree,
  883. },
  884. cost: 0,
  885. tokens: {
  886. input: 0,
  887. output: 0,
  888. reasoning: 0,
  889. cache: { read: 0, write: 0 },
  890. },
  891. modelID: input.model.id,
  892. providerID: input.providerID,
  893. time: {
  894. created: Date.now(),
  895. },
  896. sessionID: input.sessionID,
  897. }
  898. await Session.updateMessage(msg)
  899. return msg
  900. }
  901. let assistantMsg: MessageV2.Assistant | undefined
  902. const result = {
  903. async end() {
  904. if (assistantMsg) {
  905. assistantMsg.time.completed = Date.now()
  906. await Session.updateMessage(assistantMsg)
  907. assistantMsg = undefined
  908. }
  909. },
  910. async next(parentID: string) {
  911. if (assistantMsg) {
  912. throw new Error("end previous assistant message first")
  913. }
  914. assistantMsg = await createMessage(parentID)
  915. return assistantMsg
  916. },
  917. get message() {
  918. if (!assistantMsg) throw new Error("call next() first before accessing message")
  919. return assistantMsg
  920. },
  921. partFromToolCall(toolCallID: string) {
  922. return toolcalls[toolCallID]
  923. },
  924. async process(stream: StreamTextResult<Record<string, AITool>, never>, retries: { count: number; max: number }) {
  925. log.info("process")
  926. if (!assistantMsg) throw new Error("call next() first before processing")
  927. let shouldRetry = false
  928. try {
  929. let currentText: MessageV2.TextPart | undefined
  930. let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
  931. for await (const value of stream.fullStream) {
  932. input.abort.throwIfAborted()
  933. log.info("part", {
  934. type: value.type,
  935. })
  936. switch (value.type) {
  937. case "start":
  938. break
  939. case "reasoning-start":
  940. if (value.id in reasoningMap) {
  941. continue
  942. }
  943. reasoningMap[value.id] = {
  944. id: Identifier.ascending("part"),
  945. messageID: assistantMsg.id,
  946. sessionID: assistantMsg.sessionID,
  947. type: "reasoning",
  948. text: "",
  949. time: {
  950. start: Date.now(),
  951. },
  952. metadata: value.providerMetadata,
  953. }
  954. break
  955. case "reasoning-delta":
  956. if (value.id in reasoningMap) {
  957. const part = reasoningMap[value.id]
  958. part.text += value.text
  959. if (value.providerMetadata) part.metadata = value.providerMetadata
  960. if (part.text) await Session.updatePart({ part, delta: value.text })
  961. }
  962. break
  963. case "reasoning-end":
  964. if (value.id in reasoningMap) {
  965. const part = reasoningMap[value.id]
  966. part.text = part.text.trimEnd()
  967. part.time = {
  968. ...part.time,
  969. end: Date.now(),
  970. }
  971. if (value.providerMetadata) part.metadata = value.providerMetadata
  972. await Session.updatePart(part)
  973. delete reasoningMap[value.id]
  974. }
  975. break
  976. case "tool-input-start":
  977. const part = await Session.updatePart({
  978. id: toolcalls[value.id]?.id ?? Identifier.ascending("part"),
  979. messageID: assistantMsg.id,
  980. sessionID: assistantMsg.sessionID,
  981. type: "tool",
  982. tool: value.toolName,
  983. callID: value.id,
  984. state: {
  985. status: "pending",
  986. },
  987. })
  988. toolcalls[value.id] = part as MessageV2.ToolPart
  989. break
  990. case "tool-input-delta":
  991. break
  992. case "tool-input-end":
  993. break
  994. case "tool-call": {
  995. const match = toolcalls[value.toolCallId]
  996. if (match) {
  997. const part = await Session.updatePart({
  998. ...match,
  999. tool: value.toolName,
  1000. state: {
  1001. status: "running",
  1002. input: value.input,
  1003. time: {
  1004. start: Date.now(),
  1005. },
  1006. },
  1007. metadata: value.providerMetadata,
  1008. })
  1009. toolcalls[value.toolCallId] = part as MessageV2.ToolPart
  1010. }
  1011. break
  1012. }
  1013. case "tool-result": {
  1014. const match = toolcalls[value.toolCallId]
  1015. if (match && match.state.status === "running") {
  1016. await Session.updatePart({
  1017. ...match,
  1018. state: {
  1019. status: "completed",
  1020. input: value.input,
  1021. output: value.output.output,
  1022. metadata: value.output.metadata,
  1023. title: value.output.title,
  1024. time: {
  1025. start: match.state.time.start,
  1026. end: Date.now(),
  1027. },
  1028. attachments: value.output.attachments,
  1029. },
  1030. })
  1031. delete toolcalls[value.toolCallId]
  1032. }
  1033. break
  1034. }
  1035. case "tool-error": {
  1036. const match = toolcalls[value.toolCallId]
  1037. if (match && match.state.status === "running") {
  1038. await Session.updatePart({
  1039. ...match,
  1040. state: {
  1041. status: "error",
  1042. input: value.input,
  1043. error: (value.error as any).toString(),
  1044. metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
  1045. time: {
  1046. start: match.state.time.start,
  1047. end: Date.now(),
  1048. },
  1049. },
  1050. })
  1051. if (value.error instanceof Permission.RejectedError) {
  1052. blocked = true
  1053. }
  1054. delete toolcalls[value.toolCallId]
  1055. }
  1056. break
  1057. }
  1058. case "error":
  1059. throw value.error
  1060. case "start-step":
  1061. snapshot = await Snapshot.track()
  1062. await Session.updatePart({
  1063. id: Identifier.ascending("part"),
  1064. messageID: assistantMsg.id,
  1065. sessionID: assistantMsg.sessionID,
  1066. snapshot,
  1067. type: "step-start",
  1068. })
  1069. break
  1070. case "finish-step":
  1071. const usage = Session.getUsage({
  1072. model: input.model,
  1073. usage: value.usage,
  1074. metadata: value.providerMetadata,
  1075. })
  1076. assistantMsg.cost += usage.cost
  1077. assistantMsg.tokens = usage.tokens
  1078. await Session.updatePart({
  1079. id: Identifier.ascending("part"),
  1080. reason: value.finishReason,
  1081. snapshot: await Snapshot.track(),
  1082. messageID: assistantMsg.id,
  1083. sessionID: assistantMsg.sessionID,
  1084. type: "step-finish",
  1085. tokens: usage.tokens,
  1086. cost: usage.cost,
  1087. })
  1088. await Session.updateMessage(assistantMsg)
  1089. if (snapshot) {
  1090. const patch = await Snapshot.patch(snapshot)
  1091. if (patch.files.length) {
  1092. await Session.updatePart({
  1093. id: Identifier.ascending("part"),
  1094. messageID: assistantMsg.id,
  1095. sessionID: assistantMsg.sessionID,
  1096. type: "patch",
  1097. hash: patch.hash,
  1098. files: patch.files,
  1099. })
  1100. }
  1101. snapshot = undefined
  1102. }
  1103. SessionSummary.summarize({
  1104. sessionID: input.sessionID,
  1105. messageID: assistantMsg.parentID,
  1106. })
  1107. break
  1108. case "text-start":
  1109. currentText = {
  1110. id: Identifier.ascending("part"),
  1111. messageID: assistantMsg.id,
  1112. sessionID: assistantMsg.sessionID,
  1113. type: "text",
  1114. text: "",
  1115. time: {
  1116. start: Date.now(),
  1117. },
  1118. metadata: value.providerMetadata,
  1119. }
  1120. break
  1121. case "text-delta":
  1122. if (currentText) {
  1123. currentText.text += value.text
  1124. if (value.providerMetadata) currentText.metadata = value.providerMetadata
  1125. if (currentText.text)
  1126. await Session.updatePart({
  1127. part: currentText,
  1128. delta: value.text,
  1129. })
  1130. }
  1131. break
  1132. case "text-end":
  1133. if (currentText) {
  1134. currentText.text = currentText.text.trimEnd()
  1135. currentText.time = {
  1136. start: Date.now(),
  1137. end: Date.now(),
  1138. }
  1139. if (value.providerMetadata) currentText.metadata = value.providerMetadata
  1140. await Session.updatePart(currentText)
  1141. }
  1142. currentText = undefined
  1143. break
  1144. case "finish":
  1145. assistantMsg.time.completed = Date.now()
  1146. await Session.updateMessage(assistantMsg)
  1147. break
  1148. default:
  1149. log.info("unhandled", {
  1150. ...value,
  1151. })
  1152. continue
  1153. }
  1154. }
  1155. } catch (e) {
  1156. log.error("process", {
  1157. error: e,
  1158. })
  1159. const error = MessageV2.fromError(e, { providerID: input.providerID })
  1160. if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
  1161. shouldRetry = true
  1162. await Session.updatePart({
  1163. id: Identifier.ascending("part"),
  1164. messageID: assistantMsg.id,
  1165. sessionID: assistantMsg.sessionID,
  1166. type: "retry",
  1167. attempt: retries.count + 1,
  1168. time: {
  1169. created: Date.now(),
  1170. },
  1171. error,
  1172. })
  1173. } else {
  1174. assistantMsg.error = error
  1175. Bus.publish(Session.Event.Error, {
  1176. sessionID: assistantMsg.sessionID,
  1177. error: assistantMsg.error,
  1178. })
  1179. }
  1180. }
  1181. const p = await Session.getParts(assistantMsg.id)
  1182. for (const part of p) {
  1183. if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
  1184. Session.updatePart({
  1185. ...part,
  1186. state: {
  1187. status: "error",
  1188. error: "Tool execution aborted",
  1189. time: {
  1190. start: Date.now(),
  1191. end: Date.now(),
  1192. },
  1193. input: {},
  1194. },
  1195. })
  1196. }
  1197. }
  1198. if (!shouldRetry) {
  1199. assistantMsg.time.completed = Date.now()
  1200. }
  1201. await Session.updateMessage(assistantMsg)
  1202. return { info: assistantMsg, parts: p, blocked, shouldRetry }
  1203. },
  1204. }
  1205. return result
  1206. }
  1207. function isBusy(sessionID: string) {
  1208. return SessionLock.isLocked(sessionID)
  1209. }
  1210. function lock(sessionID: string) {
  1211. const handle = SessionLock.acquire({
  1212. sessionID,
  1213. })
  1214. log.info("locking", { sessionID })
  1215. return {
  1216. signal: handle.signal,
  1217. abort: handle.abort,
  1218. async [Symbol.dispose]() {
  1219. handle[Symbol.dispose]()
  1220. log.info("unlocking", { sessionID })
  1221. const session = await Session.get(sessionID)
  1222. if (session.parentID) return
  1223. Bus.publish(Event.Idle, {
  1224. sessionID,
  1225. })
  1226. },
  1227. }
  1228. }
  1229. export const ShellInput = z.object({
  1230. sessionID: Identifier.schema("session"),
  1231. agent: z.string(),
  1232. command: z.string(),
  1233. })
  1234. export type ShellInput = z.infer<typeof ShellInput>
  1235. export async function shell(input: ShellInput) {
  1236. using abort = lock(input.sessionID)
  1237. const session = await Session.get(input.sessionID)
  1238. if (session.revert) {
  1239. SessionRevert.cleanup(session)
  1240. }
  1241. const userMsg: MessageV2.User = {
  1242. id: Identifier.ascending("message"),
  1243. sessionID: input.sessionID,
  1244. time: {
  1245. created: Date.now(),
  1246. },
  1247. role: "user",
  1248. }
  1249. await Session.updateMessage(userMsg)
  1250. const userPart: MessageV2.Part = {
  1251. type: "text",
  1252. id: Identifier.ascending("part"),
  1253. messageID: userMsg.id,
  1254. sessionID: input.sessionID,
  1255. text: "The following tool was executed by the user",
  1256. synthetic: true,
  1257. }
  1258. await Session.updatePart(userPart)
  1259. const msg: MessageV2.Assistant = {
  1260. id: Identifier.ascending("message"),
  1261. sessionID: input.sessionID,
  1262. parentID: userMsg.id,
  1263. system: [],
  1264. mode: input.agent,
  1265. cost: 0,
  1266. path: {
  1267. cwd: Instance.directory,
  1268. root: Instance.worktree,
  1269. },
  1270. time: {
  1271. created: Date.now(),
  1272. },
  1273. role: "assistant",
  1274. tokens: {
  1275. input: 0,
  1276. output: 0,
  1277. reasoning: 0,
  1278. cache: { read: 0, write: 0 },
  1279. },
  1280. modelID: "",
  1281. providerID: "",
  1282. }
  1283. await Session.updateMessage(msg)
  1284. const part: MessageV2.Part = {
  1285. type: "tool",
  1286. id: Identifier.ascending("part"),
  1287. messageID: msg.id,
  1288. sessionID: input.sessionID,
  1289. tool: "bash",
  1290. callID: ulid(),
  1291. state: {
  1292. status: "running",
  1293. time: {
  1294. start: Date.now(),
  1295. },
  1296. input: {
  1297. command: input.command,
  1298. },
  1299. },
  1300. }
  1301. await Session.updatePart(part)
  1302. const shell = process.env["SHELL"] ?? "bash"
  1303. const shellName = path.basename(shell)
  1304. const invocations: Record<string, { args: string[] }> = {
  1305. nu: {
  1306. args: ["-c", input.command],
  1307. },
  1308. fish: {
  1309. args: ["-c", input.command],
  1310. },
  1311. zsh: {
  1312. args: [
  1313. "-c",
  1314. "-l",
  1315. `
  1316. [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
  1317. [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
  1318. ${input.command}
  1319. `,
  1320. ],
  1321. },
  1322. bash: {
  1323. args: [
  1324. "-c",
  1325. "-l",
  1326. `
  1327. [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
  1328. ${input.command}
  1329. `,
  1330. ],
  1331. },
  1332. // Fallback: any shell that doesn't match those above
  1333. "": {
  1334. args: ["-c", "-l", `${input.command}`],
  1335. },
  1336. }
  1337. const matchingInvocation = invocations[shellName] ?? invocations[""]
  1338. const args = matchingInvocation?.args
  1339. const proc = spawn(shell, args, {
  1340. cwd: Instance.directory,
  1341. signal: abort.signal,
  1342. detached: true,
  1343. stdio: ["ignore", "pipe", "pipe"],
  1344. env: {
  1345. ...process.env,
  1346. TERM: "dumb",
  1347. },
  1348. })
  1349. abort.signal.addEventListener("abort", () => {
  1350. if (!proc.pid) return
  1351. process.kill(-proc.pid)
  1352. })
  1353. let output = ""
  1354. proc.stdout?.on("data", (chunk) => {
  1355. output += chunk.toString()
  1356. if (part.state.status === "running") {
  1357. part.state.metadata = {
  1358. output: output,
  1359. description: "",
  1360. }
  1361. Session.updatePart(part)
  1362. }
  1363. })
  1364. proc.stderr?.on("data", (chunk) => {
  1365. output += chunk.toString()
  1366. if (part.state.status === "running") {
  1367. part.state.metadata = {
  1368. output: output,
  1369. description: "",
  1370. }
  1371. Session.updatePart(part)
  1372. }
  1373. })
  1374. await new Promise<void>((resolve) => {
  1375. proc.on("close", () => {
  1376. resolve()
  1377. })
  1378. })
  1379. msg.time.completed = Date.now()
  1380. await Session.updateMessage(msg)
  1381. if (part.state.status === "running") {
  1382. part.state = {
  1383. status: "completed",
  1384. time: {
  1385. ...part.state.time,
  1386. end: Date.now(),
  1387. },
  1388. input: part.state.input,
  1389. title: "",
  1390. metadata: {
  1391. output,
  1392. description: "",
  1393. },
  1394. output,
  1395. }
  1396. await Session.updatePart(part)
  1397. }
  1398. return { info: msg, parts: [part] }
  1399. }
  1400. export const CommandInput = z.object({
  1401. messageID: Identifier.schema("message").optional(),
  1402. sessionID: Identifier.schema("session"),
  1403. agent: z.string().optional(),
  1404. model: z.string().optional(),
  1405. arguments: z.string(),
  1406. command: z.string(),
  1407. })
  1408. export type CommandInput = z.infer<typeof CommandInput>
  1409. const bashRegex = /!`([^`]+)`/g
  1410. /**
  1411. * Regular expression to match @ file references in text
  1412. * Matches @ followed by file paths, excluding commas, periods at end of sentences, and backticks
  1413. * Does not match when preceded by word characters or backticks (to avoid email addresses and quoted references)
  1414. */
  1415. export async function command(input: CommandInput) {
  1416. log.info("command", input)
  1417. const command = await Command.get(input.command)
  1418. const agentName = command.agent ?? input.agent ?? "build"
  1419. let template = command.template.replaceAll("$ARGUMENTS", input.arguments)
  1420. const shell = ConfigMarkdown.shell(template)
  1421. if (shell.length > 0) {
  1422. const results = await Promise.all(
  1423. shell.map(async ([, cmd]) => {
  1424. try {
  1425. return await $`${{ raw: cmd }}`.nothrow().text()
  1426. } catch (error) {
  1427. return `Error executing command: ${error instanceof Error ? error.message : String(error)}`
  1428. }
  1429. }),
  1430. )
  1431. let index = 0
  1432. template = template.replace(bashRegex, () => results[index++])
  1433. }
  1434. const parts = [
  1435. {
  1436. type: "text",
  1437. text: template,
  1438. },
  1439. ] as PromptInput["parts"]
  1440. const files = ConfigMarkdown.files(template)
  1441. await Promise.all(
  1442. files.map(async (match) => {
  1443. const name = match[1]
  1444. const filepath = name.startsWith("~/")
  1445. ? path.join(os.homedir(), name.slice(2))
  1446. : path.resolve(Instance.worktree, name)
  1447. const stats = await fs.stat(filepath).catch(() => undefined)
  1448. if (!stats) {
  1449. const agent = await Agent.get(name)
  1450. if (agent) {
  1451. parts.push({
  1452. type: "agent",
  1453. name: agent.name,
  1454. })
  1455. }
  1456. return
  1457. }
  1458. if (stats.isDirectory()) {
  1459. parts.push({
  1460. type: "file",
  1461. url: `file://${filepath}`,
  1462. filename: name,
  1463. mime: "application/x-directory",
  1464. })
  1465. return
  1466. }
  1467. parts.push({
  1468. type: "file",
  1469. url: `file://${filepath}`,
  1470. filename: name,
  1471. mime: "text/plain",
  1472. })
  1473. }),
  1474. )
  1475. const model = await (async () => {
  1476. if (command.model) {
  1477. return Provider.parseModel(command.model)
  1478. }
  1479. if (command.agent) {
  1480. const cmdAgent = await Agent.get(command.agent)
  1481. if (cmdAgent.model) {
  1482. return cmdAgent.model
  1483. }
  1484. }
  1485. if (input.model) {
  1486. return Provider.parseModel(input.model)
  1487. }
  1488. return await Provider.defaultModel()
  1489. })()
  1490. const agent = await Agent.get(agentName)
  1491. if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
  1492. using abort = lock(input.sessionID)
  1493. const userMsg: MessageV2.User = {
  1494. id: Identifier.ascending("message"),
  1495. sessionID: input.sessionID,
  1496. time: {
  1497. created: Date.now(),
  1498. },
  1499. role: "user",
  1500. }
  1501. await Session.updateMessage(userMsg)
  1502. const userPart: MessageV2.Part = {
  1503. type: "text",
  1504. id: Identifier.ascending("part"),
  1505. messageID: userMsg.id,
  1506. sessionID: input.sessionID,
  1507. text: "The following tool was executed by the user",
  1508. synthetic: true,
  1509. }
  1510. await Session.updatePart(userPart)
  1511. const assistantMsg: MessageV2.Assistant = {
  1512. id: Identifier.ascending("message"),
  1513. sessionID: input.sessionID,
  1514. parentID: userMsg.id,
  1515. system: [],
  1516. mode: agentName,
  1517. cost: 0,
  1518. path: {
  1519. cwd: Instance.directory,
  1520. root: Instance.worktree,
  1521. },
  1522. time: {
  1523. created: Date.now(),
  1524. },
  1525. role: "assistant",
  1526. tokens: {
  1527. input: 0,
  1528. output: 0,
  1529. reasoning: 0,
  1530. cache: { read: 0, write: 0 },
  1531. },
  1532. modelID: model.modelID,
  1533. providerID: model.providerID,
  1534. }
  1535. await Session.updateMessage(assistantMsg)
  1536. const args = {
  1537. description: "Consulting " + agent.name,
  1538. subagent_type: agent.name,
  1539. prompt: template,
  1540. }
  1541. const toolPart: MessageV2.ToolPart = {
  1542. type: "tool",
  1543. id: Identifier.ascending("part"),
  1544. messageID: assistantMsg.id,
  1545. sessionID: input.sessionID,
  1546. tool: "task",
  1547. callID: ulid(),
  1548. state: {
  1549. status: "running",
  1550. time: {
  1551. start: Date.now(),
  1552. },
  1553. input: {
  1554. description: args.description,
  1555. subagent_type: args.subagent_type,
  1556. // truncate prompt to preserve context
  1557. prompt: args.prompt.length > 100 ? args.prompt.substring(0, 97) + "..." : args.prompt,
  1558. },
  1559. },
  1560. }
  1561. await Session.updatePart(toolPart)
  1562. const result = await TaskTool.init().then((t) =>
  1563. t.execute(args, {
  1564. sessionID: input.sessionID,
  1565. abort: abort.signal,
  1566. agent: agent.name,
  1567. messageID: assistantMsg.id,
  1568. extra: {},
  1569. metadata: async (metadata) => {
  1570. if (toolPart.state.status === "running") {
  1571. toolPart.state.metadata = metadata.metadata
  1572. toolPart.state.title = metadata.title
  1573. await Session.updatePart(toolPart)
  1574. }
  1575. },
  1576. }),
  1577. )
  1578. assistantMsg.time.completed = Date.now()
  1579. await Session.updateMessage(assistantMsg)
  1580. if (toolPart.state.status === "running") {
  1581. toolPart.state = {
  1582. status: "completed",
  1583. time: {
  1584. ...toolPart.state.time,
  1585. end: Date.now(),
  1586. },
  1587. input: toolPart.state.input,
  1588. title: "",
  1589. metadata: result.metadata,
  1590. output: result.output,
  1591. }
  1592. await Session.updatePart(toolPart)
  1593. }
  1594. return { info: assistantMsg, parts: [toolPart] }
  1595. }
  1596. return prompt({
  1597. sessionID: input.sessionID,
  1598. messageID: input.messageID,
  1599. model,
  1600. agent: agentName,
  1601. parts,
  1602. })
  1603. }
  1604. async function ensureTitle(input: {
  1605. session: Session.Info
  1606. message: MessageV2.WithParts
  1607. history: MessageV2.WithParts[]
  1608. providerID: string
  1609. modelID: string
  1610. }) {
  1611. if (input.session.parentID) return
  1612. const isFirst =
  1613. input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
  1614. .length === 1
  1615. if (!isFirst) return
  1616. const small =
  1617. (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
  1618. const options = {
  1619. ...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
  1620. ...small.info.options,
  1621. }
  1622. if (small.providerID === "openai" || small.modelID.includes("gpt-5")) {
  1623. options["reasoningEffort"] = "minimal"
  1624. }
  1625. if (small.providerID === "google") {
  1626. options["thinkingConfig"] = {
  1627. thinkingBudget: 0,
  1628. }
  1629. }
  1630. generateText({
  1631. maxOutputTokens: small.info.reasoning ? 1500 : 20,
  1632. providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
  1633. messages: [
  1634. ...SystemPrompt.title(small.providerID).map(
  1635. (x): ModelMessage => ({
  1636. role: "system",
  1637. content: x,
  1638. }),
  1639. ),
  1640. ...MessageV2.toModelMessage([
  1641. {
  1642. info: {
  1643. id: Identifier.ascending("message"),
  1644. role: "user",
  1645. sessionID: input.session.id,
  1646. time: {
  1647. created: Date.now(),
  1648. },
  1649. },
  1650. parts: input.message.parts,
  1651. },
  1652. ]),
  1653. ],
  1654. model: small.language,
  1655. })
  1656. .then((result) => {
  1657. if (result.text)
  1658. return Session.update(input.session.id, (draft) => {
  1659. const cleaned = result.text
  1660. .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
  1661. .split("\n")
  1662. .map((line) => line.trim())
  1663. .find((line) => line.length > 0)
  1664. if (!cleaned) return
  1665. const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
  1666. draft.title = title
  1667. })
  1668. })
  1669. .catch((error) => {
  1670. log.error("failed to generate title", { error, model: small.info.id })
  1671. })
  1672. }
  1673. }