opencode/js/src/session/session.ts

270 lines
6.6 KiB
TypeScript
Raw Normal View History

2025-05-18 14:43:01 +08:00
import path from "path";
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-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-18 09:31:42 +08:00
2025-05-20 23:11:06 +08:00
import ANTHROPIC_PROMPT from "./prompt/anthropic.txt";
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(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
}),
});
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<{
time: {
created: number;
};
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(),
2025-05-19 02:13:04 +08:00
tokens: {
input: 0,
output: 0,
reasoning: 0,
},
2025-05-18 09:31:42 +08:00
};
log.info("created", result);
2025-05-19 02:13:04 +08:00
await Storage.writeJSON("session/info/" + result.id, result);
2025-05-18 14:43:01 +08:00
state().sessions.set(result.id, result);
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;
session.shareID = shareID;
await update(session);
2025-05-27 06:06:41 +08:00
return shareID as string;
2025-05-24 04:20:21 +08:00
}
2025-05-19 02:13:04 +08:00
export async function update(session: Info) {
state().sessions.set(session.id, session);
await Storage.writeJSON("session/info/" + session.id, 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")) {
yield path.basename(item, ".json");
2025-05-18 14:43:01 +08:00
}
}
2025-05-19 02:13:04 +08:00
export async function chat(
sessionID: string,
...parts: UIMessagePart<UIDataTypes>[]
) {
const session = await get(sessionID);
2025-05-18 14:43:01 +08:00
const l = log.clone().tag("session", sessionID);
l.info("chatting");
2025-05-19 02:13:04 +08:00
const msgs = await messages(sessionID);
async function write(msg: Message) {
return Storage.writeJSON(
"session/message/" + sessionID + "/" + msg.id,
msg,
);
}
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-20 07:29:38 +08:00
text: ANTHROPIC_PROMPT,
2025-05-18 14:43:01 +08:00
},
],
2025-05-19 02:13:04 +08:00
metadata: {
sessionID,
2025-05-20 23:11:06 +08:00
time: {
created: Date.now(),
},
tool: {},
2025-05-19 02:13:04 +08:00
},
};
msgs.push(system);
state().messages.set(sessionID, msgs);
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"),
parts,
metadata: {
2025-05-20 23:11:06 +08:00
time: {
created: Date.now(),
},
2025-05-19 02:13:04 +08:00
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-27 01:44:59 +08:00
const model = await LLM.findModel("claude-sonnet-4-20250514");
2025-05-18 14:43:01 +08:00
const result = streamText({
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-18 14:43:01 +08:00
model,
});
2025-05-19 02:13:04 +08:00
const next: Message = {
2025-05-18 14:50:38 +08:00
id: Identifier.ascending("message"),
2025-05-18 14:43:01 +08:00
role: "assistant",
parts: [],
2025-05-19 02:13:04 +08:00
metadata: {
2025-05-20 23:11:06 +08:00
time: {
created: Date.now(),
},
2025-05-19 02:13:04 +08:00
sessionID,
2025-05-20 23:11:06 +08:00
tool: {},
2025-05-19 02:13:04 +08:00
},
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) {
const { done, value } = await reader.read();
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-19 02:13:04 +08:00
const usage = await result.totalUsage;
session.tokens.input += usage.inputTokens || 0;
session.tokens.output += usage.outputTokens || 0;
session.tokens.reasoning += usage.reasoningTokens || 0;
2025-05-27 01:21:15 +08:00
console.log(session);
2025-05-19 02:13:04 +08:00
await update(session);
return next;
2025-05-18 14:43:01 +08:00
}
2025-05-18 09:31:42 +08:00
}