prompt.ts 51 KB

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