message-v2.ts 20 KB

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