index.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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. export namespace Session {
  19. const log = Log.create({ service: "session" })
  20. const parentSessionTitlePrefix = "New session - "
  21. const childSessionTitlePrefix = "Child session - "
  22. function createDefaultTitle(isChild = false) {
  23. return (isChild ? childSessionTitlePrefix : parentSessionTitlePrefix) + new Date().toISOString()
  24. }
  25. export const Info = z
  26. .object({
  27. id: Identifier.schema("session"),
  28. projectID: z.string(),
  29. directory: z.string(),
  30. parentID: Identifier.schema("session").optional(),
  31. share: z
  32. .object({
  33. url: z.string(),
  34. })
  35. .optional(),
  36. title: z.string(),
  37. version: z.string(),
  38. time: z.object({
  39. created: z.number(),
  40. updated: z.number(),
  41. compacting: z.number().optional(),
  42. }),
  43. revert: z
  44. .object({
  45. messageID: z.string(),
  46. partID: z.string().optional(),
  47. snapshot: z.string().optional(),
  48. diff: z.string().optional(),
  49. })
  50. .optional(),
  51. })
  52. .meta({
  53. ref: "Session",
  54. })
  55. export type Info = z.output<typeof Info>
  56. export const ShareInfo = z
  57. .object({
  58. secret: z.string(),
  59. url: z.string(),
  60. })
  61. .meta({
  62. ref: "SessionShare",
  63. })
  64. export type ShareInfo = z.output<typeof ShareInfo>
  65. export const Event = {
  66. Updated: Bus.event(
  67. "session.updated",
  68. z.object({
  69. info: Info,
  70. }),
  71. ),
  72. Deleted: Bus.event(
  73. "session.deleted",
  74. z.object({
  75. info: Info,
  76. }),
  77. ),
  78. Error: Bus.event(
  79. "session.error",
  80. z.object({
  81. sessionID: z.string().optional(),
  82. error: MessageV2.Assistant.shape.error,
  83. }),
  84. ),
  85. }
  86. export async function create(parentID?: string, title?: string) {
  87. return createNext({
  88. parentID,
  89. directory: Instance.directory,
  90. title,
  91. })
  92. }
  93. export async function touch(sessionID: string) {
  94. await update(sessionID, (draft) => {
  95. draft.time.updated = Date.now()
  96. })
  97. }
  98. export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
  99. const result: Info = {
  100. id: Identifier.descending("session", input.id),
  101. version: Installation.VERSION,
  102. projectID: Instance.project.id,
  103. directory: input.directory,
  104. parentID: input.parentID,
  105. title: input.title ?? createDefaultTitle(!!input.parentID),
  106. time: {
  107. created: Date.now(),
  108. updated: Date.now(),
  109. },
  110. }
  111. log.info("created", result)
  112. await Storage.write(["session", Instance.project.id, result.id], result)
  113. const cfg = await Config.get()
  114. if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto"))
  115. share(result.id)
  116. .then((share) => {
  117. update(result.id, (draft) => {
  118. draft.share = share
  119. })
  120. })
  121. .catch(() => {
  122. // Silently ignore sharing errors during session creation
  123. })
  124. Bus.publish(Event.Updated, {
  125. info: result,
  126. })
  127. return result
  128. }
  129. export async function get(id: string) {
  130. const read = await Storage.read<Info>(["session", Instance.project.id, id])
  131. return read as Info
  132. }
  133. export async function getShare(id: string) {
  134. return Storage.read<ShareInfo>(["share", id])
  135. }
  136. export async function share(id: string) {
  137. const cfg = await Config.get()
  138. if (cfg.share === "disabled") {
  139. throw new Error("Sharing is disabled in configuration")
  140. }
  141. const session = await get(id)
  142. if (session.share) return session.share
  143. const share = await Share.create(id)
  144. await update(id, (draft) => {
  145. draft.share = {
  146. url: share.url,
  147. }
  148. })
  149. await Storage.write(["share", id], share)
  150. await Share.sync("session/info/" + id, session)
  151. for (const msg of await messages(id)) {
  152. await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info)
  153. for (const part of msg.parts) {
  154. await Share.sync("session/part/" + id + "/" + msg.info.id + "/" + part.id, part)
  155. }
  156. }
  157. return share
  158. }
  159. export async function unshare(id: string) {
  160. const share = await getShare(id)
  161. if (!share) return
  162. await Storage.remove(["share", id])
  163. await update(id, (draft) => {
  164. draft.share = undefined
  165. })
  166. await Share.remove(id, share.secret)
  167. }
  168. export async function update(id: string, editor: (session: Info) => void) {
  169. const project = Instance.project
  170. const result = await Storage.update<Info>(["session", project.id, id], (draft) => {
  171. editor(draft)
  172. draft.time.updated = Date.now()
  173. })
  174. Bus.publish(Event.Updated, {
  175. info: result,
  176. })
  177. return result
  178. }
  179. export async function messages(sessionID: string) {
  180. const result = [] as MessageV2.WithParts[]
  181. for (const p of await Storage.list(["message", sessionID])) {
  182. const read = await Storage.read<MessageV2.Info>(p)
  183. result.push({
  184. info: read,
  185. parts: await getParts(read.id),
  186. })
  187. }
  188. result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1))
  189. return result
  190. }
  191. export async function getMessage(sessionID: string, messageID: string) {
  192. return {
  193. info: await Storage.read<MessageV2.Info>(["message", sessionID, messageID]),
  194. parts: await getParts(messageID),
  195. }
  196. }
  197. export async function getParts(messageID: string) {
  198. const result = [] as MessageV2.Part[]
  199. for (const item of await Storage.list(["part", messageID])) {
  200. const read = await Storage.read<MessageV2.Part>(item)
  201. result.push(read)
  202. }
  203. result.sort((a, b) => (a.id > b.id ? 1 : -1))
  204. return result
  205. }
  206. export async function* list() {
  207. const project = Instance.project
  208. for (const item of await Storage.list(["session", project.id])) {
  209. yield Storage.read<Info>(item)
  210. }
  211. }
  212. export async function children(parentID: string) {
  213. const project = Instance.project
  214. const result = [] as Session.Info[]
  215. for (const item of await Storage.list(["session", project.id])) {
  216. const session = await Storage.read<Info>(item)
  217. if (session.parentID !== parentID) continue
  218. result.push(session)
  219. }
  220. return result
  221. }
  222. export async function remove(sessionID: string, emitEvent = true) {
  223. const project = Instance.project
  224. try {
  225. const session = await get(sessionID)
  226. for (const child of await children(sessionID)) {
  227. await remove(child.id, false)
  228. }
  229. await unshare(sessionID).catch(() => {})
  230. for (const msg of await Storage.list(["message", sessionID])) {
  231. for (const part of await Storage.list(["part", msg.at(-1)!])) {
  232. await Storage.remove(part)
  233. }
  234. await Storage.remove(msg)
  235. }
  236. await Storage.remove(["session", project.id, sessionID])
  237. if (emitEvent) {
  238. Bus.publish(Event.Deleted, {
  239. info: session,
  240. })
  241. }
  242. } catch (e) {
  243. log.error(e)
  244. }
  245. }
  246. export async function updateMessage(msg: MessageV2.Info) {
  247. await Storage.write(["message", msg.sessionID, msg.id], msg)
  248. Bus.publish(MessageV2.Event.Updated, {
  249. info: msg,
  250. })
  251. return msg
  252. }
  253. export async function removeMessage(sessionID: string, messageID: string) {
  254. await Storage.remove(["message", sessionID, messageID])
  255. Bus.publish(MessageV2.Event.Removed, {
  256. sessionID,
  257. messageID,
  258. })
  259. return messageID
  260. }
  261. export async function updatePart(part: MessageV2.Part) {
  262. await Storage.write(["part", part.messageID, part.id], part)
  263. Bus.publish(MessageV2.Event.PartUpdated, {
  264. part,
  265. })
  266. return part
  267. }
  268. export function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) {
  269. const tokens = {
  270. input: usage.inputTokens ?? 0,
  271. output: usage.outputTokens ?? 0,
  272. reasoning: usage?.reasoningTokens ?? 0,
  273. cache: {
  274. write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
  275. // @ts-expect-error
  276. metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
  277. 0) as number,
  278. read: usage.cachedInputTokens ?? 0,
  279. },
  280. }
  281. return {
  282. cost: new Decimal(0)
  283. .add(new Decimal(tokens.input).mul(model.cost?.input ?? 0).div(1_000_000))
  284. .add(new Decimal(tokens.output).mul(model.cost?.output ?? 0).div(1_000_000))
  285. .add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000))
  286. .add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000))
  287. .toNumber(),
  288. tokens,
  289. }
  290. }
  291. export class BusyError extends Error {
  292. constructor(public readonly sessionID: string) {
  293. super(`Session ${sessionID} is busy`)
  294. }
  295. }
  296. export async function initialize(input: {
  297. sessionID: string
  298. modelID: string
  299. providerID: string
  300. messageID: string
  301. }) {
  302. await SessionPrompt.prompt({
  303. sessionID: input.sessionID,
  304. messageID: input.messageID,
  305. model: {
  306. providerID: input.providerID,
  307. modelID: input.modelID,
  308. },
  309. parts: [
  310. {
  311. id: Identifier.ascending("part"),
  312. type: "text",
  313. text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
  314. },
  315. ],
  316. })
  317. await Project.setInitialized(Instance.project.id)
  318. }
  319. }