prompt.ts 50 KB

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