prompt.ts 48 KB

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