message-v2.ts 20 KB

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