message-v2.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import z from "zod"
  4. import { NamedError } from "@opencode-ai/util/error"
  5. import { Message } from "./message"
  6. import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
  7. import { Identifier } from "../id/id"
  8. import { LSP } from "../lsp"
  9. import { Snapshot } from "@/snapshot"
  10. import { fn } from "@/util/fn"
  11. import { Storage } from "@/storage/storage"
  12. import { ProviderTransform } from "@/provider/transform"
  13. import { STATUS_CODES } from "http"
  14. import { iife } from "@/util/iife"
  15. import { type SystemError } from "bun"
  16. export namespace MessageV2 {
  17. export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
  18. export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() }))
  19. export const AuthError = NamedError.create(
  20. "ProviderAuthError",
  21. z.object({
  22. providerID: z.string(),
  23. message: z.string(),
  24. }),
  25. )
  26. export const APIError = NamedError.create(
  27. "APIError",
  28. z.object({
  29. message: z.string(),
  30. statusCode: z.number().optional(),
  31. isRetryable: z.boolean(),
  32. responseHeaders: z.record(z.string(), z.string()).optional(),
  33. responseBody: z.string().optional(),
  34. details: z.record(z.string(), z.string()).optional(),
  35. }),
  36. )
  37. export type APIError = z.infer<typeof APIError.Schema>
  38. const PartBase = z.object({
  39. id: z.string(),
  40. sessionID: z.string(),
  41. messageID: z.string(),
  42. })
  43. export const SnapshotPart = PartBase.extend({
  44. type: z.literal("snapshot"),
  45. snapshot: z.string(),
  46. }).meta({
  47. ref: "SnapshotPart",
  48. })
  49. export type SnapshotPart = z.infer<typeof SnapshotPart>
  50. export const PatchPart = PartBase.extend({
  51. type: z.literal("patch"),
  52. hash: z.string(),
  53. files: z.string().array(),
  54. }).meta({
  55. ref: "PatchPart",
  56. })
  57. export type PatchPart = z.infer<typeof PatchPart>
  58. export const TextPart = PartBase.extend({
  59. type: z.literal("text"),
  60. text: z.string(),
  61. synthetic: z.boolean().optional(),
  62. ignored: 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 CompactionPart = PartBase.extend({
  140. type: z.literal("compaction"),
  141. auto: z.boolean(),
  142. }).meta({
  143. ref: "CompactionPart",
  144. })
  145. export type CompactionPart = z.infer<typeof CompactionPart>
  146. export const SubtaskPart = PartBase.extend({
  147. type: z.literal("subtask"),
  148. prompt: z.string(),
  149. description: z.string(),
  150. agent: z.string(),
  151. command: z.string().optional(),
  152. })
  153. export type SubtaskPart = z.infer<typeof SubtaskPart>
  154. export const RetryPart = PartBase.extend({
  155. type: z.literal("retry"),
  156. attempt: z.number(),
  157. error: APIError.Schema,
  158. time: z.object({
  159. created: z.number(),
  160. }),
  161. }).meta({
  162. ref: "RetryPart",
  163. })
  164. export type RetryPart = z.infer<typeof RetryPart>
  165. export const StepStartPart = PartBase.extend({
  166. type: z.literal("step-start"),
  167. snapshot: z.string().optional(),
  168. }).meta({
  169. ref: "StepStartPart",
  170. })
  171. export type StepStartPart = z.infer<typeof StepStartPart>
  172. export const StepFinishPart = PartBase.extend({
  173. type: z.literal("step-finish"),
  174. reason: z.string(),
  175. snapshot: z.string().optional(),
  176. cost: z.number(),
  177. tokens: z.object({
  178. input: z.number(),
  179. output: z.number(),
  180. reasoning: z.number(),
  181. cache: z.object({
  182. read: z.number(),
  183. write: z.number(),
  184. }),
  185. }),
  186. }).meta({
  187. ref: "StepFinishPart",
  188. })
  189. export type StepFinishPart = z.infer<typeof StepFinishPart>
  190. export const ToolStatePending = z
  191. .object({
  192. status: z.literal("pending"),
  193. input: z.record(z.string(), z.any()),
  194. raw: z.string(),
  195. })
  196. .meta({
  197. ref: "ToolStatePending",
  198. })
  199. export type ToolStatePending = z.infer<typeof ToolStatePending>
  200. export const ToolStateRunning = z
  201. .object({
  202. status: z.literal("running"),
  203. input: z.record(z.string(), z.any()),
  204. title: z.string().optional(),
  205. metadata: z.record(z.string(), z.any()).optional(),
  206. time: z.object({
  207. start: z.number(),
  208. }),
  209. })
  210. .meta({
  211. ref: "ToolStateRunning",
  212. })
  213. export type ToolStateRunning = z.infer<typeof ToolStateRunning>
  214. export const ToolStateCompleted = z
  215. .object({
  216. status: z.literal("completed"),
  217. input: z.record(z.string(), z.any()),
  218. output: z.string(),
  219. title: z.string(),
  220. metadata: z.record(z.string(), z.any()),
  221. time: z.object({
  222. start: z.number(),
  223. end: z.number(),
  224. compacted: z.number().optional(),
  225. }),
  226. attachments: FilePart.array().optional(),
  227. })
  228. .meta({
  229. ref: "ToolStateCompleted",
  230. })
  231. export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
  232. export const ToolStateError = z
  233. .object({
  234. status: z.literal("error"),
  235. input: z.record(z.string(), z.any()),
  236. error: z.string(),
  237. metadata: z.record(z.string(), z.any()).optional(),
  238. time: z.object({
  239. start: z.number(),
  240. end: z.number(),
  241. }),
  242. })
  243. .meta({
  244. ref: "ToolStateError",
  245. })
  246. export type ToolStateError = z.infer<typeof ToolStateError>
  247. export const ToolState = z
  248. .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
  249. .meta({
  250. ref: "ToolState",
  251. })
  252. export const ToolPart = PartBase.extend({
  253. type: z.literal("tool"),
  254. callID: z.string(),
  255. tool: z.string(),
  256. state: ToolState,
  257. metadata: z.record(z.string(), z.any()).optional(),
  258. }).meta({
  259. ref: "ToolPart",
  260. })
  261. export type ToolPart = z.infer<typeof ToolPart>
  262. const Base = z.object({
  263. id: z.string(),
  264. sessionID: z.string(),
  265. })
  266. export const User = Base.extend({
  267. role: z.literal("user"),
  268. time: z.object({
  269. created: z.number(),
  270. }),
  271. summary: z
  272. .object({
  273. title: z.string().optional(),
  274. body: z.string().optional(),
  275. diffs: Snapshot.FileDiff.array(),
  276. })
  277. .optional(),
  278. agent: z.string(),
  279. model: z.object({
  280. providerID: z.string(),
  281. modelID: z.string(),
  282. }),
  283. system: z.string().optional(),
  284. tools: z.record(z.string(), z.boolean()).optional(),
  285. }).meta({
  286. ref: "UserMessage",
  287. })
  288. export type User = z.infer<typeof User>
  289. export const Part = z
  290. .discriminatedUnion("type", [
  291. TextPart,
  292. SubtaskPart,
  293. ReasoningPart,
  294. FilePart,
  295. ToolPart,
  296. StepStartPart,
  297. StepFinishPart,
  298. SnapshotPart,
  299. PatchPart,
  300. AgentPart,
  301. RetryPart,
  302. CompactionPart,
  303. ])
  304. .meta({
  305. ref: "Part",
  306. })
  307. export type Part = z.infer<typeof Part>
  308. export const Assistant = Base.extend({
  309. role: z.literal("assistant"),
  310. time: z.object({
  311. created: z.number(),
  312. completed: z.number().optional(),
  313. }),
  314. error: z
  315. .discriminatedUnion("name", [
  316. AuthError.Schema,
  317. NamedError.Unknown.Schema,
  318. OutputLengthError.Schema,
  319. AbortedError.Schema,
  320. APIError.Schema,
  321. ])
  322. .optional(),
  323. parentID: z.string(),
  324. modelID: z.string(),
  325. providerID: z.string(),
  326. /**
  327. * @deprecated
  328. */
  329. mode: z.string(),
  330. agent: z.string(),
  331. path: z.object({
  332. cwd: z.string(),
  333. root: z.string(),
  334. }),
  335. summary: z.boolean().optional(),
  336. cost: z.number(),
  337. tokens: z.object({
  338. input: z.number(),
  339. output: z.number(),
  340. reasoning: z.number(),
  341. cache: z.object({
  342. read: z.number(),
  343. write: z.number(),
  344. }),
  345. }),
  346. finish: z.string().optional(),
  347. }).meta({
  348. ref: "AssistantMessage",
  349. })
  350. export type Assistant = z.infer<typeof Assistant>
  351. export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({
  352. ref: "Message",
  353. })
  354. export type Info = z.infer<typeof Info>
  355. export const Event = {
  356. Updated: BusEvent.define(
  357. "message.updated",
  358. z.object({
  359. info: Info,
  360. }),
  361. ),
  362. Removed: BusEvent.define(
  363. "message.removed",
  364. z.object({
  365. sessionID: z.string(),
  366. messageID: z.string(),
  367. }),
  368. ),
  369. PartUpdated: BusEvent.define(
  370. "message.part.updated",
  371. z.object({
  372. part: Part,
  373. delta: z.string().optional(),
  374. }),
  375. ),
  376. PartRemoved: BusEvent.define(
  377. "message.part.removed",
  378. z.object({
  379. sessionID: z.string(),
  380. messageID: z.string(),
  381. partID: z.string(),
  382. }),
  383. ),
  384. }
  385. export const WithParts = z.object({
  386. info: Info,
  387. parts: z.array(Part),
  388. })
  389. export type WithParts = z.infer<typeof WithParts>
  390. export function toModelMessage(input: WithParts[]): ModelMessage[] {
  391. const result: UIMessage[] = []
  392. for (const msg of input) {
  393. if (msg.parts.length === 0) continue
  394. if (msg.info.role === "user") {
  395. const userMessage: UIMessage = {
  396. id: msg.info.id,
  397. role: "user",
  398. parts: [],
  399. }
  400. result.push(userMessage)
  401. for (const part of msg.parts) {
  402. if (part.type === "text" && !part.ignored)
  403. userMessage.parts.push({
  404. type: "text",
  405. text: part.text,
  406. })
  407. // text/plain and directory files are converted into text parts, ignore them
  408. if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory")
  409. userMessage.parts.push({
  410. type: "file",
  411. url: part.url,
  412. mediaType: part.mime,
  413. filename: part.filename,
  414. })
  415. if (part.type === "compaction") {
  416. userMessage.parts.push({
  417. type: "text",
  418. text: "What did we do so far?",
  419. })
  420. }
  421. if (part.type === "subtask") {
  422. userMessage.parts.push({
  423. type: "text",
  424. text: "The following tool was executed by the user",
  425. })
  426. }
  427. }
  428. }
  429. if (msg.info.role === "assistant") {
  430. if (
  431. msg.info.error &&
  432. !(
  433. MessageV2.AbortedError.isInstance(msg.info.error) &&
  434. msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
  435. )
  436. ) {
  437. continue
  438. }
  439. const assistantMessage: UIMessage = {
  440. id: msg.info.id,
  441. role: "assistant",
  442. parts: [],
  443. }
  444. result.push(assistantMessage)
  445. for (const part of msg.parts) {
  446. if (part.type === "text")
  447. assistantMessage.parts.push({
  448. type: "text",
  449. text: part.text,
  450. providerMetadata: part.metadata,
  451. })
  452. if (part.type === "step-start")
  453. assistantMessage.parts.push({
  454. type: "step-start",
  455. })
  456. if (part.type === "tool") {
  457. if (part.state.status === "completed") {
  458. if (part.state.attachments?.length) {
  459. result.push({
  460. id: Identifier.ascending("message"),
  461. role: "user",
  462. parts: [
  463. {
  464. type: "text",
  465. text: `Tool ${part.tool} returned an attachment:`,
  466. },
  467. ...part.state.attachments.map((attachment) => ({
  468. type: "file" as const,
  469. url: attachment.url,
  470. mediaType: attachment.mime,
  471. filename: attachment.filename,
  472. })),
  473. ],
  474. })
  475. }
  476. assistantMessage.parts.push({
  477. type: ("tool-" + part.tool) as `tool-${string}`,
  478. state: "output-available",
  479. toolCallId: part.callID,
  480. input: part.state.input,
  481. output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
  482. callProviderMetadata: part.metadata,
  483. })
  484. }
  485. if (part.state.status === "error")
  486. assistantMessage.parts.push({
  487. type: ("tool-" + part.tool) as `tool-${string}`,
  488. state: "output-error",
  489. toolCallId: part.callID,
  490. input: part.state.input,
  491. errorText: part.state.error,
  492. callProviderMetadata: part.metadata,
  493. })
  494. }
  495. if (part.type === "reasoning") {
  496. assistantMessage.parts.push({
  497. type: "reasoning",
  498. text: part.text,
  499. providerMetadata: part.metadata,
  500. })
  501. }
  502. }
  503. }
  504. }
  505. return convertToModelMessages(result.filter((msg) => msg.parts.length > 0))
  506. }
  507. export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
  508. const list = await Array.fromAsync(await Storage.list(["message", sessionID]))
  509. for (let i = list.length - 1; i >= 0; i--) {
  510. yield await get({
  511. sessionID,
  512. messageID: list[i][2],
  513. })
  514. }
  515. })
  516. export const parts = fn(Identifier.schema("message"), async (messageID) => {
  517. const result = [] as MessageV2.Part[]
  518. for (const item of await Storage.list(["part", messageID])) {
  519. const read = await Storage.read<MessageV2.Part>(item)
  520. result.push(read)
  521. }
  522. result.sort((a, b) => (a.id > b.id ? 1 : -1))
  523. return result
  524. })
  525. export const get = fn(
  526. z.object({
  527. sessionID: Identifier.schema("session"),
  528. messageID: Identifier.schema("message"),
  529. }),
  530. async (input) => {
  531. return {
  532. info: await Storage.read<MessageV2.Info>(["message", input.sessionID, input.messageID]),
  533. parts: await parts(input.messageID),
  534. }
  535. },
  536. )
  537. export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
  538. const result = [] as MessageV2.WithParts[]
  539. const completed = new Set<string>()
  540. for await (const msg of stream) {
  541. result.push(msg)
  542. if (
  543. msg.info.role === "user" &&
  544. completed.has(msg.info.id) &&
  545. msg.parts.some((part) => part.type === "compaction")
  546. )
  547. break
  548. if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish) completed.add(msg.info.parentID)
  549. }
  550. result.reverse()
  551. return result
  552. }
  553. export function fromError(e: unknown, ctx: { providerID: string }) {
  554. switch (true) {
  555. case e instanceof DOMException && e.name === "AbortError":
  556. return new MessageV2.AbortedError(
  557. { message: e.message },
  558. {
  559. cause: e,
  560. },
  561. ).toObject()
  562. case MessageV2.OutputLengthError.isInstance(e):
  563. return e
  564. case LoadAPIKeyError.isInstance(e):
  565. return new MessageV2.AuthError(
  566. {
  567. providerID: ctx.providerID,
  568. message: e.message,
  569. },
  570. { cause: e },
  571. ).toObject()
  572. case (e as SystemError)?.code === "ECONNRESET" &&
  573. (e as SystemError)?.message.includes("The socket connection was closed unexpectedly"):
  574. return new MessageV2.APIError(
  575. {
  576. message: "Connection reset by server",
  577. isRetryable: true,
  578. details: {
  579. code: (e as SystemError).code ?? "",
  580. syscall: (e as SystemError).syscall ?? "",
  581. message: (e as SystemError).message ?? "",
  582. },
  583. },
  584. { cause: e },
  585. ).toObject()
  586. case APICallError.isInstance(e):
  587. const message = iife(() => {
  588. let msg = e.message
  589. if (msg === "") {
  590. if (e.responseBody) return e.responseBody
  591. if (e.statusCode) {
  592. const err = STATUS_CODES[e.statusCode]
  593. if (err) return err
  594. }
  595. return "Unknown error"
  596. }
  597. const transformed = ProviderTransform.error(ctx.providerID, e)
  598. if (transformed !== msg) {
  599. return transformed
  600. }
  601. if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
  602. return msg
  603. }
  604. try {
  605. const body = JSON.parse(e.responseBody)
  606. // try to extract common error message fields
  607. const errMsg = body.message || body.error || body.error?.message
  608. if (errMsg && typeof errMsg === "string") {
  609. return `${msg}: ${errMsg}`
  610. }
  611. } catch {}
  612. return `${msg}: ${e.responseBody}`
  613. }).trim()
  614. return new MessageV2.APIError(
  615. {
  616. message,
  617. statusCode: e.statusCode,
  618. isRetryable: e.isRetryable,
  619. responseHeaders: e.responseHeaders,
  620. responseBody: e.responseBody,
  621. },
  622. { cause: e },
  623. ).toObject()
  624. case e instanceof Error:
  625. return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
  626. default:
  627. return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
  628. }
  629. }
  630. }