index.ts 13 KB

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