index.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import { Decimal } from "decimal.js"
  4. import z from "zod"
  5. import { type LanguageModelUsage, type ProviderMetadata } from "ai"
  6. import { Config } from "../config/config"
  7. import { Flag } from "../flag/flag"
  8. import { Identifier } from "../id/id"
  9. import { Installation } from "../installation"
  10. import { Storage } from "../storage/storage"
  11. import { Log } from "../util/log"
  12. import { MessageV2 } from "./message-v2"
  13. import { Instance } from "../project/instance"
  14. import { SessionPrompt } from "./prompt"
  15. import { fn } from "@/util/fn"
  16. import { Command } from "../command"
  17. import { Snapshot } from "@/snapshot"
  18. import type { Provider } from "@/provider/provider"
  19. export namespace Session {
  20. const log = Log.create({ service: "session" })
  21. const parentTitlePrefix = "New session - "
  22. const childTitlePrefix = "Child session - "
  23. function createDefaultTitle(isChild = false) {
  24. return (isChild ? childTitlePrefix : parentTitlePrefix) + new Date().toISOString()
  25. }
  26. export function isDefaultTitle(title: string) {
  27. return new RegExp(
  28. `^(${parentTitlePrefix}|${childTitlePrefix})\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$`,
  29. ).test(title)
  30. }
  31. export const Info = z
  32. .object({
  33. id: Identifier.schema("session"),
  34. projectID: z.string(),
  35. directory: z.string(),
  36. parentID: Identifier.schema("session").optional(),
  37. summary: z
  38. .object({
  39. additions: z.number(),
  40. deletions: z.number(),
  41. files: z.number(),
  42. diffs: Snapshot.FileDiff.array().optional(),
  43. })
  44. .optional(),
  45. share: z
  46. .object({
  47. url: z.string(),
  48. })
  49. .optional(),
  50. title: z.string(),
  51. version: z.string(),
  52. time: z.object({
  53. created: z.number(),
  54. updated: z.number(),
  55. compacting: z.number().optional(),
  56. archived: z.number().optional(),
  57. }),
  58. revert: z
  59. .object({
  60. messageID: z.string(),
  61. partID: z.string().optional(),
  62. snapshot: z.string().optional(),
  63. diff: z.string().optional(),
  64. })
  65. .optional(),
  66. })
  67. .meta({
  68. ref: "Session",
  69. })
  70. export type Info = z.output<typeof Info>
  71. export const ShareInfo = z
  72. .object({
  73. secret: z.string(),
  74. url: z.string(),
  75. })
  76. .meta({
  77. ref: "SessionShare",
  78. })
  79. export type ShareInfo = z.output<typeof ShareInfo>
  80. export const Event = {
  81. Created: BusEvent.define(
  82. "session.created",
  83. z.object({
  84. info: Info,
  85. }),
  86. ),
  87. Updated: BusEvent.define(
  88. "session.updated",
  89. z.object({
  90. info: Info,
  91. }),
  92. ),
  93. Deleted: BusEvent.define(
  94. "session.deleted",
  95. z.object({
  96. info: Info,
  97. }),
  98. ),
  99. Diff: BusEvent.define(
  100. "session.diff",
  101. z.object({
  102. sessionID: z.string(),
  103. diff: Snapshot.FileDiff.array(),
  104. }),
  105. ),
  106. Error: BusEvent.define(
  107. "session.error",
  108. z.object({
  109. sessionID: z.string().optional(),
  110. error: MessageV2.Assistant.shape.error,
  111. }),
  112. ),
  113. }
  114. export const create = fn(
  115. z
  116. .object({
  117. parentID: Identifier.schema("session").optional(),
  118. title: z.string().optional(),
  119. })
  120. .optional(),
  121. async (input) => {
  122. return createNext({
  123. parentID: input?.parentID,
  124. directory: Instance.directory,
  125. title: input?.title,
  126. })
  127. },
  128. )
  129. export const fork = fn(
  130. z.object({
  131. sessionID: Identifier.schema("session"),
  132. messageID: Identifier.schema("message").optional(),
  133. }),
  134. async (input) => {
  135. const session = await createNext({
  136. directory: Instance.directory,
  137. })
  138. const msgs = await messages({ sessionID: input.sessionID })
  139. for (const msg of msgs) {
  140. if (input.messageID && msg.info.id >= input.messageID) break
  141. const cloned = await updateMessage({
  142. ...msg.info,
  143. sessionID: session.id,
  144. id: Identifier.ascending("message"),
  145. })
  146. for (const part of msg.parts) {
  147. await updatePart({
  148. ...part,
  149. id: Identifier.ascending("part"),
  150. messageID: cloned.id,
  151. sessionID: session.id,
  152. })
  153. }
  154. }
  155. return session
  156. },
  157. )
  158. export const touch = fn(Identifier.schema("session"), async (sessionID) => {
  159. await update(sessionID, (draft) => {
  160. draft.time.updated = Date.now()
  161. })
  162. })
  163. export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
  164. const result: Info = {
  165. id: Identifier.descending("session", input.id),
  166. version: Installation.VERSION,
  167. projectID: Instance.project.id,
  168. directory: input.directory,
  169. parentID: input.parentID,
  170. title: input.title ?? createDefaultTitle(!!input.parentID),
  171. time: {
  172. created: Date.now(),
  173. updated: Date.now(),
  174. },
  175. }
  176. log.info("created", result)
  177. await Storage.write(["session", Instance.project.id, result.id], result)
  178. Bus.publish(Event.Created, {
  179. info: result,
  180. })
  181. const cfg = await Config.get()
  182. if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
  183. share(result.id)
  184. .then((share) => {
  185. update(result.id, (draft) => {
  186. draft.share = share
  187. })
  188. })
  189. .catch(() => {
  190. // Silently ignore sharing errors during session creation
  191. })
  192. Bus.publish(Event.Updated, {
  193. info: result,
  194. })
  195. return result
  196. }
  197. export const get = fn(Identifier.schema("session"), async (id) => {
  198. const read = await Storage.read<Info>(["session", Instance.project.id, id])
  199. return read as Info
  200. })
  201. export const getShare = fn(Identifier.schema("session"), async (id) => {
  202. return Storage.read<ShareInfo>(["share", id])
  203. })
  204. export const share = fn(Identifier.schema("session"), async (id) => {
  205. const cfg = await Config.get()
  206. if (cfg.share === "disabled") {
  207. throw new Error("Sharing is disabled in configuration")
  208. }
  209. const { ShareNext } = await import("@/share/share-next")
  210. const share = await ShareNext.create(id)
  211. await update(id, (draft) => {
  212. draft.share = {
  213. url: share.url,
  214. }
  215. })
  216. return share
  217. })
  218. export const unshare = fn(Identifier.schema("session"), async (id) => {
  219. const cfg = await Config.get()
  220. if (cfg.enterprise?.url) {
  221. const { ShareNext } = await import("@/share/share-next")
  222. await ShareNext.remove(id)
  223. await update(id, (draft) => {
  224. draft.share = undefined
  225. })
  226. }
  227. const share = await getShare(id)
  228. if (!share) return
  229. await Storage.remove(["share", id])
  230. await update(id, (draft) => {
  231. draft.share = undefined
  232. })
  233. const { Share } = await import("../share/share")
  234. await Share.remove(id, share.secret)
  235. })
  236. export async function update(id: string, editor: (session: Info) => void) {
  237. const project = Instance.project
  238. const result = await Storage.update<Info>(["session", project.id, id], (draft) => {
  239. editor(draft)
  240. draft.time.updated = Date.now()
  241. })
  242. Bus.publish(Event.Updated, {
  243. info: result,
  244. })
  245. return result
  246. }
  247. export const diff = fn(Identifier.schema("session"), async (sessionID) => {
  248. const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
  249. return diffs ?? []
  250. })
  251. export const messages = fn(
  252. z.object({
  253. sessionID: Identifier.schema("session"),
  254. limit: z.number().optional(),
  255. }),
  256. async (input) => {
  257. const result = [] as MessageV2.WithParts[]
  258. for await (const msg of MessageV2.stream(input.sessionID)) {
  259. if (input.limit && result.length >= input.limit) break
  260. result.push(msg)
  261. }
  262. result.reverse()
  263. return result
  264. },
  265. )
  266. export async function* list() {
  267. const project = Instance.project
  268. for (const item of await Storage.list(["session", project.id])) {
  269. yield Storage.read<Info>(item)
  270. }
  271. }
  272. export const children = fn(Identifier.schema("session"), async (parentID) => {
  273. const project = Instance.project
  274. const result = [] as Session.Info[]
  275. for (const item of await Storage.list(["session", project.id])) {
  276. const session = await Storage.read<Info>(item)
  277. if (session.parentID !== parentID) continue
  278. result.push(session)
  279. }
  280. return result
  281. })
  282. export const remove = fn(Identifier.schema("session"), async (sessionID) => {
  283. const project = Instance.project
  284. try {
  285. const session = await get(sessionID)
  286. for (const child of await children(sessionID)) {
  287. await remove(child.id)
  288. }
  289. await unshare(sessionID).catch(() => {})
  290. for (const msg of await Storage.list(["message", sessionID])) {
  291. for (const part of await Storage.list(["part", msg.at(-1)!])) {
  292. await Storage.remove(part)
  293. }
  294. await Storage.remove(msg)
  295. }
  296. await Storage.remove(["session", project.id, sessionID])
  297. Bus.publish(Event.Deleted, {
  298. info: session,
  299. })
  300. } catch (e) {
  301. log.error(e)
  302. }
  303. })
  304. export const updateMessage = fn(MessageV2.Info, async (msg) => {
  305. await Storage.write(["message", msg.sessionID, msg.id], msg)
  306. Bus.publish(MessageV2.Event.Updated, {
  307. info: msg,
  308. })
  309. return msg
  310. })
  311. export const removeMessage = fn(
  312. z.object({
  313. sessionID: Identifier.schema("session"),
  314. messageID: Identifier.schema("message"),
  315. }),
  316. async (input) => {
  317. await Storage.remove(["message", input.sessionID, input.messageID])
  318. Bus.publish(MessageV2.Event.Removed, {
  319. sessionID: input.sessionID,
  320. messageID: input.messageID,
  321. })
  322. return input.messageID
  323. },
  324. )
  325. const UpdatePartInput = z.union([
  326. MessageV2.Part,
  327. z.object({
  328. part: MessageV2.TextPart,
  329. delta: z.string(),
  330. }),
  331. z.object({
  332. part: MessageV2.ReasoningPart,
  333. delta: z.string(),
  334. }),
  335. ])
  336. export const updatePart = fn(UpdatePartInput, async (input) => {
  337. const part = "delta" in input ? input.part : input
  338. const delta = "delta" in input ? input.delta : undefined
  339. await Storage.write(["part", part.messageID, part.id], part)
  340. Bus.publish(MessageV2.Event.PartUpdated, {
  341. part,
  342. delta,
  343. })
  344. return part
  345. })
  346. export const getUsage = fn(
  347. z.object({
  348. model: z.custom<Provider.Model>(),
  349. usage: z.custom<LanguageModelUsage>(),
  350. metadata: z.custom<ProviderMetadata>().optional(),
  351. }),
  352. (input) => {
  353. const cachedInputTokens = input.usage.cachedInputTokens ?? 0
  354. const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"])
  355. const adjustedInputTokens = excludesCachedTokens
  356. ? (input.usage.inputTokens ?? 0)
  357. : (input.usage.inputTokens ?? 0) - cachedInputTokens
  358. const safe = (value: number) => {
  359. if (!Number.isFinite(value)) return 0
  360. return value
  361. }
  362. const tokens = {
  363. input: safe(adjustedInputTokens),
  364. output: safe(input.usage.outputTokens ?? 0),
  365. reasoning: safe(input.usage?.reasoningTokens ?? 0),
  366. cache: {
  367. write: safe(
  368. (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
  369. // @ts-expect-error
  370. input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
  371. 0) as number,
  372. ),
  373. read: safe(cachedInputTokens),
  374. },
  375. }
  376. const costInfo =
  377. input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
  378. ? input.model.cost.experimentalOver200K
  379. : input.model.cost
  380. return {
  381. cost: safe(
  382. new Decimal(0)
  383. .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
  384. .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
  385. .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000))
  386. .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000))
  387. // TODO: update models.dev to have better pricing model, for now:
  388. // charge reasoning tokens at the same rate as output tokens
  389. .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
  390. .toNumber(),
  391. ),
  392. tokens,
  393. }
  394. },
  395. )
  396. export class BusyError extends Error {
  397. constructor(public readonly sessionID: string) {
  398. super(`Session ${sessionID} is busy`)
  399. }
  400. }
  401. export const initialize = fn(
  402. z.object({
  403. sessionID: Identifier.schema("session"),
  404. modelID: z.string(),
  405. providerID: z.string(),
  406. messageID: Identifier.schema("message"),
  407. }),
  408. async (input) => {
  409. await SessionPrompt.command({
  410. sessionID: input.sessionID,
  411. messageID: input.messageID,
  412. model: input.providerID + "/" + input.modelID,
  413. command: Command.Default.INIT,
  414. arguments: "",
  415. })
  416. },
  417. )
  418. }