feat: initialize experimental agent repo

This commit is contained in:
2026-06-30 14:03:49 +08:00
commit e83ac54a9e
75 changed files with 11690 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
export type SupportedModel = string;
export type AgentModelIcon = "bolt" | "sparkle";
export type AgentModelOption = {
id: SupportedModel;
label: string;
description: string;
icon: AgentModelIcon;
};
export const defaultAgentModelOptions: AgentModelOption[] = [
{
id: "deepseek/deepseek-v4-flash",
label: "快速",
description: "快速回答和任务执行",
icon: "bolt",
},
{
id: "deepseek/deepseek-v4-pro",
label: "专家",
description: "探索、解决复杂任务",
icon: "sparkle",
},
];
export const defaultAgentModelOptionsJson = JSON.stringify(defaultAgentModelOptions);
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const parseAgentModelOptions = (value: string): AgentModelOption[] => {
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error("OPENCODE_MODEL_OPTIONS must be valid JSON");
}
if (!Array.isArray(parsed)) {
throw new Error("OPENCODE_MODEL_OPTIONS must be a JSON array");
}
const seen = new Set<string>();
return parsed.map((item, index) => {
if (!isObjectRecord(item)) {
throw new Error(`OPENCODE_MODEL_OPTIONS[${index}] must be an object`);
}
const id = typeof item.id === "string" ? item.id.trim() : "";
if (!id) {
throw new Error(`OPENCODE_MODEL_OPTIONS[${index}].id is required`);
}
if (seen.has(id)) {
throw new Error(`duplicate OPENCODE_MODEL_OPTIONS id: ${id}`);
}
seen.add(id);
const label =
typeof item.label === "string" && item.label.trim()
? item.label.trim()
: id;
const description =
typeof item.description === "string" && item.description.trim()
? item.description.trim()
: label;
const icon = item.icon === "bolt" || item.icon === "sparkle"
? item.icon
: "sparkle";
return {
id,
label,
description,
icon,
};
});
};
export const getAgentModelIds = (options: AgentModelOption[]) =>
options.map((option) => option.id);
+28
View File
@@ -0,0 +1,28 @@
import { config } from "../config.js";
import {
getAgentModelIds,
parseAgentModelOptions,
type SupportedModel,
} from "./modelConfig.js";
export {
type AgentModelIcon,
type AgentModelOption,
type SupportedModel,
} from "./modelConfig.js";
export const agentModelOptions = parseAgentModelOptions(
config.OPENCODE_MODEL_OPTIONS,
);
export const supportedModels = getAgentModelIds(agentModelOptions);
export const isSupportedModel = (model: string): model is SupportedModel =>
supportedModels.includes(model);
export const resolveDefaultModel = (model: string): SupportedModel => {
if (!isSupportedModel(model)) {
throw new Error(`unsupported default agent model: ${model}`);
}
return model;
};
+183
View File
@@ -0,0 +1,183 @@
import { randomUUID } from "node:crypto";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import {
removeRuntimeSessionContext,
setRuntimeSessionContext,
} from "../runtime/sessionContext.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
export type SessionBinding = {
clientSessionId: string;
sessionId: string;
startedAt: number;
};
export type SessionContext = {
clientSessionId: string;
network?: string;
projectId?: string;
userId?: string;
};
export type ChatRequestContext = SessionContext & {
actorKey: string;
projectKey: string;
traceId: string;
};
export class ChatSessionBridge {
private readonly abortControllers = new Map<string, AbortController>();
constructor(private readonly runtime: OpencodeRuntimeAdapter) {}
async resolve(context: {
sessionId?: string;
network?: string;
projectId?: string;
traceId?: string;
userId?: string;
}): Promise<{
binding: SessionBinding;
requestContext: ChatRequestContext;
created: boolean;
}> {
let requestContext = this.buildRequestContext(context);
const existingSessionId = context.sessionId?.trim();
await this.abortActiveRuntime(requestContext.clientSessionId, existingSessionId);
let sessionId = existingSessionId;
let created = false;
if (!sessionId) {
const session = await this.runtime.createSession();
sessionId = session.id;
requestContext = {
...requestContext,
clientSessionId: sessionId,
};
created = true;
}
const binding: SessionBinding = {
clientSessionId: requestContext.clientSessionId,
sessionId,
startedAt: Date.now(),
};
setRuntimeSessionContext({
actorKey: requestContext.actorKey,
allowLearningWrite: true,
clientSessionId: requestContext.clientSessionId,
learningMode: "interactive",
network: requestContext.network,
projectId: requestContext.projectId,
projectKey: requestContext.projectKey,
sessionId,
traceId: requestContext.traceId,
});
return { binding, requestContext, created };
}
count(): number {
return this.abortControllers.size;
}
createClientSessionId() {
return `agent-${randomUUID().slice(0, 12)}`;
}
registerAbortController(clientSessionId: string, controller: AbortController) {
this.abortControllers.set(clientSessionId, controller);
}
finalizeRequest(clientSessionId: string) {
this.abortControllers.delete(clientSessionId);
}
async abort(context: {
clientSessionId?: string;
sessionId?: string;
}): Promise<SessionBinding | null> {
const clientSessionId = context.clientSessionId?.trim();
const sessionId = context.sessionId?.trim();
if (!clientSessionId || !sessionId) {
return null;
}
await this.abortActiveRuntime(clientSessionId, sessionId);
return {
clientSessionId,
sessionId,
startedAt: Date.now(),
};
}
async deleteSession(context: {
clientSessionId: string;
sessionId: string;
}) {
const clientSessionId = context.clientSessionId.trim();
const sessionId = context.sessionId.trim();
const controller = this.abortControllers.get(clientSessionId);
if (controller) {
this.abortControllers.delete(clientSessionId);
controller.abort();
}
await this.runtime.abortSession(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed to abort runtime session",
);
});
await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed while waiting for runtime session to become idle",
);
});
removeRuntimeSessionContext(sessionId);
}
private buildRequestContext(context: {
sessionId?: string;
network?: string;
projectId?: string;
traceId?: string;
userId?: string;
}): ChatRequestContext {
const sessionId = context.sessionId?.trim();
return {
clientSessionId: sessionId || this.createClientSessionId(),
actorKey: toActorKey(context.userId),
network: context.network,
projectId: context.projectId,
projectKey: toProjectKey(context.projectId),
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
userId: context.userId?.trim(),
};
}
private async abortActiveRuntime(clientSessionId: string, sessionId?: string) {
const controller = this.abortControllers.get(clientSessionId);
if (controller) {
this.abortControllers.delete(clientSessionId);
controller.abort();
}
if (!sessionId) {
return;
}
await this.runtime.abortSession(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed to abort active runtime session",
);
});
await this.runtime.waitForSessionIdle(sessionId).catch((error) => {
logger.warn(
{ clientSessionId, sessionId, err: error },
"failed while waiting for active runtime session to become idle",
);
});
}
}