message-v2.ts 16 KB

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