prompt.ts 53 KB

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