session.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import path from "path";
  2. import { App } from "../app/";
  3. import { Identifier } from "../id/id";
  4. import { LLM } from "../llm/llm";
  5. import { Storage } from "../storage/storage";
  6. import { Log } from "../util/log";
  7. import {
  8. convertToModelMessages,
  9. generateText,
  10. stepCountIs,
  11. streamText,
  12. } from "ai";
  13. import { z } from "zod";
  14. import * as tools from "../tool";
  15. import { Decimal } from "decimal.js";
  16. import PROMPT_ANTHROPIC from "./prompt/anthropic.txt";
  17. import PROMPT_TITLE from "./prompt/title.txt";
  18. import { Share } from "../share/share";
  19. import { Message } from "./message";
  20. import { Bus } from "../bus";
  21. export namespace Session {
  22. const log = Log.create({ service: "session" });
  23. export const Info = z.object({
  24. id: Identifier.schema("session"),
  25. shareID: z.string().optional(),
  26. title: z.string(),
  27. });
  28. export type Info = z.output<typeof Info>;
  29. export const Event = {
  30. Updated: Bus.event(
  31. "session.updated",
  32. z.object({
  33. sessionID: z.string(),
  34. }),
  35. ),
  36. };
  37. const state = App.state("session", () => {
  38. const sessions = new Map<string, Info>();
  39. const messages = new Map<string, Message.Info[]>();
  40. return {
  41. sessions,
  42. messages,
  43. };
  44. });
  45. export async function create() {
  46. const result: Info = {
  47. id: Identifier.descending("session"),
  48. title: "New Session - " + new Date().toISOString(),
  49. };
  50. log.info("created", result);
  51. state().sessions.set(result.id, result);
  52. await Storage.writeJSON("session/info/" + result.id, result);
  53. await share(result.id);
  54. Bus.publish(Event.Updated, {
  55. sessionID: result.id,
  56. });
  57. return result;
  58. }
  59. export async function get(id: string) {
  60. const result = state().sessions.get(id);
  61. if (result) {
  62. return result;
  63. }
  64. const read = await Storage.readJSON<Info>("session/info/" + id);
  65. state().sessions.set(id, read);
  66. return read as Info;
  67. }
  68. export async function share(id: string) {
  69. const session = await get(id);
  70. if (session.shareID) return session.shareID;
  71. const shareID = await Share.create(id);
  72. if (!shareID) return;
  73. await update(id, (draft) => {
  74. draft.shareID = shareID;
  75. });
  76. return shareID as string;
  77. }
  78. export async function update(id: string, editor: (session: Info) => void) {
  79. const { sessions } = state();
  80. const session = await get(id);
  81. if (!session) return;
  82. editor(session);
  83. sessions.set(id, session);
  84. await Storage.writeJSON("session/info/" + id, session);
  85. Bus.publish(Event.Updated, {
  86. sessionID: id,
  87. });
  88. return session;
  89. }
  90. export async function messages(sessionID: string) {
  91. const match = state().messages.get(sessionID);
  92. if (match) {
  93. return match;
  94. }
  95. const result = [] as Message.Info[];
  96. const list = Storage.list("session/message/" + sessionID);
  97. for await (const p of list) {
  98. const read = await Storage.readJSON<Message.Info>(p);
  99. result.push(read);
  100. }
  101. state().messages.set(sessionID, result);
  102. return result;
  103. }
  104. export async function* list() {
  105. for await (const item of Storage.list("session/info")) {
  106. const sessionID = path.basename(item, ".json");
  107. yield get(sessionID);
  108. }
  109. }
  110. const pending = new Map<string, AbortController>();
  111. export function abort(sessionID: string) {
  112. const controller = pending.get(sessionID);
  113. if (!controller) return false;
  114. controller.abort();
  115. pending.delete(sessionID);
  116. return true;
  117. }
  118. export async function chat(input: {
  119. sessionID: string;
  120. providerID: string;
  121. modelID: string;
  122. parts: Message.Part[];
  123. }) {
  124. const l = log.clone().tag("session", input.sessionID);
  125. l.info("chatting");
  126. const model = await LLM.findModel(input.providerID, input.modelID);
  127. const msgs = await messages(input.sessionID);
  128. async function write(msg: Message.Info) {
  129. await Storage.writeJSON(
  130. "session/message/" + input.sessionID + "/" + msg.id,
  131. msg,
  132. );
  133. Bus.publish(Message.Event.Updated, {
  134. sessionID: input.sessionID,
  135. messageID: msg.id,
  136. });
  137. }
  138. const app = await App.use();
  139. if (msgs.length === 0) {
  140. const system: Message.Info = {
  141. id: Identifier.ascending("message"),
  142. role: "system",
  143. parts: [
  144. {
  145. type: "text",
  146. text: PROMPT_ANTHROPIC,
  147. },
  148. ],
  149. metadata: {
  150. sessionID: input.sessionID,
  151. time: {
  152. created: Date.now(),
  153. },
  154. tool: {},
  155. },
  156. };
  157. const contextFile = Bun.file(path.join(app.root, "CONTEXT.md"));
  158. if (await contextFile.exists()) {
  159. const context = await contextFile.text();
  160. system.parts.push({
  161. type: "text",
  162. text: context,
  163. });
  164. }
  165. msgs.push(system);
  166. state().messages.set(input.sessionID, msgs);
  167. generateText({
  168. messages: convertToModelMessages([
  169. {
  170. role: "system",
  171. parts: [
  172. {
  173. type: "text",
  174. text: PROMPT_TITLE,
  175. },
  176. ],
  177. },
  178. {
  179. role: "user",
  180. parts: input.parts,
  181. },
  182. ]),
  183. model: model.instance,
  184. }).then((result) => {
  185. return Session.update(input.sessionID, (draft) => {
  186. draft.title = result.text;
  187. });
  188. });
  189. await write(system);
  190. }
  191. const msg: Message.Info = {
  192. role: "user",
  193. id: Identifier.ascending("message"),
  194. parts: input.parts,
  195. metadata: {
  196. time: {
  197. created: Date.now(),
  198. },
  199. sessionID: input.sessionID,
  200. tool: {},
  201. },
  202. };
  203. msgs.push(msg);
  204. await write(msg);
  205. const next: Message.Info = {
  206. id: Identifier.ascending("message"),
  207. role: "assistant",
  208. parts: [],
  209. metadata: {
  210. assistant: {
  211. cost: 0,
  212. tokens: {
  213. input: 0,
  214. output: 0,
  215. reasoning: 0,
  216. },
  217. modelID: input.modelID,
  218. providerID: input.providerID,
  219. },
  220. time: {
  221. created: Date.now(),
  222. },
  223. sessionID: input.sessionID,
  224. tool: {},
  225. },
  226. };
  227. const controller = new AbortController();
  228. pending.set(input.sessionID, controller);
  229. const result = streamText({
  230. onStepFinish: async (step) => {
  231. const assistant = next.metadata!.assistant!;
  232. assistant.tokens.input = step.usage.inputTokens ?? 0;
  233. assistant.tokens.output = step.usage.outputTokens ?? 0;
  234. assistant.tokens.reasoning = step.usage.reasoningTokens ?? 0;
  235. assistant.cost = new Decimal(0)
  236. .add(new Decimal(assistant.tokens.input).mul(model.info.cost.input))
  237. .add(new Decimal(assistant.tokens.output).mul(model.info.cost.output))
  238. .toNumber();
  239. await write(next);
  240. },
  241. abortSignal: controller.signal,
  242. maxRetries: 6,
  243. stopWhen: stepCountIs(1000),
  244. messages: convertToModelMessages(msgs),
  245. temperature: 0,
  246. tools,
  247. model: model.instance,
  248. });
  249. msgs.push(next);
  250. let text: Message.TextPart | undefined;
  251. const reader = result.toUIMessageStream().getReader();
  252. while (true) {
  253. const result = await reader.read().catch((e) => {
  254. if (e instanceof DOMException && e.name === "AbortError") {
  255. return;
  256. }
  257. throw e;
  258. });
  259. if (!result) break;
  260. const { done, value } = result;
  261. if (done) break;
  262. l.info("part", {
  263. type: value.type,
  264. });
  265. switch (value.type) {
  266. case "start":
  267. break;
  268. case "start-step":
  269. text = undefined;
  270. next.parts.push({
  271. type: "step-start",
  272. });
  273. break;
  274. case "text":
  275. if (!text) {
  276. text = value;
  277. next.parts.push(value);
  278. break;
  279. }
  280. text.text += value.text;
  281. break;
  282. case "tool-call":
  283. next.parts.push({
  284. type: "tool-invocation",
  285. toolInvocation: {
  286. state: "call",
  287. ...value,
  288. // hack until zod v4
  289. args: value.args as any,
  290. },
  291. });
  292. break;
  293. case "tool-result":
  294. const match = next.parts.find(
  295. (p) =>
  296. p.type === "tool-invocation" &&
  297. p.toolInvocation.toolCallId === value.toolCallId,
  298. );
  299. if (match && match.type === "tool-invocation") {
  300. const { output, metadata } = value.result as any;
  301. next.metadata!.tool[value.toolCallId] = metadata;
  302. match.toolInvocation = {
  303. ...match.toolInvocation,
  304. state: "result",
  305. result: output,
  306. };
  307. }
  308. break;
  309. case "finish":
  310. break;
  311. case "finish-step":
  312. break;
  313. case "error":
  314. log.error("error", value);
  315. break;
  316. default:
  317. l.info("unhandled", {
  318. type: value.type,
  319. });
  320. }
  321. await write(next);
  322. }
  323. pending.delete(input.sessionID);
  324. next.metadata!.time.completed = Date.now();
  325. await write(next);
  326. return next;
  327. }
  328. }