opencode/js/src/session/session.ts

353 lines
9.1 KiB
TypeScript
Raw Normal View History

2025-05-29 05:24:15 +08:00
import path from "path";
2025-05-18 14:43:01 +08:00
import { App } from "../app/";
2025-05-18 09:31:42 +08:00
import { Identifier } from "../id/id";
2025-05-18 14:43:01 +08:00
import { LLM } from "../llm/llm";
2025-05-18 09:31:42 +08:00
import { Storage } from "../storage/storage";
import { Log } from "../util/log";
2025-05-18 14:43:01 +08:00
import {
convertToModelMessages,
2025-05-29 05:24:15 +08:00
generateText,
2025-05-27 02:09:17 +08:00
stepCountIs,
2025-05-18 14:43:01 +08:00
streamText,
type TextUIPart,
type ToolInvocationUIPart,
2025-05-19 02:13:04 +08:00
type UIDataTypes,
2025-05-18 14:43:01 +08:00
type UIMessage,
2025-05-19 02:13:04 +08:00
type UIMessagePart,
2025-05-18 14:43:01 +08:00
} from "ai";
2025-05-19 10:30:41 +08:00
import { z } from "zod";
2025-05-21 22:30:39 +08:00
import * as tools from "../tool";
2025-05-29 01:57:02 +08:00
import { Decimal } from "decimal.js";
2025-05-18 09:31:42 +08:00
2025-05-27 09:08:15 +08:00
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt";
2025-05-29 05:25:00 +08:00
import PROMPT_TITLE from "./prompt/title.txt";
2025-05-27 09:08:15 +08:00
2025-05-20 23:11:06 +08:00
import type { Tool } from "../tool/tool";
2025-05-24 04:20:21 +08:00
import { Share } from "../share/share";
2025-05-20 23:11:06 +08:00
2025-05-18 09:31:42 +08:00
export namespace Session {
const log = Log.create({ service: "session" });
2025-05-19 10:30:41 +08:00
export const Info = z.object({
id: Identifier.schema("session"),
2025-05-24 04:20:21 +08:00
shareID: z.string().optional(),
2025-05-19 10:30:41 +08:00
title: z.string(),
});
export type Info = z.output<typeof Info>;
2025-05-18 09:31:42 +08:00
2025-05-20 23:11:06 +08:00
export type Message = UIMessage<{
2025-05-29 03:39:51 +08:00
assistant?: {
modelID: string;
providerID: string;
cost: number;
tokens: {
input: number;
output: number;
reasoning: number;
};
};
2025-05-20 23:11:06 +08:00
time: {
created: number;
completed?: number;
2025-05-20 23:11:06 +08:00
};
sessionID: string;
tool: Record<string, Tool.Metadata>;
}>;
2025-05-19 02:13:04 +08:00
2025-05-18 14:43:01 +08:00
const state = App.state("session", () => {
const sessions = new Map<string, Info>();
2025-05-19 02:13:04 +08:00
const messages = new Map<string, Message[]>();
2025-05-18 14:43:01 +08:00
return {
sessions,
messages,
};
});
2025-05-18 09:31:42 +08:00
export async function create() {
const result: Info = {
2025-05-18 14:50:38 +08:00
id: Identifier.descending("session"),
2025-05-18 09:31:42 +08:00
title: "New Session - " + new Date().toISOString(),
};
log.info("created", result);
2025-05-18 14:43:01 +08:00
state().sessions.set(result.id, result);
2025-05-29 03:39:51 +08:00
await Storage.writeJSON("session/info/" + result.id, result);
2025-05-29 05:34:37 +08:00
await share(result.id);
2025-05-18 09:31:42 +08:00
return result;
}
2025-05-18 14:43:01 +08:00
export async function get(id: string) {
const result = state().sessions.get(id);
if (result) {
return result;
}
2025-05-19 02:13:04 +08:00
const read = await Storage.readJSON<Info>("session/info/" + id);
2025-05-18 14:43:01 +08:00
state().sessions.set(id, read);
2025-05-19 02:13:04 +08:00
return read as Info;
}
2025-05-24 04:20:21 +08:00
export async function share(id: string) {
const session = await get(id);
if (session.shareID) return session.shareID;
const shareID = await Share.create(id);
if (!shareID) return;
await update(id, (draft) => {
draft.shareID = shareID;
2025-05-27 09:13:46 +08:00
});
2025-05-27 06:06:41 +08:00
return shareID as string;
2025-05-24 04:20:21 +08:00
}
2025-05-27 09:13:46 +08:00
export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state();
const session = await get(id);
2025-05-27 09:13:46 +08:00
if (!session) return;
editor(session);
sessions.set(id, session);
await Storage.writeJSON("session/info/" + id, session);
2025-05-27 09:13:46 +08:00
return session;
2025-05-18 14:43:01 +08:00
}
export async function messages(sessionID: string) {
2025-05-19 02:13:04 +08:00
const match = state().messages.get(sessionID);
if (match) {
return match;
}
const result = [] as Message[];
2025-05-27 01:21:15 +08:00
const list = Storage.list("session/message/" + sessionID);
for await (const p of list) {
const read = await Storage.readJSON<Message>(p);
2025-05-19 02:13:04 +08:00
result.push(read);
2025-05-18 14:43:01 +08:00
}
2025-05-19 02:13:04 +08:00
state().messages.set(sessionID, result);
return result;
2025-05-18 14:43:01 +08:00
}
export async function* list() {
2025-05-27 01:21:15 +08:00
for await (const item of Storage.list("session/info")) {
2025-05-28 03:34:46 +08:00
const sessionID = path.basename(item, ".json");
yield get(sessionID);
2025-05-18 14:43:01 +08:00
}
}
2025-05-29 03:07:51 +08:00
const pending = new Map<string, AbortController>();
export function abort(sessionID: string) {
const controller = pending.get(sessionID);
if (!controller) return false;
controller.abort();
pending.delete(sessionID);
return true;
}
2025-05-29 00:53:22 +08:00
export async function chat(input: {
sessionID: string;
providerID: string;
modelID: string;
parts: UIMessagePart<UIDataTypes>[];
}) {
const l = log.clone().tag("session", input.sessionID);
2025-05-18 14:43:01 +08:00
l.info("chatting");
2025-05-29 00:53:22 +08:00
const model = await LLM.findModel(input.providerID, input.modelID);
const msgs = await messages(input.sessionID);
2025-05-19 02:13:04 +08:00
async function write(msg: Message) {
return Storage.writeJSON(
2025-05-29 00:53:22 +08:00
"session/message/" + input.sessionID + "/" + msg.id,
2025-05-19 02:13:04 +08:00
msg,
);
}
const app = await App.use();
2025-05-19 02:13:04 +08:00
if (msgs.length === 0) {
2025-05-20 23:11:06 +08:00
const system: Message = {
2025-05-18 14:50:38 +08:00
id: Identifier.ascending("message"),
2025-05-18 14:43:01 +08:00
role: "system",
parts: [
{
type: "text",
2025-05-27 09:08:15 +08:00
text: PROMPT_ANTHROPIC,
2025-05-18 14:43:01 +08:00
},
],
2025-05-19 02:13:04 +08:00
metadata: {
2025-05-29 00:53:22 +08:00
sessionID: input.sessionID,
2025-05-20 23:11:06 +08:00
time: {
created: Date.now(),
},
tool: {},
2025-05-19 02:13:04 +08:00
},
};
const contextFile = Bun.file(path.join(app.root, "CONTEXT.md"));
if (await contextFile.exists()) {
const context = await contextFile.text();
system.parts.push({
type: "text",
text: context,
});
}
2025-05-19 02:13:04 +08:00
msgs.push(system);
2025-05-29 00:53:22 +08:00
state().messages.set(input.sessionID, msgs);
2025-05-27 09:08:15 +08:00
generateText({
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_TITLE,
},
],
},
{
role: "user",
2025-05-29 00:53:22 +08:00
parts: input.parts,
2025-05-27 09:08:15 +08:00
},
]),
2025-05-29 01:57:02 +08:00
model: model.instance,
2025-05-27 09:13:46 +08:00
}).then((result) => {
2025-05-29 00:53:22 +08:00
return Session.update(input.sessionID, (draft) => {
draft.title = result.text;
2025-05-27 09:13:46 +08:00
});
2025-05-27 09:08:15 +08:00
});
2025-05-19 02:13:04 +08:00
await write(system);
2025-05-18 14:43:01 +08:00
}
2025-05-19 02:13:04 +08:00
const msg: Message = {
role: "user",
id: Identifier.ascending("message"),
2025-05-29 00:53:22 +08:00
parts: input.parts,
2025-05-19 02:13:04 +08:00
metadata: {
2025-05-20 23:11:06 +08:00
time: {
created: Date.now(),
},
2025-05-29 00:53:22 +08:00
sessionID: input.sessionID,
2025-05-20 23:11:06 +08:00
tool: {},
2025-05-19 02:13:04 +08:00
},
};
msgs.push(msg);
await write(msg);
2025-05-18 14:43:01 +08:00
2025-05-29 00:53:22 +08:00
const next: Message = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
metadata: {
2025-05-29 03:39:51 +08:00
assistant: {
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
},
modelID: input.modelID,
providerID: input.providerID,
},
2025-05-29 00:53:22 +08:00
time: {
created: Date.now(),
},
sessionID: input.sessionID,
tool: {},
},
};
2025-05-29 03:07:51 +08:00
const controller = new AbortController();
pending.set(input.sessionID, controller);
2025-05-18 14:43:01 +08:00
const result = streamText({
2025-05-29 03:39:51 +08:00
onStepFinish: async (step) => {
const assistant = next.metadata!.assistant!;
assistant.tokens.input = step.usage.inputTokens ?? 0;
assistant.tokens.output = step.usage.outputTokens ?? 0;
assistant.tokens.reasoning = step.usage.reasoningTokens ?? 0;
assistant.cost = new Decimal(0)
.add(new Decimal(assistant.tokens.input).mul(model.info.cost.input))
.add(new Decimal(assistant.tokens.output).mul(model.info.cost.output))
.toNumber();
await write(next);
2025-05-27 10:35:30 +08:00
},
2025-05-29 03:07:51 +08:00
abortSignal: controller.signal,
maxRetries: 6,
2025-05-27 02:09:17 +08:00
stopWhen: stepCountIs(1000),
2025-05-18 14:43:01 +08:00
messages: convertToModelMessages(msgs),
temperature: 0,
2025-05-21 22:30:39 +08:00
tools,
2025-05-29 01:57:02 +08:00
model: model.instance,
2025-05-18 14:43:01 +08:00
});
2025-05-21 22:30:39 +08:00
2025-05-18 14:43:01 +08:00
msgs.push(next);
let text: TextUIPart | undefined;
const reader = result.toUIMessageStream().getReader();
while (true) {
2025-05-29 03:07:51 +08:00
const result = await reader.read().catch((e) => {
if (e instanceof DOMException && e.name === "AbortError") {
return;
}
throw e;
});
if (!result) break;
const { done, value } = result;
2025-05-18 14:43:01 +08:00
if (done) break;
2025-05-19 02:13:04 +08:00
l.info("part", {
type: value.type,
});
2025-05-18 14:43:01 +08:00
switch (value.type) {
case "start":
break;
case "start-step":
2025-05-20 07:29:38 +08:00
text = undefined;
2025-05-18 14:43:01 +08:00
next.parts.push({
type: "step-start",
});
break;
case "text":
if (!text) {
text = value;
next.parts.push(value);
break;
}
text.text += value.text;
break;
case "tool-call":
next.parts.push({
type: "tool-invocation",
toolInvocation: {
state: "call",
...value,
},
});
break;
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId,
) as ToolInvocationUIPart | undefined;
if (match) {
2025-05-20 23:11:06 +08:00
const { output, metadata } = value.result as any;
next.metadata!.tool[value.toolCallId] = metadata;
2025-05-18 14:43:01 +08:00
match.toolInvocation = {
...match.toolInvocation,
state: "result",
2025-05-20 23:11:06 +08:00
result: output,
2025-05-18 14:43:01 +08:00
};
}
break;
case "finish":
break;
case "finish-step":
2025-05-19 02:13:04 +08:00
break;
case "error":
log.error("error", value);
2025-05-18 14:43:01 +08:00
break;
default:
l.info("unhandled", {
type: value.type,
});
}
2025-05-19 02:13:04 +08:00
await write(next);
2025-05-18 14:43:01 +08:00
}
2025-05-29 03:07:51 +08:00
pending.delete(input.sessionID);
next.metadata!.time.completed = Date.now();
await write(next);
2025-05-19 02:13:04 +08:00
return next;
2025-05-18 14:43:01 +08:00
}
2025-05-18 09:31:42 +08:00
}