message-v2.ts 18 KB

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