message-v2.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  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. import { fn } from "@/util/fn"
  10. import { Storage } from "@/storage/storage"
  11. export namespace MessageV2 {
  12. export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
  13. export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
  14. export const AuthError = NamedError.create(
  15. "ProviderAuthError",
  16. z.object({
  17. providerID: z.string(),
  18. message: z.string(),
  19. }),
  20. )
  21. export const APIError = NamedError.create(
  22. "APIError",
  23. z.object({
  24. message: z.string(),
  25. statusCode: z.number().optional(),
  26. isRetryable: z.boolean(),
  27. responseHeaders: z.record(z.string(), z.string()).optional(),
  28. responseBody: z.string().optional(),
  29. }),
  30. )
  31. export type APIError = z.infer<typeof APIError.Schema>
  32. const PartBase = z.object({
  33. id: z.string(),
  34. sessionID: z.string(),
  35. messageID: z.string(),
  36. })
  37. export const SnapshotPart = PartBase.extend({
  38. type: z.literal("snapshot"),
  39. snapshot: z.string(),
  40. }).meta({
  41. ref: "SnapshotPart",
  42. })
  43. export type SnapshotPart = z.infer<typeof SnapshotPart>
  44. export const PatchPart = PartBase.extend({
  45. type: z.literal("patch"),
  46. hash: z.string(),
  47. files: z.string().array(),
  48. }).meta({
  49. ref: "PatchPart",
  50. })
  51. export type PatchPart = z.infer<typeof PatchPart>
  52. export const TextPart = PartBase.extend({
  53. type: z.literal("text"),
  54. text: z.string(),
  55. synthetic: z.boolean().optional(),
  56. time: z
  57. .object({
  58. start: z.number(),
  59. end: z.number().optional(),
  60. })
  61. .optional(),
  62. metadata: z.record(z.string(), z.any()).optional(),
  63. }).meta({
  64. ref: "TextPart",
  65. })
  66. export type TextPart = z.infer<typeof TextPart>
  67. export const ReasoningPart = PartBase.extend({
  68. type: z.literal("reasoning"),
  69. text: z.string(),
  70. metadata: z.record(z.string(), z.any()).optional(),
  71. time: z.object({
  72. start: z.number(),
  73. end: z.number().optional(),
  74. }),
  75. }).meta({
  76. ref: "ReasoningPart",
  77. })
  78. export type ReasoningPart = z.infer<typeof ReasoningPart>
  79. const FilePartSourceBase = z.object({
  80. text: z
  81. .object({
  82. value: z.string(),
  83. start: z.number().int(),
  84. end: z.number().int(),
  85. })
  86. .meta({
  87. ref: "FilePartSourceText",
  88. }),
  89. })
  90. export const FileSource = FilePartSourceBase.extend({
  91. type: z.literal("file"),
  92. path: z.string(),
  93. }).meta({
  94. ref: "FileSource",
  95. })
  96. export const SymbolSource = FilePartSourceBase.extend({
  97. type: z.literal("symbol"),
  98. path: z.string(),
  99. range: LSP.Range,
  100. name: z.string(),
  101. kind: z.number().int(),
  102. }).meta({
  103. ref: "SymbolSource",
  104. })
  105. export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource]).meta({
  106. ref: "FilePartSource",
  107. })
  108. export const FilePart = PartBase.extend({
  109. type: z.literal("file"),
  110. mime: z.string(),
  111. filename: z.string().optional(),
  112. url: z.string(),
  113. source: FilePartSource.optional(),
  114. }).meta({
  115. ref: "FilePart",
  116. })
  117. export type FilePart = z.infer<typeof FilePart>
  118. export const AgentPart = PartBase.extend({
  119. type: z.literal("agent"),
  120. name: z.string(),
  121. source: z
  122. .object({
  123. value: z.string(),
  124. start: z.number().int(),
  125. end: z.number().int(),
  126. })
  127. .optional(),
  128. }).meta({
  129. ref: "AgentPart",
  130. })
  131. export type AgentPart = z.infer<typeof AgentPart>
  132. export const RetryPart = PartBase.extend({
  133. type: z.literal("retry"),
  134. attempt: z.number(),
  135. error: APIError.Schema,
  136. time: z.object({
  137. created: z.number(),
  138. }),
  139. }).meta({
  140. ref: "RetryPart",
  141. })
  142. export type RetryPart = z.infer<typeof RetryPart>
  143. export const StepStartPart = PartBase.extend({
  144. type: z.literal("step-start"),
  145. snapshot: z.string().optional(),
  146. }).meta({
  147. ref: "StepStartPart",
  148. })
  149. export type StepStartPart = z.infer<typeof StepStartPart>
  150. export const StepFinishPart = PartBase.extend({
  151. type: z.literal("step-finish"),
  152. reason: z.string(),
  153. snapshot: z.string().optional(),
  154. cost: z.number(),
  155. tokens: z.object({
  156. input: z.number(),
  157. output: z.number(),
  158. reasoning: z.number(),
  159. cache: z.object({
  160. read: z.number(),
  161. write: z.number(),
  162. }),
  163. }),
  164. }).meta({
  165. ref: "StepFinishPart",
  166. })
  167. export type StepFinishPart = z.infer<typeof StepFinishPart>
  168. export const ToolStatePending = z
  169. .object({
  170. status: z.literal("pending"),
  171. input: z.record(z.string(), z.any()),
  172. raw: z.string(),
  173. })
  174. .meta({
  175. ref: "ToolStatePending",
  176. })
  177. export type ToolStatePending = z.infer<typeof ToolStatePending>
  178. export const ToolStateRunning = z
  179. .object({
  180. status: z.literal("running"),
  181. input: z.record(z.string(), z.any()),
  182. title: z.string().optional(),
  183. metadata: z.record(z.string(), z.any()).optional(),
  184. time: z.object({
  185. start: z.number(),
  186. }),
  187. })
  188. .meta({
  189. ref: "ToolStateRunning",
  190. })
  191. export type ToolStateRunning = z.infer<typeof ToolStateRunning>
  192. export const ToolStateCompleted = z
  193. .object({
  194. status: z.literal("completed"),
  195. input: z.record(z.string(), z.any()),
  196. output: z.string(),
  197. title: z.string(),
  198. metadata: z.record(z.string(), z.any()),
  199. time: z.object({
  200. start: z.number(),
  201. end: z.number(),
  202. compacted: z.number().optional(),
  203. }),
  204. attachments: FilePart.array().optional(),
  205. })
  206. .meta({
  207. ref: "ToolStateCompleted",
  208. })
  209. export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
  210. export const ToolStateError = z
  211. .object({
  212. status: z.literal("error"),
  213. input: z.record(z.string(), z.any()),
  214. error: z.string(),
  215. metadata: z.record(z.string(), z.any()).optional(),
  216. time: z.object({
  217. start: z.number(),
  218. end: z.number(),
  219. }),
  220. })
  221. .meta({
  222. ref: "ToolStateError",
  223. })
  224. export type ToolStateError = z.infer<typeof ToolStateError>
  225. export const ToolState = z
  226. .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
  227. .meta({
  228. ref: "ToolState",
  229. })
  230. export const ToolPart = PartBase.extend({
  231. type: z.literal("tool"),
  232. callID: z.string(),
  233. tool: z.string(),
  234. state: ToolState,
  235. metadata: z.record(z.string(), z.any()).optional(),
  236. }).meta({
  237. ref: "ToolPart",
  238. })
  239. export type ToolPart = z.infer<typeof ToolPart>
  240. const Base = z.object({
  241. id: z.string(),
  242. sessionID: z.string(),
  243. })
  244. export const User = Base.extend({
  245. role: z.literal("user"),
  246. time: z.object({
  247. created: z.number(),
  248. }),
  249. summary: z
  250. .object({
  251. title: z.string().optional(),
  252. body: z.string().optional(),
  253. diffs: Snapshot.FileDiff.array(),
  254. })
  255. .optional(),
  256. }).meta({
  257. ref: "UserMessage",
  258. })
  259. export type User = z.infer<typeof User>
  260. export const Part = z
  261. .discriminatedUnion("type", [
  262. TextPart,
  263. ReasoningPart,
  264. FilePart,
  265. ToolPart,
  266. StepStartPart,
  267. StepFinishPart,
  268. SnapshotPart,
  269. PatchPart,
  270. AgentPart,
  271. RetryPart,
  272. ])
  273. .meta({
  274. ref: "Part",
  275. })
  276. export type Part = z.infer<typeof Part>
  277. export const Assistant = Base.extend({
  278. role: z.literal("assistant"),
  279. time: z.object({
  280. created: z.number(),
  281. completed: z.number().optional(),
  282. }),
  283. error: z
  284. .discriminatedUnion("name", [
  285. AuthError.Schema,
  286. NamedError.Unknown.Schema,
  287. OutputLengthError.Schema,
  288. AbortedError.Schema,
  289. APIError.Schema,
  290. ])
  291. .optional(),
  292. parentID: z.string(),
  293. modelID: z.string(),
  294. providerID: z.string(),
  295. mode: z.string(),
  296. path: z.object({
  297. cwd: z.string(),
  298. root: z.string(),
  299. }),
  300. summary: z.boolean().optional(),
  301. cost: z.number(),
  302. tokens: z.object({
  303. input: z.number(),
  304. output: z.number(),
  305. reasoning: z.number(),
  306. cache: z.object({
  307. read: z.number(),
  308. write: z.number(),
  309. }),
  310. }),
  311. }).meta({
  312. ref: "AssistantMessage",
  313. })
  314. export type Assistant = z.infer<typeof Assistant>
  315. export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({
  316. ref: "Message",
  317. })
  318. export type Info = z.infer<typeof Info>
  319. export const Event = {
  320. Updated: Bus.event(
  321. "message.updated",
  322. z.object({
  323. info: Info,
  324. }),
  325. ),
  326. Removed: Bus.event(
  327. "message.removed",
  328. z.object({
  329. sessionID: z.string(),
  330. messageID: z.string(),
  331. }),
  332. ),
  333. PartUpdated: Bus.event(
  334. "message.part.updated",
  335. z.object({
  336. part: Part,
  337. delta: z.string().optional(),
  338. }),
  339. ),
  340. PartRemoved: Bus.event(
  341. "message.part.removed",
  342. z.object({
  343. sessionID: z.string(),
  344. messageID: z.string(),
  345. partID: z.string(),
  346. }),
  347. ),
  348. }
  349. export const WithParts = z.object({
  350. info: Info,
  351. parts: z.array(Part),
  352. })
  353. export type WithParts = z.infer<typeof WithParts>
  354. export function fromV1(v1: Message.Info) {
  355. if (v1.role === "assistant") {
  356. const info: Assistant = {
  357. id: v1.id,
  358. parentID: "",
  359. sessionID: v1.metadata.sessionID,
  360. role: "assistant",
  361. time: {
  362. created: v1.metadata.time.created,
  363. completed: v1.metadata.time.completed,
  364. },
  365. cost: v1.metadata.assistant!.cost,
  366. path: v1.metadata.assistant!.path,
  367. summary: v1.metadata.assistant!.summary,
  368. tokens: v1.metadata.assistant!.tokens,
  369. modelID: v1.metadata.assistant!.modelID,
  370. providerID: v1.metadata.assistant!.providerID,
  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 const stream = fn(Identifier.schema("session"), async function* (sessionID) {
  599. const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
  600. for (let i = list.length - 1; i >= 0; i--) {
  601. yield await get({
  602. sessionID,
  603. messageID: list[i][2],
  604. })
  605. }
  606. })
  607. export const parts = fn(Identifier.schema("message"), async (messageID) => {
  608. const result = [] as MessageV2.Part[]
  609. for (const item of await Storage.list(["part", messageID])) {
  610. const read = await Storage.read<MessageV2.Part>(item)
  611. result.push(read)
  612. }
  613. result.sort((a, b) => (a.id > b.id ? 1 : -1))
  614. return result
  615. })
  616. export const get = fn(
  617. z.object({
  618. sessionID: Identifier.schema("session"),
  619. messageID: Identifier.schema("message"),
  620. }),
  621. async (input) => {
  622. return {
  623. info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
  624. parts: await parts(input.messageID),
  625. }
  626. },
  627. )
  628. export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
  629. const result = [] as MessageV2.WithParts[]
  630. for await (const msg of stream) {
  631. result.push(msg)
  632. if (msg.info.role === "assistant" && msg.info.summary === true) break
  633. }
  634. result.reverse()
  635. return result
  636. }
  637. export function fromError(e: unknown, ctx: { providerID: string }) {
  638. switch (true) {
  639. case e instanceof DOMException && e.name === "AbortError":
  640. return new MessageV2.AbortedError(
  641. { message: e.message },
  642. {
  643. cause: e,
  644. },
  645. ).toObject()
  646. case MessageV2.OutputLengthError.isInstance(e):
  647. return e
  648. case LoadAPIKeyError.isInstance(e):
  649. return new MessageV2.AuthError(
  650. {
  651. providerID: ctx.providerID,
  652. message: e.message,
  653. },
  654. { cause: e },
  655. ).toObject()
  656. case APICallError.isInstance(e):
  657. return new MessageV2.APIError(
  658. {
  659. message: e.message,
  660. statusCode: e.statusCode,
  661. isRetryable: e.isRetryable,
  662. responseHeaders: e.responseHeaders,
  663. responseBody: e.responseBody,
  664. },
  665. { cause: e },
  666. ).toObject()
  667. case e instanceof Error:
  668. return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
  669. default:
  670. return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
  671. }
  672. }
  673. }