session.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. stepCountIs,
  10. streamText,
  11. type TextUIPart,
  12. type ToolInvocationUIPart,
  13. type UIDataTypes,
  14. type UIMessage,
  15. type UIMessagePart,
  16. } from "ai";
  17. import { z } from "zod";
  18. import * as tools from "../tool";
  19. import ANTHROPIC_PROMPT from "./prompt/anthropic.txt";
  20. import type { Tool } from "../tool/tool";
  21. import { Share } from "../share/share";
  22. export namespace Session {
  23. const log = Log.create({ service: "session" });
  24. export const Info = z.object({
  25. id: Identifier.schema("session"),
  26. shareID: z.string().optional(),
  27. title: z.string(),
  28. tokens: z.object({
  29. input: z.number(),
  30. output: z.number(),
  31. reasoning: z.number(),
  32. }),
  33. });
  34. export type Info = z.output<typeof Info>;
  35. export type Message = UIMessage<{
  36. time: {
  37. created: number;
  38. };
  39. sessionID: string;
  40. tool: Record<string, Tool.Metadata>;
  41. }>;
  42. const state = App.state("session", () => {
  43. const sessions = new Map<string, Info>();
  44. const messages = new Map<string, Message[]>();
  45. return {
  46. sessions,
  47. messages,
  48. };
  49. });
  50. export async function create() {
  51. const result: Info = {
  52. id: Identifier.descending("session"),
  53. title: "New Session - " + new Date().toISOString(),
  54. tokens: {
  55. input: 0,
  56. output: 0,
  57. reasoning: 0,
  58. },
  59. };
  60. log.info("created", result);
  61. await Storage.writeJSON("session/info/" + result.id, result);
  62. state().sessions.set(result.id, result);
  63. return result;
  64. }
  65. export async function get(id: string) {
  66. const result = state().sessions.get(id);
  67. if (result) {
  68. return result;
  69. }
  70. const read = await Storage.readJSON<Info>("session/info/" + id);
  71. state().sessions.set(id, read);
  72. return read as Info;
  73. }
  74. export async function share(id: string) {
  75. const session = await get(id);
  76. if (session.shareID) return session.shareID;
  77. const shareID = await Share.create(id);
  78. if (!shareID) return;
  79. session.shareID = shareID;
  80. await update(session);
  81. return shareID as string;
  82. }
  83. export async function update(session: Info) {
  84. state().sessions.set(session.id, session);
  85. await Storage.writeJSON("session/info/" + session.id, session);
  86. }
  87. export async function messages(sessionID: string) {
  88. const match = state().messages.get(sessionID);
  89. if (match) {
  90. return match;
  91. }
  92. const result = [] as Message[];
  93. const list = Storage.list("session/message/" + sessionID);
  94. for await (const p of list) {
  95. const read = await Storage.readJSON<Message>(p);
  96. result.push(read);
  97. }
  98. state().messages.set(sessionID, result);
  99. return result;
  100. }
  101. export async function* list() {
  102. for await (const item of Storage.list("session/info")) {
  103. yield path.basename(item, ".json");
  104. }
  105. }
  106. export async function chat(
  107. sessionID: string,
  108. ...parts: UIMessagePart<UIDataTypes>[]
  109. ) {
  110. const session = await get(sessionID);
  111. const l = log.clone().tag("session", sessionID);
  112. l.info("chatting");
  113. const msgs = await messages(sessionID);
  114. async function write(msg: Message) {
  115. return Storage.writeJSON(
  116. "session/message/" + sessionID + "/" + msg.id,
  117. msg,
  118. );
  119. }
  120. if (msgs.length === 0) {
  121. const system: Message = {
  122. id: Identifier.ascending("message"),
  123. role: "system",
  124. parts: [
  125. {
  126. type: "text",
  127. text: ANTHROPIC_PROMPT,
  128. },
  129. ],
  130. metadata: {
  131. sessionID,
  132. time: {
  133. created: Date.now(),
  134. },
  135. tool: {},
  136. },
  137. };
  138. msgs.push(system);
  139. state().messages.set(sessionID, msgs);
  140. await write(system);
  141. }
  142. const msg: Message = {
  143. role: "user",
  144. id: Identifier.ascending("message"),
  145. parts,
  146. metadata: {
  147. time: {
  148. created: Date.now(),
  149. },
  150. sessionID,
  151. tool: {},
  152. },
  153. };
  154. msgs.push(msg);
  155. await write(msg);
  156. const model = await LLM.findModel("claude-sonnet-4-20250514");
  157. const result = streamText({
  158. stopWhen: stepCountIs(1000),
  159. messages: convertToModelMessages(msgs),
  160. temperature: 0,
  161. tools,
  162. model,
  163. });
  164. const next: Message = {
  165. id: Identifier.ascending("message"),
  166. role: "assistant",
  167. parts: [],
  168. metadata: {
  169. time: {
  170. created: Date.now(),
  171. },
  172. sessionID,
  173. tool: {},
  174. },
  175. };
  176. msgs.push(next);
  177. let text: TextUIPart | undefined;
  178. const reader = result.toUIMessageStream().getReader();
  179. while (true) {
  180. const { done, value } = await reader.read();
  181. if (done) break;
  182. l.info("part", {
  183. type: value.type,
  184. });
  185. switch (value.type) {
  186. case "start":
  187. break;
  188. case "start-step":
  189. text = undefined;
  190. next.parts.push({
  191. type: "step-start",
  192. });
  193. break;
  194. case "text":
  195. if (!text) {
  196. text = value;
  197. next.parts.push(value);
  198. break;
  199. }
  200. text.text += value.text;
  201. break;
  202. case "tool-call":
  203. next.parts.push({
  204. type: "tool-invocation",
  205. toolInvocation: {
  206. state: "call",
  207. ...value,
  208. },
  209. });
  210. break;
  211. case "tool-result":
  212. const match = next.parts.find(
  213. (p) =>
  214. p.type === "tool-invocation" &&
  215. p.toolInvocation.toolCallId === value.toolCallId,
  216. ) as ToolInvocationUIPart | undefined;
  217. if (match) {
  218. const { output, metadata } = value.result as any;
  219. next.metadata!.tool[value.toolCallId] = metadata;
  220. match.toolInvocation = {
  221. ...match.toolInvocation,
  222. state: "result",
  223. result: output,
  224. };
  225. }
  226. break;
  227. case "finish":
  228. break;
  229. case "finish-step":
  230. break;
  231. case "error":
  232. log.error("error", value);
  233. break;
  234. default:
  235. l.info("unhandled", {
  236. type: value.type,
  237. });
  238. }
  239. await write(next);
  240. }
  241. const usage = await result.totalUsage;
  242. session.tokens.input += usage.inputTokens || 0;
  243. session.tokens.output += usage.outputTokens || 0;
  244. session.tokens.reasoning += usage.reasoningTokens || 0;
  245. console.log(session);
  246. await update(session);
  247. return next;
  248. }
  249. }