message-v2.ts 20 KB

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