Refactor runtime around isolated bot instances

This commit is contained in:
zenord
2026-08-16 23:45:09 +08:00
parent 914579a94c
commit f9fb4ef776
51 changed files with 2355 additions and 1459 deletions
+39 -41
View File
@@ -1,9 +1,8 @@
import type { AcpConfig } from "../config.js";
import { chatKeyFor, type DurableSessionStore, type SessionBinding } from "../core/durable-session-store.js";
import type { RoleRegistry } from "../roles/role-registry.js";
import type { AcpBackendRegistry } from "./backend-registry.js";
import type { ResolvedBot } from "../roles/role-registry.js";
import type { ConversationRequest, ConversationResponse, ConversationRuntime, RuntimeStats } from "./types.js";
import { AcpWorker } from "./worker.js";
import { AcpSessionRestoreError, AcpWorker } from "./worker.js";
export class AcpSessionManager implements ConversationRuntime {
private readonly workers = new Map<string, AcpWorker>();
@@ -14,8 +13,7 @@ export class AcpSessionManager implements ConversationRuntime {
constructor(
private readonly config: AcpConfig,
private readonly backends: AcpBackendRegistry,
private readonly roles: RoleRegistry,
private readonly bot: ResolvedBot,
private readonly store: DurableSessionStore
) {
this.sweeper = setInterval(() => void this.sweep(), config.sweepIntervalMs);
@@ -25,30 +23,36 @@ export class AcpSessionManager implements ConversationRuntime {
async prompt(request: ConversationRequest): Promise<ConversationResponse> {
if (this.shuttingDown) throw new Error("ACP runtime is shutting down");
const chatKey = chatKeyFor(request.platform, request.chatId);
const role = this.roles.get(this.store.getSelectedRole(chatKey, this.roles.defaultId()));
let binding = this.store.getBinding(chatKey, role.id);
if (binding && (binding.roleFingerprint !== role.fingerprint || binding.backendId !== role.backend || binding.workspace !== role.workspace)) {
let binding = this.store.getBinding(chatKey);
if (binding && (binding.botFingerprint !== this.bot.fingerprint || binding.agentId !== this.bot.agent.id || binding.workspace !== this.bot.workspace)) {
await this.dropBinding(binding);
binding = undefined;
}
const worker = await this.acquireWorker(role.id, binding);
let worker: AcpWorker;
try { worker = await this.acquireWorker(binding); }
catch (error) {
if (!(error instanceof AcpSessionRestoreError) || !binding) throw error;
await this.store.deleteBinding(chatKey);
binding = undefined;
worker = await this.acquireWorker();
}
this.inFlight.set(chatKey, worker);
try {
if (!binding) {
const now = Date.now();
await worker.prompt(role.bootstrap);
await worker.prompt(this.bot.bootstrap);
binding = {
chatKey, roleId: role.id, backendId: role.backend, nativeSessionId: worker.nativeSessionId!,
workspace: role.workspace, roleFingerprint: role.fingerprint, createdAt: now, updatedAt: now
chatKey, agentId: this.bot.agent.id, nativeSessionId: worker.nativeSessionId!,
workspace: this.bot.workspace, botFingerprint: this.bot.fingerprint, createdAt: now, updatedAt: now
};
await this.store.setBinding(binding);
}
const text = await worker.prompt(request.text);
await this.store.touchBinding(chatKey, role.id);
return { text: text || "(ACP agent returned no text)", roleId: role.id, backendId: role.backend };
await this.store.touchBinding(chatKey);
return { text: text || "(ACP agent returned no text)", botId: this.bot.id, agentId: this.bot.agent.id };
} catch (error) {
if (worker.nativeSessionId) this.workers.delete(workerKey(worker.backend.id, worker.nativeSessionId));
if (worker.nativeSessionId) this.workers.delete(workerKey(this.bot.agent.id, worker.nativeSessionId));
await worker.terminate();
throw error;
} finally {
@@ -64,25 +68,20 @@ export class AcpSessionManager implements ConversationRuntime {
async reset(platform: string, chatId: string): Promise<void> {
const chatKey = chatKeyFor(platform, chatId);
await this.cancel(platform, chatId);
const roleId = this.store.getSelectedRole(chatKey, this.roles.defaultId());
const binding = await this.store.deleteBinding(chatKey, roleId);
if (binding) await this.stopWorker(binding.backendId, binding.nativeSessionId);
}
async selectRole(platform: string, chatId: string, roleId: string): Promise<void> {
this.roles.get(roleId);
await this.store.setSelectedRole(chatKeyFor(platform, chatId), roleId);
}
selectedRole(platform: string, chatId: string): string {
return this.store.getSelectedRole(chatKeyFor(platform, chatId), this.roles.defaultId());
const binding = await this.store.deleteBinding(chatKey);
if (binding) await this.stopWorker(binding.agentId, binding.nativeSessionId);
}
status(platform: string, chatId: string): Record<string, string | number | boolean> {
const chatKey = chatKeyFor(platform, chatId);
const roleId = this.store.getSelectedRole(chatKey, this.roles.defaultId());
const binding = this.store.getBinding(chatKey, roleId);
return { role: roleId, backend: this.roles.get(roleId).backend, persisted: Boolean(binding), running: this.inFlight.has(chatKey) };
const binding = this.store.getBinding(chatKey);
return {
bot: this.bot.id,
agent: this.bot.agent.id,
workspace: this.bot.workspace,
persisted: Boolean(binding),
running: this.inFlight.has(chatKey)
};
}
stats(): RuntimeStats {
@@ -99,20 +98,19 @@ export class AcpSessionManager implements ConversationRuntime {
this.inFlight.clear();
}
private async acquireWorker(roleId: string, binding?: SessionBinding): Promise<AcpWorker> {
const role = this.roles.get(roleId);
private async acquireWorker(binding?: SessionBinding): Promise<AcpWorker> {
if (binding) {
const existing = this.workers.get(workerKey(binding.backendId, binding.nativeSessionId));
const existing = this.workers.get(workerKey(binding.agentId, binding.nativeSessionId));
if (existing) return existing;
}
await this.ensureCapacity();
const worker = new AcpWorker(this.backends.get(role.backend), role, this.config, (crashed, error) => {
const worker = new AcpWorker(this.bot, this.config, (crashed, error) => {
this.crashes++;
if (crashed.nativeSessionId) this.workers.delete(workerKey(crashed.backend.id, crashed.nativeSessionId));
if (crashed.nativeSessionId) this.workers.delete(workerKey(this.bot.agent.id, crashed.nativeSessionId));
console.error(`ACP worker crash: ${error.message}`);
});
const nativeSessionId = await worker.start(binding?.nativeSessionId);
this.workers.set(workerKey(role.backend, nativeSessionId), worker);
this.workers.set(workerKey(this.bot.agent.id, nativeSessionId), worker);
return worker;
}
@@ -134,12 +132,12 @@ export class AcpSessionManager implements ConversationRuntime {
}
private async dropBinding(binding: SessionBinding): Promise<void> {
await this.store.deleteBinding(binding.chatKey, binding.roleId);
await this.stopWorker(binding.backendId, binding.nativeSessionId);
await this.store.deleteBinding(binding.chatKey);
await this.stopWorker(binding.agentId, binding.nativeSessionId);
}
private async stopWorker(backendId: string, nativeSessionId: string): Promise<void> {
const key = workerKey(backendId, nativeSessionId);
private async stopWorker(agentId: string, nativeSessionId: string): Promise<void> {
const key = workerKey(agentId, nativeSessionId);
const worker = this.workers.get(key);
if (!worker) return;
this.workers.delete(key);
@@ -147,4 +145,4 @@ export class AcpSessionManager implements ConversationRuntime {
}
}
function workerKey(backendId: string, nativeSessionId: string): string { return `${backendId}:${nativeSessionId}`; }
function workerKey(agentId: string, nativeSessionId: string): string { return `${agentId}:${nativeSessionId}`; }