message-v2.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. import z from "zod/v4"
  2. import { Bus } from "../bus"
  3. import { NamedError } from "../util/error"
  4. import { Message } from "./message"
  5. import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
  6. import { Identifier } from "../id/id"
  7. import { LSP } from "../lsp"
  8. import { Snapshot } from "@/snapshot"
  9. import { fn } from "@/util/fn"
  10. export namespace MessageV2 {
  11. export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
  12. export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
  13. export const AuthError = NamedError.create(
  14. "ProviderAuthError",
  15. z.object({
  16. providerID: z.string(),
  17. message: z.string(),
  18. }),
  19. )
  20. const PartBase = z.object({
  21. id: z.string(),
  22. sessionID: z.string(),
  23. messageID: z.string(),
  24. })
  25. export const SnapshotPart = PartBase.extend({
  26. type: z.literal("snapshot"),
  27. snapshot: z.string(),
  28. }).meta({
  29. ref: "SnapshotPart",
  30. })
  31. export type SnapshotPart = z.infer<typeof SnapshotPart>
  32. export const PatchPart = PartBase.extend({
  33. type: z.literal("patch"),
  34. hash: z.string(),
  35. files: z.string().array(),
  36. }).meta({
  37. ref: "PatchPart",
  38. })
  39. export type PatchPart = z.infer<typeof PatchPart>
  40. export const TextPart = PartBase.extend({
  41. type: z.literal("text"),
  42. text: z.string(),
  43. synthetic: z.boolean().optional(),
  44. time: z
  45. .object({
  46. start: z.number(),
  47. end: z.number().optional(),
  48. })
  49. .optional(),
  50. metadata: z.record(z.string(), z.any()).optional(),
  51. }).meta({
  52. ref: "TextPart",
  53. })
  54. export type TextPart = z.infer<typeof TextPart>
  55. export const ReasoningPart = PartBase.extend({
  56. type: z.literal("reasoning"),
  57. text: z.string(),
  58. metadata: z.record(z.string(), z.any()).optional(),
  59. time: z.object({
  60. start: z.number(),
  61. end: z.number().optional(),
  62. }),
  63. }).meta({
  64. ref: "ReasoningPart",
  65. })
  66. export type ReasoningPart = z.infer<typeof ReasoningPart>
  67. const FilePartSourceBase = z.object({
  68. text: z
  69. .object({
  70. value: z.string(),
  71. start: z.number().int(),
  72. end: z.number().int(),
  73. })
  74. .meta({
  75. ref: "FilePartSourceText",
  76. }),
  77. })
  78. export const FileSource = FilePartSourceBase.extend({
  79. type: z.literal("file"),
  80. path: z.string(),
  81. }).meta({
  82. ref: "FileSource",
  83. })
  84. export const SymbolSource = FilePartSourceBase.extend({
  85. type: z.literal("symbol"),
  86. path: z.string(),
  87. range: LSP.Range,
  88. name: z.string(),
  89. kind: z.number().int(),
  90. }).meta({
  91. ref: "SymbolSource",
  92. })
  93. export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource]).meta({
  94. ref: "FilePartSource",
  95. })
  96. export const FilePart = PartBase.extend({
  97. type: z.literal("file"),
  98. mime: z.string(),
  99. filename: z.string().optional(),
  100. url: z.string(),
  101. source: FilePartSource.optional(),
  102. }).meta({
  103. ref: "FilePart",
  104. })
  105. export type FilePart = z.infer<typeof FilePart>
  106. export const AgentPart = PartBase.extend({
  107. type: z.literal("agent"),
  108. name: z.string(),
  109. source: z
  110. .object({
  111. value: z.string(),
  112. start: z.number().int(),
  113. end: z.number().int(),
  114. })
  115. .optional(),
  116. }).meta({
  117. ref: "AgentPart",
  118. })
  119. export type AgentPart = z.infer<typeof AgentPart>
  120. export const StepStartPart = PartBase.extend({
  121. type: z.literal("step-start"),
  122. snapshot: z.string().optional(),
  123. }).meta({
  124. ref: "StepStartPart",
  125. })
  126. export type StepStartPart = z.infer<typeof StepStartPart>
  127. export const StepFinishPart = PartBase.extend({
  128. type: z.literal("step-finish"),
  129. snapshot: z.string().optional(),
  130. cost: z.number(),
  131. tokens: z.object({
  132. input: z.number(),
  133. output: z.number(),
  134. reasoning: z.number(),
  135. cache: z.object({
  136. read: z.number(),
  137. write: z.number(),
  138. }),
  139. }),
  140. }).meta({
  141. ref: "StepFinishPart",
  142. })
  143. export type StepFinishPart = z.infer<typeof StepFinishPart>
  144. export const ToolStatePending = z
  145. .object({
  146. status: z.literal("pending"),
  147. })
  148. .meta({
  149. ref: "ToolStatePending",
  150. })
  151. export type ToolStatePending = z.infer<typeof ToolStatePending>
  152. export const ToolStateRunning = z
  153. .object({
  154. status: z.literal("running"),
  155. input: z.any(),
  156. title: z.string().optional(),
  157. metadata: z.record(z.string(), z.any()).optional(),
  158. time: z.object({
  159. start: z.number(),
  160. }),
  161. })
  162. .meta({
  163. ref: "ToolStateRunning",
  164. })
  165. export type ToolStateRunning = z.infer<typeof ToolStateRunning>
  166. export const ToolStateCompleted = z
  167. .object({
  168. status: z.literal("completed"),
  169. input: z.record(z.string(), z.any()),
  170. output: z.string(),
  171. title: z.string(),
  172. metadata: z.record(z.string(), z.any()),
  173. time: z.object({
  174. start: z.number(),
  175. end: z.number(),
  176. compacted: z.number().optional(),
  177. }),
  178. attachments: FilePart.array().optional(),
  179. })
  180. .meta({
  181. ref: "ToolStateCompleted",
  182. })
  183. export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
  184. export const ToolStateError = z
  185. .object({
  186. status: z.literal("error"),
  187. input: z.record(z.string(), z.any()),
  188. error: z.string(),
  189. metadata: z.record(z.string(), z.any()).optional(),
  190. time: z.object({
  191. start: z.number(),
  192. end: z.number(),
  193. }),
  194. })
  195. .meta({
  196. ref: "ToolStateError",
  197. })
  198. export type ToolStateError = z.infer<typeof ToolStateError>
  199. export const ToolState = z
  200. .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
  201. .meta({
  202. ref: "ToolState",
  203. })
  204. export const ToolPart = PartBase.extend({
  205. type: z.literal("tool"),
  206. callID: z.string(),
  207. tool: z.string(),
  208. state: ToolState,
  209. metadata: z.record(z.string(), z.any()).optional(),
  210. }).meta({
  211. ref: "ToolPart",
  212. })
  213. export type ToolPart = z.infer<typeof ToolPart>
  214. const Base = z.object({
  215. id: z.string(),
  216. sessionID: z.string(),
  217. })
  218. export const User = Base.extend({
  219. role: z.literal("user"),
  220. time: z.object({
  221. created: z.number(),
  222. }),
  223. summary: z
  224. .object({
  225. diffs: Snapshot.FileDiff.array(),
  226. text: z.string(),
  227. })
  228. .optional(),
  229. }).meta({
  230. ref: "UserMessage",
  231. })
  232. export type User = z.infer<typeof User>
  233. export const Part = z
  234. .discriminatedUnion("type", [
  235. TextPart,
  236. ReasoningPart,
  237. FilePart,
  238. ToolPart,
  239. StepStartPart,
  240. StepFinishPart,
  241. SnapshotPart,
  242. PatchPart,
  243. AgentPart,
  244. ])
  245. .meta({
  246. ref: "Part",
  247. })
  248. export type Part = z.infer<typeof Part>
  249. export const Assistant = Base.extend({
  250. role: z.literal("assistant"),
  251. time: z.object({
  252. created: z.number(),
  253. completed: z.number().optional(),
  254. }),
  255. error: z
  256. .discriminatedUnion("name", [
  257. AuthError.Schema,
  258. NamedError.Unknown.Schema,
  259. OutputLengthError.Schema,
  260. AbortedError.Schema,
  261. ])
  262. .optional(),
  263. system: z.string().array(),
  264. parentID: z.string(),
  265. modelID: z.string(),
  266. providerID: z.string(),
  267. mode: z.string(),
  268. path: z.object({
  269. cwd: z.string(),
  270. root: z.string(),
  271. }),
  272. summary: z.boolean().optional(),
  273. cost: z.number(),
  274. tokens: z.object({
  275. input: z.number(),
  276. output: z.number(),
  277. reasoning: z.number(),
  278. cache: z.object({
  279. read: z.number(),
  280. write: z.number(),
  281. }),
  282. }),
  283. }).meta({
  284. ref: "AssistantMessage",
  285. })
  286. export type Assistant = z.infer<typeof Assistant>
  287. export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({
  288. ref: "Message",
  289. })
  290. export type Info = z.infer<typeof Info>
  291. export const Event = {
  292. Updated: Bus.event(
  293. "message.updated",
  294. z.object({
  295. info: Info,
  296. }),
  297. ),
  298. Removed: Bus.event(
  299. "message.removed",
  300. z.object({
  301. sessionID: z.string(),
  302. messageID: z.string(),
  303. }),
  304. ),
  305. PartUpdated: Bus.event(
  306. "message.part.updated",
  307. z.object({
  308. part: Part,
  309. }),
  310. ),
  311. PartRemoved: Bus.event(
  312. "message.part.removed",
  313. z.object({
  314. sessionID: z.string(),
  315. messageID: z.string(),
  316. partID: z.string(),
  317. }),
  318. ),
  319. }
  320. export const WithParts = z.object({
  321. info: Info,
  322. parts: z.array(Part),
  323. })
  324. export type WithParts = z.infer<typeof WithParts>
  325. export function fromV1(v1: Message.Info) {
  326. if (v1.role === "assistant") {
  327. const info: Assistant = {
  328. id: v1.id,
  329. parentID: "",
  330. sessionID: v1.metadata.sessionID,
  331. role: "assistant",
  332. time: {
  333. created: v1.metadata.time.created,
  334. completed: v1.metadata.time.completed,
  335. },
  336. cost: v1.metadata.assistant!.cost,
  337. path: v1.metadata.assistant!.path,
  338. summary: v1.metadata.assistant!.summary,
  339. tokens: v1.metadata.assistant!.tokens,
  340. modelID: v1.metadata.assistant!.modelID,
  341. providerID: v1.metadata.assistant!.providerID,
  342. system: v1.metadata.assistant!.system,
  343. mode: "build",
  344. error: v1.metadata.error,
  345. }
  346. const parts = v1.parts.flatMap((part): Part[] => {
  347. const base = {
  348. id: Identifier.ascending("part"),
  349. messageID: v1.id,
  350. sessionID: v1.metadata.sessionID,
  351. }
  352. if (part.type === "text") {
  353. return [
  354. {
  355. ...base,
  356. type: "text",
  357. text: part.text,
  358. },
  359. ]
  360. }
  361. if (part.type === "step-start") {
  362. return [
  363. {
  364. ...base,
  365. type: "step-start",
  366. },
  367. ]
  368. }
  369. if (part.type === "tool-invocation") {
  370. return [
  371. {
  372. ...base,
  373. type: "tool",
  374. callID: part.toolInvocation.toolCallId,
  375. tool: part.toolInvocation.toolName,
  376. state: (() => {
  377. if (part.toolInvocation.state === "partial-call") {
  378. return {
  379. status: "pending",
  380. }
  381. }
  382. const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
  383. if (part.toolInvocation.state === "call") {
  384. return {
  385. status: "running",
  386. input: part.toolInvocation.args,
  387. time: {
  388. start: time?.start,
  389. },
  390. }
  391. }
  392. if (part.toolInvocation.state === "result") {
  393. return {
  394. status: "completed",
  395. input: part.toolInvocation.args,
  396. output: part.toolInvocation.result,
  397. title,
  398. time,
  399. metadata,
  400. }
  401. }
  402. throw new Error("unknown tool invocation state")
  403. })(),
  404. },
  405. ]
  406. }
  407. return []
  408. })
  409. return {
  410. info,
  411. parts,
  412. }
  413. }
  414. if (v1.role === "user") {
  415. const info: User = {
  416. id: v1.id,
  417. sessionID: v1.metadata.sessionID,
  418. role: "user",
  419. time: {
  420. created: v1.metadata.time.created,
  421. },
  422. }
  423. const parts = v1.parts.flatMap((part): Part[] => {
  424. const base = {
  425. id: Identifier.ascending("part"),
  426. messageID: v1.id,
  427. sessionID: v1.metadata.sessionID,
  428. }
  429. if (part.type === "text") {
  430. return [
  431. {
  432. ...base,
  433. type: "text",
  434. text: part.text,
  435. },
  436. ]
  437. }
  438. if (part.type === "file") {
  439. return [
  440. {
  441. ...base,
  442. type: "file",
  443. mime: part.mediaType,
  444. filename: part.filename,
  445. url: part.url,
  446. },
  447. ]
  448. }
  449. return []
  450. })
  451. return { info, parts }
  452. }
  453. throw new Error("unknown message type")
  454. }
  455. export function toModelMessage(
  456. input: {
  457. info: Info
  458. parts: Part[]
  459. }[],
  460. ): ModelMessage[] {
  461. const result: UIMessage[] = []
  462. for (const msg of input) {
  463. if (msg.parts.length === 0) continue
  464. if (msg.info.role === "user") {
  465. result.push({
  466. id: msg.info.id,
  467. role: "user",
  468. parts: msg.parts.flatMap((part): UIMessage["parts"] => {
  469. if (part.type === "text")
  470. return [
  471. {
  472. type: "text",
  473. text: part.text,
  474. },
  475. ]
  476. // text/plain and directory files are converted into text parts, ignore them
  477. if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
  478. return [
  479. {
  480. type: "file",
  481. url: part.url,
  482. mediaType: part.mime,
  483. filename: part.filename,
  484. },
  485. ]
  486. return []
  487. }),
  488. })
  489. }
  490. if (msg.info.role === "assistant") {
  491. result.push({
  492. id: msg.info.id,
  493. role: "assistant",
  494. parts: msg.parts.flatMap((part): UIMessage["parts"] => {
  495. if (part.type === "text")
  496. return [
  497. {
  498. type: "text",
  499. text: part.text,
  500. providerMetadata: part.metadata,
  501. },
  502. ]
  503. if (part.type === "step-start")
  504. return [
  505. {
  506. type: "step-start",
  507. },
  508. ]
  509. if (part.type === "tool") {
  510. if (part.state.status === "completed") {
  511. if (part.state.attachments?.length) {
  512. result.push({
  513. id: Identifier.ascending("message"),
  514. role: "user",
  515. parts: [
  516. {
  517. type: "text",
  518. text: `Tool ${part.tool} returned an attachment:`,
  519. },
  520. ...part.state.attachments.map((attachment) => ({
  521. type: "file" as const,
  522. url: attachment.url,
  523. mediaType: attachment.mime,
  524. filename: attachment.filename,
  525. })),
  526. ],
  527. })
  528. }
  529. return [
  530. {
  531. type: ("tool-" + part.tool) as `tool-${string}`,
  532. state: "output-available",
  533. toolCallId: part.callID,
  534. input: part.state.input,
  535. output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
  536. callProviderMetadata: part.metadata,
  537. },
  538. ]
  539. }
  540. if (part.state.status === "error")
  541. return [
  542. {
  543. type: ("tool-" + part.tool) as `tool-${string}`,
  544. state: "output-error",
  545. toolCallId: part.callID,
  546. input: part.state.input,
  547. errorText: part.state.error,
  548. callProviderMetadata: part.metadata,
  549. },
  550. ]
  551. }
  552. if (part.type === "reasoning") {
  553. return [
  554. {
  555. type: "reasoning",
  556. text: part.text,
  557. providerMetadata: part.metadata,
  558. },
  559. ]
  560. }
  561. return []
  562. }),
  563. })
  564. }
  565. }
  566. return convertToModelMessages(result)
  567. }
  568. export function filterCompacted(msgs: { info: MessageV2.Info; parts: MessageV2.Part[] }[]) {
  569. const i = msgs.findLastIndex((m) => m.info.role === "assistant" && !!m.info.summary)
  570. if (i === -1) return msgs.slice()
  571. return msgs.slice(i)
  572. }
  573. }