prompt.ts 54 KB

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