message-v2.ts 19 KB

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