Files
TJWaterAgent/src/runtime/opencode.ts
T

183 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
createOpencode,
createOpencodeClient,
type OpencodeClient,
} from "@opencode-ai/sdk/v2";
import { config } from "../config.js";
import { logger } from "../logger.js";
export type RuntimeHealth = {
healthy: boolean;
version: string;
};
type RuntimeModelOverride = {
providerID: string;
modelID: string;
};
export class OpencodeRuntimeAdapter {
private clientPromise: Promise<OpencodeClient> | null = null;
private closeServer: (() => void) | null = null;
async ensureClient(): Promise<OpencodeClient> {
if (!this.clientPromise) {
this.clientPromise = this.bootstrapClient();
}
return this.clientPromise;
}
async health(): Promise<RuntimeHealth> {
const client = await this.ensureClient();
const response = await client.global.health();
return requireData(response.data, "global.health");
}
async createSession(title?: string) {
const client = await this.ensureClient();
const response = await client.session.create({
title,
});
return requireData(response.data, "session.create");
}
async getSession(id: string) {
const client = await this.ensureClient();
const response = await client.session.get({
sessionID: id,
});
return requireData(response.data, "session.get");
}
async sendPrompt(sessionId: string, text: string) {
await this.prompt(sessionId, text);
// 当前 SDK 响应风格下,prompt() 本身不会直接返回完整 assistant parts
// 所以这里紧跟一次 messages() 回读,给上层路由统一消费。
return this.messages(sessionId);
}
async prompt(sessionId: string, text: string, model?: RuntimeModelOverride) {
const client = await this.ensureClient();
await client.session.prompt({
sessionID: sessionId,
model,
parts: [{ type: "text", text }],
});
}
async messages(sessionId: string, limit = 20) {
const client = await this.ensureClient();
const messages = await client.session.messages({
sessionID: sessionId,
limit,
});
return requireData(messages.data, "session.messages");
}
async forkSession(sessionId: string, messageId?: string) {
const client = await this.ensureClient();
const response = await client.session.fork({
sessionID: sessionId,
messageID: messageId,
});
return requireData(response.data, "session.fork");
}
async abortSession(sessionId: string) {
const client = await this.ensureClient();
const response = await client.session.abort({
sessionID: sessionId,
});
return requireData(response.data, "session.abort");
}
async subscribeEvents() {
const client = await this.ensureClient();
const response = await client.event.subscribe();
return response.stream;
}
async dispose(): Promise<void> {
this.closeServer?.();
this.closeServer = null;
this.clientPromise = null;
}
private async bootstrapClient(): Promise<OpencodeClient> {
if (config.OPENCODE_MODE === "client") {
logger.info(
{
baseUrl: config.OPENCODE_CLIENT_BASE_URL,
mode: config.OPENCODE_MODE,
},
"connecting to opencode server in client mode",
);
return createOpencodeClient({
baseUrl: config.OPENCODE_CLIENT_BASE_URL,
});
}
// embedded 模式下,把服务内工具桥地址注入到 opencode 进程环境里,
// 这样 .opencode/tools 下的自定义工具可以回调本服务。
process.env.TJWATER_AGENT_INTERNAL_BASE_URL = `http://127.0.0.1:${config.PORT}`;
process.env.TJWATER_AGENT_INTERNAL_TOKEN =
config.AGENT_INTERNAL_TOKEN ??
process.env.TJWATER_AGENT_INTERNAL_TOKEN ??
"";
logger.info(
{
hostname: config.OPENCODE_HOSTNAME,
port: config.OPENCODE_PORT,
model: config.OPENCODE_MODEL,
mode: config.OPENCODE_MODE,
},
"starting opencode server in embedded mode",
);
let runtime;
try {
runtime = await createOpencode({
hostname: config.OPENCODE_HOSTNAME,
port: config.OPENCODE_PORT,
timeout: config.OPENCODE_TIMEOUT_MS,
config: {
model: config.OPENCODE_MODEL,
},
});
} catch (error) {
if (isMissingOpencodeCli(error)) {
throw new Error(
"embedded mode requires the opencode CLI to be installed and available in PATH; otherwise set OPENCODE_MODE=client and provide OPENCODE_CLIENT_BASE_URL",
);
}
throw error;
}
this.closeServer = () => {
runtime.server.close();
};
return runtime.client;
}
}
export const opencodeRuntime = new OpencodeRuntimeAdapter();
function isMissingOpencodeCli(error: unknown): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
(error as NodeJS.ErrnoException).code === "ENOENT"
);
}
function requireData<T>(data: T | undefined, operation: string): T {
if (data === undefined) {
throw new Error(`${operation} returned no data`);
}
return data;
}