prompt.ts 50 KB

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