message-v2.ts 18 KB

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