cloudaxe-opencode/js/src/session/session.ts

352 lines
9.1 KiB
TypeScript
Raw Normal View History

2025-05-28 21:24:15 +00:00
import path from "path";
2025-05-18 06:43:01 +00:00
import { App } from "../app/";
2025-05-18 01:31:42 +00:00
import { Identifier } from "../id/id";
2025-05-18 06:43:01 +00:00
import { LLM } from "../llm/llm";
2025-05-18 01:31:42 +00:00
import { Storage } from "../storage/storage";
import { Log } from "../util/log";
2025-05-18 06:43:01 +00:00
import {
convertToModelMessages,
2025-05-28 21:24:15 +00:00
generateText,
2025-05-26 18:09:17 +00:00
stepCountIs,
2025-05-18 06:43:01 +00:00
streamText,
} from "ai";
2025-05-19 02:30:41 +00:00
import { z } from "zod";
2025-05-29 15:17:34 +00:00
import { z as zv4 } from "zod/v4";
2025-05-21 14:30:39 +00:00
import * as tools from "../tool";
2025-05-28 17:57:02 +00:00
import { Decimal } from "decimal.js";
2025-05-18 01:31:42 +00:00
2025-05-27 01:08:15 +00:00
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt";
2025-05-28 21:25:00 +00:00
import PROMPT_TITLE from "./prompt/title.txt";
2025-05-27 01:08:15 +00:00
2025-05-23 20:20:21 +00:00
import { Share } from "../share/share";
2025-05-29 15:17:34 +00:00
import { Message } from "./message";
import { Bus } from "../bus";
2025-05-20 15:11:06 +00:00
2025-05-18 01:31:42 +00:00
export namespace Session {
const log = Log.create({ service: "session" });
2025-05-19 02:30:41 +00:00
export const Info = z.object({
id: Identifier.schema("session"),
2025-05-23 20:20:21 +00:00
shareID: z.string().optional(),
2025-05-19 02:30:41 +00:00
title: z.string(),
});
export type Info = z.output<typeof Info>;
2025-05-18 01:31:42 +00:00
2025-05-29 15:17:34 +00:00
export const Event = {
Updated: Bus.event(
"session.updated",
zv4.object({
sessionID: zv4.string(),
}),
),
};
2025-05-18 06:43:01 +00:00
const state = App.state("session", () => {
const sessions = new Map<string, Info>();
2025-05-29 14:21:59 +00:00
const messages = new Map<string, Message.Info[]>();
2025-05-18 06:43:01 +00:00
return {
sessions,
messages,
};
});
2025-05-18 01:31:42 +00:00
export async function create() {
const result: Info = {
2025-05-18 06:50:38 +00:00
id: Identifier.descending("session"),
2025-05-18 01:31:42 +00:00
title: "New Session - " + new Date().toISOString(),
};
log.info("created", result);
2025-05-18 06:43:01 +00:00
state().sessions.set(result.id, result);
2025-05-28 19:39:51 +00:00
await Storage.writeJSON("session/info/" + result.id, result);
2025-05-28 21:34:37 +00:00
await share(result.id);
2025-05-29 15:17:34 +00:00
Bus.publish(Event.Updated, {
sessionID: result.id,
});
2025-05-18 01:31:42 +00:00
return result;
}
2025-05-18 06:43:01 +00:00
export async function get(id: string) {
const result = state().sessions.get(id);
if (result) {
return result;
}
2025-05-18 18:13:04 +00:00
const read = await Storage.readJSON<Info>("session/info/" + id);
2025-05-18 06:43:01 +00:00
state().sessions.set(id, read);
2025-05-18 18:13:04 +00:00
return read as Info;
}
2025-05-23 20:20:21 +00: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 01:13:46 +00:00
});
2025-05-26 22:06:41 +00:00
return shareID as string;
2025-05-23 20:20:21 +00:00
}
2025-05-27 01:13:46 +00:00
export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state();
const session = await get(id);
2025-05-27 01:13:46 +00:00
if (!session) return;
editor(session);
sessions.set(id, session);
await Storage.writeJSON("session/info/" + id, session);
2025-05-29 15:17:34 +00:00
Bus.publish(Event.Updated, {
sessionID: id,
});
2025-05-27 01:13:46 +00:00
return session;
2025-05-18 06:43:01 +00:00
}
export async function messages(sessionID: string) {
2025-05-18 18:13:04 +00:00
const match = state().messages.get(sessionID);
if (match) {
return match;
}
2025-05-29 14:21:59 +00:00
const result = [] as Message.Info[];
2025-05-26 17:21:15 +00:00
const list = Storage.list("session/message/" + sessionID);
for await (const p of list) {
2025-05-29 14:21:59 +00:00
const read = await Storage.readJSON<Message.Info>(p);
2025-05-18 18:13:04 +00:00
result.push(read);
2025-05-18 06:43:01 +00:00
}
2025-05-18 18:13:04 +00:00
state().messages.set(sessionID, result);
return result;
2025-05-18 06:43:01 +00:00
}
export async function* list() {
2025-05-26 17:21:15 +00:00
for await (const item of Storage.list("session/info")) {
2025-05-27 19:34:46 +00:00
const sessionID = path.basename(item, ".json");
yield get(sessionID);
2025-05-18 06:43:01 +00:00
}
}
2025-05-28 19:07:51 +00: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-28 16:53:22 +00:00
export async function chat(input: {
sessionID: string;
providerID: string;
modelID: string;
2025-05-29 14:21:59 +00:00
parts: Message.Part[];
2025-05-28 16:53:22 +00:00
}) {
const l = log.clone().tag("session", input.sessionID);
2025-05-18 06:43:01 +00:00
l.info("chatting");
2025-05-28 16:53:22 +00:00
const model = await LLM.findModel(input.providerID, input.modelID);
const msgs = await messages(input.sessionID);
2025-05-29 14:21:59 +00:00
async function write(msg: Message.Info) {
2025-05-29 15:17:34 +00:00
await Storage.writeJSON(
2025-05-28 16:53:22 +00:00
"session/message/" + input.sessionID + "/" + msg.id,
2025-05-18 18:13:04 +00:00
msg,
);
2025-05-29 15:17:34 +00:00
Bus.publish(Message.Event.Updated, {
sessionID: input.sessionID,
messageID: msg.id,
});
2025-05-18 18:13:04 +00:00
}
const app = await App.use();
2025-05-18 18:13:04 +00:00
if (msgs.length === 0) {
2025-05-29 14:21:59 +00:00
const system: Message.Info = {
2025-05-18 06:50:38 +00:00
id: Identifier.ascending("message"),
2025-05-18 06:43:01 +00:00
role: "system",
parts: [
{
type: "text",
2025-05-27 01:08:15 +00:00
text: PROMPT_ANTHROPIC,
2025-05-18 06:43:01 +00:00
},
],
2025-05-18 18:13:04 +00:00
metadata: {
2025-05-28 16:53:22 +00:00
sessionID: input.sessionID,
2025-05-20 15:11:06 +00:00
time: {
created: Date.now(),
},
tool: {},
2025-05-18 18:13:04 +00: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-18 18:13:04 +00:00
msgs.push(system);
2025-05-28 16:53:22 +00:00
state().messages.set(input.sessionID, msgs);
2025-05-27 01:08:15 +00:00
generateText({
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_TITLE,
},
],
},
{
role: "user",
2025-05-28 16:53:22 +00:00
parts: input.parts,
2025-05-27 01:08:15 +00:00
},
]),
2025-05-28 17:57:02 +00:00
model: model.instance,
2025-05-27 01:13:46 +00:00
}).then((result) => {
2025-05-28 16:53:22 +00:00
return Session.update(input.sessionID, (draft) => {
draft.title = result.text;
2025-05-27 01:13:46 +00:00
});
2025-05-27 01:08:15 +00:00
});
2025-05-18 18:13:04 +00:00
await write(system);
2025-05-18 06:43:01 +00:00
}
2025-05-29 14:21:59 +00:00
const msg: Message.Info = {
2025-05-18 18:13:04 +00:00
role: "user",
id: Identifier.ascending("message"),
2025-05-28 16:53:22 +00:00
parts: input.parts,
2025-05-18 18:13:04 +00:00
metadata: {
2025-05-20 15:11:06 +00:00
time: {
created: Date.now(),
},
2025-05-28 16:53:22 +00:00
sessionID: input.sessionID,
2025-05-20 15:11:06 +00:00
tool: {},
2025-05-18 18:13:04 +00:00
},
};
msgs.push(msg);
await write(msg);
2025-05-18 06:43:01 +00:00
2025-05-29 14:21:59 +00:00
const next: Message.Info = {
2025-05-28 16:53:22 +00:00
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
metadata: {
2025-05-28 19:39:51 +00:00
assistant: {
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
},
modelID: input.modelID,
providerID: input.providerID,
},
2025-05-28 16:53:22 +00:00
time: {
created: Date.now(),
},
sessionID: input.sessionID,
tool: {},
},
};
2025-05-28 19:07:51 +00:00
const controller = new AbortController();
pending.set(input.sessionID, controller);
2025-05-18 06:43:01 +00:00
const result = streamText({
2025-05-28 19:39:51 +00: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 02:35:30 +00:00
},
2025-05-28 19:07:51 +00:00
abortSignal: controller.signal,
maxRetries: 6,
2025-05-26 18:09:17 +00:00
stopWhen: stepCountIs(1000),
2025-05-18 06:43:01 +00:00
messages: convertToModelMessages(msgs),
temperature: 0,
2025-05-21 14:30:39 +00:00
tools,
2025-05-28 17:57:02 +00:00
model: model.instance,
2025-05-18 06:43:01 +00:00
});
2025-05-21 14:30:39 +00:00
2025-05-18 06:43:01 +00:00
msgs.push(next);
2025-05-29 14:21:59 +00:00
let text: Message.TextPart | undefined;
2025-05-18 06:43:01 +00:00
const reader = result.toUIMessageStream().getReader();
while (true) {
2025-05-28 19:07:51 +00: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 06:43:01 +00:00
if (done) break;
2025-05-18 18:13:04 +00:00
l.info("part", {
type: value.type,
});
2025-05-18 06:43:01 +00:00
switch (value.type) {
case "start":
break;
case "start-step":
2025-05-19 23:29:38 +00:00
text = undefined;
2025-05-18 06:43:01 +00: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,
2025-05-29 14:21:59 +00:00
// hack until zod v4
args: value.args as any,
2025-05-18 06:43:01 +00:00
},
});
break;
case "tool-result":
const match = next.parts.find(
(p) =>
p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId,
2025-05-29 14:21:59 +00:00
);
if (match && match.type === "tool-invocation") {
2025-05-20 15:11:06 +00:00
const { output, metadata } = value.result as any;
next.metadata!.tool[value.toolCallId] = metadata;
2025-05-18 06:43:01 +00:00
match.toolInvocation = {
...match.toolInvocation,
state: "result",
2025-05-20 15:11:06 +00:00
result: output,
2025-05-18 06:43:01 +00:00
};
}
break;
case "finish":
break;
case "finish-step":
2025-05-18 18:13:04 +00:00
break;
case "error":
log.error("error", value);
2025-05-18 06:43:01 +00:00
break;
default:
l.info("unhandled", {
type: value.type,
});
}
2025-05-18 18:13:04 +00:00
await write(next);
2025-05-18 06:43:01 +00:00
}
2025-05-28 19:07:51 +00:00
pending.delete(input.sessionID);
next.metadata!.time.completed = Date.now();
await write(next);
2025-05-18 18:13:04 +00:00
return next;
2025-05-18 06:43:01 +00:00
}
2025-05-18 01:31:42 +00:00
}